Compare commits

...

248 Commits

Author SHA1 Message Date
Daniel Saidi 6f74774953 Update documentation 2023-06-12 15:19:56 +02:00
Daniel Saidi 4913287915 Remove demo folder 2023-06-07 16:06:00 +02:00
Daniel Saidi 7a9e8f3234 Update readme 2023-02-09 09:00:02 +01:00
Daniel Saidi 32a752b640 Merge commit '7f8c9c2db593f39b356d09ba749d21701d106e96' 2023-02-09 08:57:11 +01:00
Daniel Saidi 68d7bcd272 Adjust documentation 2023-02-09 08:56:39 +01:00
Daniel Saidi 7f8c9c2db5
Merge pull request #2 from devraj/master
Fix method signature for the csv parser demo
2022-12-27 09:29:54 +01:00
Dev Mukherjee f61e2fdd81 fix: method signature for the csv parser demo
the csv string parser call was incorrectly spelt, the method signature had also
been updated, this fixes these for the demo project
2022-12-27 16:25:15 +11:00
Daniel Saidi 86753bf3ed Adjust csv parsing 2022-10-23 10:20:18 +02:00
Daniel Saidi 3d092ab2c2 Adjust formatting 2022-10-23 09:41:11 +02:00
Daniel Saidi 66589c6ccb Remove slugified string extension 2022-09-08 19:01:26 +02:00
Daniel Saidi 97c265f431 Bump to 1.3.0 2022-09-01 16:29:16 +02:00
Daniel Saidi 5a64443221 Bump macOS dependency to 11 2022-09-01 16:27:56 +02:00
Daniel Saidi a6b1077441 Adjust minimum macOS version 2022-09-01 16:23:06 +02:00
Daniel Saidi 651d94a048 Use watchOS supporting Quick & Nimble forks 2022-09-01 16:18:58 +02:00
Daniel Saidi 30cae98daf Disable unit test 2022-09-01 16:12:33 +02:00
Daniel Saidi 8e0dc7e858 Adjust random logic for dispatch queue 2022-09-01 16:10:33 +02:00
Daniel Saidi 12ce18a036 Update documentation 2022-09-01 16:07:13 +02:00
Daniel Saidi 0082707c69 Remove DocC plugin 2022-09-01 15:56:11 +02:00
Daniel Saidi d86a241982 Adjust Fastlane to build documentation for new versions 2022-09-01 15:55:17 +02:00
Daniel Saidi 1f9db1283e Deprecate slugified string exension 2022-08-18 22:33:47 +02:00
Daniel Saidi 9f9d8e45f2 Update demo 2022-06-20 15:24:29 +02:00
Daniel Saidi dbb1df6942 Deprecate the Messaging namespace 2022-06-20 15:04:51 +02:00
Daniel Saidi 0b99113342 Deprecate IoC namespace 2022-06-20 15:03:28 +02:00
Daniel Saidi e28cf84d3c Remove StoreKit from documentation 2022-06-20 14:59:45 +02:00
Daniel Saidi bab5074eec Deprecate StoreKit types 2022-06-20 12:43:10 +02:00
Daniel Saidi b0720dbe58 Update podspec file 2022-06-20 09:33:01 +02:00
Daniel Saidi 708a060067 Make async extensions available to all supported os versions 2022-06-20 09:25:08 +02:00
Daniel Saidi 12d9cc5758 Adjust documentation 2022-06-20 09:25:06 +02:00
Daniel Saidi d0aa08edf2 Move string coder protocols into the same file 2022-06-20 09:24:33 +02:00
Daniel Saidi 5d064644e9
Merge pull request #1 from SkiingIsFun123/patch-1
Update README.md
2022-06-14 08:45:39 +02:00
Liam 80ec239c65
Update README.md 2022-06-13 23:14:04 -07:00
Daniel Saidi 3be09bb381 Bump to 1.2.0 2022-05-10 21:18:15 +02:00
Daniel Saidi 2700b1aada Fix typo in Fastfile 2022-05-10 21:17:15 +02:00
Daniel Saidi 298c086262 Update external dependencies and bump the iOS deployment target 2022-05-10 21:16:55 +02:00
Daniel Saidi df8fbaba07 Add new LocalAuthenticationService type 2022-04-29 10:57:09 +02:00
Daniel Saidi 8de53563f7 Add an async authentication extension to LAContext 2022-04-29 10:37:11 +02:00
Daniel Saidi b7bbe6acb3 Update Fastfile documentation scripts 2022-04-27 13:42:06 +02:00
Daniel Saidi b23977cf0e Update readme and documentation 2022-04-27 13:39:11 +02:00
Daniel Saidi a0645690dc Bump to 1.1.0 2022-04-23 11:41:51 +02:00
Daniel Saidi e2d0307932 Update readme and release notes 2022-04-23 11:40:38 +02:00
Daniel Saidi f9d316aeb1 Update gitignore 2022-04-23 11:40:38 +02:00
Daniel Saidi cc589f16f1 Update failing tests 2022-04-23 11:40:38 +02:00
Daniel Saidi af91f7dc5a Adjust url encode for + 2022-04-23 11:38:33 +02:00
Daniel Saidi c3d401059a Add new extensions 2022-04-23 11:38:33 +02:00
Daniel Saidi ed6ced3f9d Make plain text error an NSAttributedString type 2022-04-23 11:38:33 +02:00
Daniel Saidi 7d9a661080 Add string utils 2022-01-06 23:45:01 +01:00
Daniel Saidi 9fe09382b5 Bump to 1.0.0 2021-11-29 17:08:32 +01:00
Daniel Saidi f30b5ea14b Fix failing test 2021-11-29 17:08:32 +01:00
Daniel Saidi c7fe7b20e0 Add NSAttributedString extensions 2021-11-29 17:08:32 +01:00
Daniel Saidi ee4e321e40 Cleanup demo app 2021-11-29 17:08:32 +01:00
Daniel Saidi 96b7b3de6a Update demo to use SwiftUIKit 2.2 2021-11-29 17:08:32 +01:00
Daniel Saidi bdd9c0544c Update docs 2021-11-29 17:08:32 +01:00
Daniel Saidi 91b96cb9f6 Add documentation link to readme 2021-11-29 17:08:32 +01:00
Daniel Saidi a2bd714cbb Add async Collection extensions 2021-11-29 17:08:32 +01:00
Daniel Saidi 9505a4ff73 Add StoreKit utils 2021-11-29 17:07:40 +01:00
Daniel Saidi 05d92c0a85 Add a date components extension 2021-11-03 15:37:04 +01:00
Daniel Saidi 53e77cddf0 Add display name to Bundle/BundleInformation 2021-11-03 12:56:54 +01:00
Daniel Saidi 214069fa72 Improve Authentication documentation 2021-11-03 11:16:01 +01:00
Daniel Saidi 08d7761955 Add documentation lane to Fastlane 2021-11-03 11:00:41 +01:00
Daniel Saidi 3be8310ebb Add DocC documentation archive 2021-11-03 10:56:45 +01:00
Daniel Saidi 8616c3fc76 Update demo to use SwiftKit 0.7 and SwiftUIKit 2.0 2021-09-23 08:51:28 +02:00
Daniel Saidi 3cd5182ee8 Bump to 0.7.0 2021-09-23 07:08:13 +02:00
Daniel Saidi b2a33fecc5 Add throttle and debounce extensions for DispatchQueue 2021-09-17 16:42:06 +02:00
Daniel Saidi e1b0530520 Update services documentation 2021-09-08 14:46:23 +02:00
Daniel Saidi c8885efa1a Update messanging documentation 2021-09-08 14:31:26 +02:00
Daniel Saidi c046f62bcd Update localization documentation 2021-09-08 12:06:36 +02:00
Daniel Saidi f373cce6c7 Update iCloud documentation 2021-09-08 12:04:00 +02:00
Daniel Saidi a8b55ba005 Make CLLocationCoordinate2D implement Equatable 2021-09-08 12:02:12 +02:00
Daniel Saidi 9a1c070e3e Update geo documentation 2021-09-08 11:59:19 +02:00
Daniel Saidi 46de9ae0d4 Update files documentation 2021-09-08 11:57:30 +02:00
Daniel Saidi 8bc1f18e12 Update extension documentation 2021-09-08 11:55:41 +02:00
Daniel Saidi 7410d89f87 Update device documentation 2021-09-08 11:29:00 +02:00
Daniel Saidi a453a281e9 Update date documentation 2021-09-08 11:25:34 +02:00
Daniel Saidi 9521f80e38 Add more Calendar+Date functions 2021-09-08 11:20:41 +02:00
Daniel Saidi ce65d545d0 Update data documentation 2021-09-08 11:13:47 +02:00
Daniel Saidi 50e30153bc Update auth documentation 2021-09-08 11:13:47 +02:00
Daniel Saidi 9df955bebc Remove deprecated parts of the library 2021-09-08 11:13:47 +02:00
Daniel Saidi df38e174fb Update release notes 2021-09-08 11:13:47 +02:00
Daniel Saidi 139578e442 Update MockingKit invokation code 2021-09-08 10:37:03 +02:00
Daniel Saidi a7e4653ab0 Replace string base API routes with enum-based ones 2021-09-08 10:36:35 +02:00
Daniel Saidi a65f0bc37c Add string split by components extension 2021-09-08 10:36:08 +02:00
Daniel Saidi b69f53eb4e Add a new userSubscriptions URL 2021-09-08 10:35:05 +02:00
Daniel Saidi ed57117c86 Fix failing bundle test 2021-09-08 10:33:54 +02:00
Daniel Saidi efe908f21c Add HttpMethod tests 2021-09-08 10:26:22 +02:00
Daniel Saidi fd381d8bb8 Rename has content extension files 2021-09-08 10:25:49 +02:00
Daniel Saidi fb62848007 Update external dependencies 2021-09-08 10:23:44 +02:00
Daniel Saidi 952a48f951 Bump to 0.6.2 2021-05-04 11:26:09 +02:00
Daniel Saidi f25f2447a7 Remove urlEncodeParams for api route and always encode post params 2021-05-04 11:24:05 +02:00
Daniel Saidi 45d9ad185c Bump to 0.6.1 2021-05-04 10:45:11 +02:00
Daniel Saidi 41c3a2afc3 Remove explicit URL encoding of query params 2021-05-04 10:40:22 +02:00
Daniel Saidi 4969f38b19 Add url encode switch to api route 2021-05-02 00:57:08 +02:00
Daniel Saidi 790da03409 Make url encoding opt-in in url extension 2021-05-02 00:37:28 +02:00
Daniel Saidi 13d0f1b5d6 Fix bug where error completions weren't performed on main 2021-05-01 23:55:11 +02:00
Daniel Saidi ea289e2277 Update release notes 2021-05-01 23:14:29 +02:00
Daniel Saidi dcd67d8312 Bump to 0.6.0 2021-05-01 23:12:13 +02:00
Daniel Saidi b7202e1c11 Update release notes 2021-05-01 23:11:24 +02:00
Daniel Saidi caa93fc529 Add more stuff to api route 2021-05-01 22:36:22 +02:00
Daniel Saidi 2fbcc30ee2 Add more iCloud url extensions 2021-04-30 15:19:44 +02:00
Daniel Saidi 3d15b0264b Add calendar extensions 2021-04-29 23:41:28 +02:00
Daniel Saidi 1d22874ba7 Add iCloud-specific URL extensions 2021-04-29 23:32:57 +02:00
Daniel Saidi 5724a90ad5 Add iCloud document sync types 2021-04-29 23:22:31 +02:00
Daniel Saidi 2915042fa5 Add String+Slugified extension 2021-04-22 17:12:17 +02:00
Daniel Saidi dc5a29a2ba Tighten demo navigation bar 2021-04-06 09:47:47 +02:00
Daniel Saidi c729229885 Make demo use SwiftUIKit list items 2021-04-06 09:42:26 +02:00
Daniel Saidi e9409cde36 Bump to 0.5.0 2021-04-06 09:26:20 +02:00
Daniel Saidi f1222a771e Bump to 0.4.5 2021-04-06 09:26:05 +02:00
Daniel Saidi 32a8bf3442 Make MIMe types iterable and some inits public 2021-04-06 09:24:04 +02:00
Daniel Saidi 7fcd149811 Add file exporter 2021-03-30 00:08:00 +02:00
Daniel Saidi b02590d35f Add new MimeType and MF composer extensions that use it 2021-03-30 00:07:41 +02:00
Daniel Saidi f1c498f5eb Update ApiError with fewer cases and more info 2021-03-26 10:39:39 +01:00
Daniel Saidi 5aab278e14 Bump to 0.4.4 2021-03-24 19:13:01 +01:00
Daniel Saidi 3b5f6659b6 Add a new HttpMethod enum 2021-03-24 19:12:29 +01:00
Daniel Saidi cb435f766b Make fastlane run tests 2021-03-24 16:15:31 +01:00
Daniel Saidi 228bb96419 Bump to 0.4.3 2021-03-24 16:06:34 +01:00
Daniel Saidi 029048200c Update release notes 2021-03-24 16:05:59 +01:00
Daniel Saidi e97241cfce Add request logic to api route 2021-03-24 16:03:57 +01:00
Daniel Saidi 0462d1641e Update to MockingKit 0.9 2021-02-05 08:41:39 +01:00
Daniel Saidi 407a514dec Bump to 0.4.2 2021-01-23 07:20:57 +01:00
Daniel Saidi f04eba2a98 Add missing initializers to external map services 2021-01-23 07:19:27 +01:00
Daniel Saidi 7c12a3eb75 Replace Mockery with MockingKit 2020-12-31 12:11:08 +01:00
Daniel Saidi b3893beb06 Update readme 2020-12-30 15:25:41 +01:00
Daniel Saidi efae05445e Update readme 2020-12-30 15:24:00 +01:00
Daniel Saidi 51783fafbf Update readme 2020-12-30 15:21:37 +01:00
Daniel Saidi 81aec5eabe Update readme 2020-12-30 15:20:12 +01:00
Daniel Saidi 4e17afb0e5 Update readme 2020-12-30 15:17:20 +01:00
Daniel Saidi 4032b16bc1 Update readme 2020-12-30 15:13:35 +01:00
Daniel Saidi 0f3baef0d1 Update readme 2020-12-29 16:22:09 +01:00
Daniel Saidi 13b566ed58 Update readme 2020-12-29 16:21:10 +01:00
Daniel Saidi 6b35ac7a0a Change version badge to use semver 2020-12-29 16:20:27 +01:00
Daniel Saidi 7ee1f8f35e Update readme 2020-12-18 15:56:43 +01:00
Daniel Saidi 8a01717584 Re-add https 2020-12-18 15:39:07 +01:00
Daniel Saidi 384e971cd9 Use http in podspec twitter url to silent warnings 2020-12-18 15:38:06 +01:00
Daniel Saidi 1f00b20516 Update app icon 2020-12-18 15:37:07 +01:00
Daniel Saidi fa183f6a5f Bump to 0.4.1 2020-12-18 15:30:57 +01:00
Daniel Saidi 7603153b6d Update readme 2020-12-18 15:21:48 +01:00
Daniel Saidi 3d824ad307 Remove old demo file 2020-12-10 10:36:36 +01:00
Daniel Saidi d2ee34b984 Add rounded Mac app to demo 2020-12-08 00:28:02 +01:00
Daniel Saidi 6a08e1dd80 Update Swift version in Package file 2020-12-07 22:42:06 +01:00
Daniel Saidi 8a34cb5b01 Change demo bundle display name 2020-12-07 22:09:37 +01:00
Daniel Saidi 5f9c6b74f2 Add macOS to podfile and increase Swift version to 5.3 2020-12-07 21:59:00 +01:00
Daniel Saidi 6d140b722d Add SwiftLint to demo 2020-12-07 21:53:47 +01:00
Daniel Saidi 501c8bcd29 Update Nimble to compile for tvOS 2020-12-07 18:12:21 +01:00
Daniel Saidi 4be491079f Bump to 0.4.0 2020-12-07 18:05:21 +01:00
Daniel Saidi 62530ac8cf Add interface file 2020-12-07 18:05:12 +01:00
Daniel Saidi 22a8be3a41 Use inset group style 2020-12-07 18:02:33 +01:00
Daniel Saidi 55ebaea6de Fix icon 2020-12-07 18:00:46 +01:00
Daniel Saidi 1493632952 Remove old demo project 2020-12-07 17:51:04 +01:00
Daniel Saidi 7c7a14a718 Tweak style 2020-12-07 17:49:40 +01:00
Daniel Saidi bf404dbf39 Apply a light title font 2020-12-07 16:44:00 +01:00
Daniel Saidi 9225e5429f Add demo appearance 2020-12-07 16:24:47 +01:00
Daniel Saidi e457ebeafb Add last demo screens 2020-12-07 16:04:12 +01:00
Daniel Saidi b79cda2244 Add localization demos 2020-12-07 15:58:47 +01:00
Daniel Saidi e7d6232aaa Add keychain demo 2020-12-07 15:05:07 +01:00
Daniel Saidi f8c467e551 Add IoC demo 2020-12-07 14:34:18 +01:00
Daniel Saidi 18d40cd26b Add geo demo 2020-12-07 14:28:04 +01:00
Daniel Saidi e6893fd86c Add file demos 2020-12-07 13:51:31 +01:00
Daniel Saidi 772cdd4aa7 Add extensions screen 2020-12-07 13:10:05 +01:00
Daniel Saidi 1b5adb146c Add device demo 2020-12-07 10:58:35 +01:00
Daniel Saidi fa23c5f7ad Add date demo 2020-12-07 10:32:48 +01:00
Daniel Saidi 63d3c7537e Add auth and data demos 2020-11-30 07:50:29 +01:00
Daniel Saidi 2c684072f9 Begin creating a new demo project 2020-11-28 01:44:37 +01:00
Daniel Saidi e9dc84041d Add new extensions 2020-11-24 15:43:47 +01:00
Daniel Saidi e7f8988857 Add new trim string extention 2020-11-15 08:40:13 +01:00
Daniel Saidi a562a58ace Add string dictation extension 2020-11-14 16:42:54 +01:00
Daniel Saidi df4902c6b6 Use accept range 2020-11-03 16:19:37 +01:00
Daniel Saidi ff296a0567 Add form data request to api service 2020-11-02 23:25:32 +01:00
Daniel Saidi ac0aed1aa4 Remove all automatic url encoding from the api route an service implementations 2020-11-01 11:39:19 +01:00
Daniel Saidi 7784c7176f Move parameter encoding from api service to api route 2020-11-01 11:20:15 +01:00
Daniel Saidi 4a516761b8 Add network utils 2020-10-25 08:55:24 +01:00
Daniel Saidi 510411b9fb Update Swift version badge 2020-10-12 15:28:02 +02:00
Daniel Saidi 6df33336a3 Add map services 2020-09-26 09:50:41 +02:00
Daniel Saidi 0560a7fa49 Bump to 0.3.2 2020-09-23 22:58:59 +02:00
Daniel Saidi d3610b1e1b Bump to 0.3.1 2020-09-23 22:58:13 +02:00
Daniel Saidi 82e2191808 Add UserDefaults extension for persisting codable 2020-09-23 22:36:44 +02:00
Daniel Saidi dfb1e33ad4 Make standard cvs parser use path instead of url 2020-09-16 11:57:54 +02:00
Daniel Saidi 940c19e519 Bump to 0.3.0 2020-09-05 12:00:48 +02:00
Daniel Saidi 79d809647e Update release notes 2020-09-05 11:59:53 +02:00
Daniel Saidi 1ca3184fd8 Make code pass lint validation 2020-09-05 11:59:20 +02:00
Daniel Saidi e56f64daa3 Improve the version bump process to include test verification and linting 2020-09-05 11:30:35 +02:00
Daniel Saidi db828e1869 Disable biometric authentication for watchOS and tvOS 2020-09-05 11:02:23 +02:00
Daniel Saidi dcf76bc471 Bump to 0.2.0 2020-08-31 21:02:30 +02:00
Daniel Saidi f73e266fd0 Add macOS 10.13 availability 2020-08-31 21:01:30 +02:00
Daniel Saidi d4fd16231a Add more readmes 2020-08-31 20:59:31 +02:00
Daniel Saidi 583aca1047 Add file directory service 2020-08-31 10:57:56 +02:00
Daniel Saidi 561f919871 Add localization utilities 2020-08-07 16:46:23 +02:00
Daniel Saidi bd9fffe8a7 Bump to 0.1.0 2020-08-05 16:19:20 +02:00
Daniel Saidi f7d52552a0 Add numeric conversions 2020-08-05 15:36:58 +02:00
Daniel Saidi 44892b2655 Add date extensions 2020-08-05 15:11:27 +02:00
Daniel Saidi f9dc1d7a72 Improve async trigger 2020-08-05 14:19:15 +02:00
Daniel Saidi 8e19d14373 Update to latest version of Mockery 2020-07-20 12:37:05 +02:00
Daniel Saidi 145f48e986 Update package dependencies to latest majors 2020-06-29 10:42:44 +02:00
Daniel Saidi 7ba2279fb8 Update release notes 2020-06-29 10:41:21 +02:00
Daniel Saidi 626e30463c Bump to 0.0.6 2020-06-29 10:38:29 +02:00
Daniel Saidi 8883057b55 Update test dependencies 2020-06-29 10:38:04 +02:00
Daniel Saidi b10c14496b Fix closest bug 2020-06-17 15:03:16 +02:00
Daniel Saidi 230276c161 Add a bunch of numeric exstensions 2020-06-12 10:42:40 +02:00
Daniel Saidi 9b8f126d36 Add e-mail validator 2020-06-11 15:52:55 +02:00
Daniel Saidi d10adc321e Add a bunch of extensions and utils 2020-06-11 15:30:00 +02:00
Daniel Saidi 26bd51fd81 Bump to 0.0.5 2020-06-06 09:35:26 +02:00
Daniel Saidi cffc3b0e97 Add new types 2020-06-06 09:34:35 +02:00
Daniel Saidi 942f6b6afe Add date formats, encoder and decoder 2020-06-04 21:53:48 +02:00
Daniel Saidi 08f1d44d4f Add csv parser 2020-06-04 21:38:44 +02:00
Daniel Saidi ec9d974dbe Add extensions 2020-06-04 16:40:42 +02:00
Daniel Saidi fe4d02359d Add collection extensions 2020-06-04 16:30:40 +02:00
Daniel Saidi e8d6bdaa6f Remove always modes for keychain access 2020-06-04 15:54:39 +02:00
Daniel Saidi dc04f69547 Update keychain api:s to work on tvOS 2020-06-04 15:49:28 +02:00
Daniel Saidi 757d86dc64 Bump to 0.0.4 2020-06-04 15:38:14 +02:00
Daniel Saidi e348b9a6b7 Add documentation, tests and more demos 2020-06-04 15:36:26 +02:00
Daniel Saidi a427a047cc Cleanup keychain wrapper 2020-06-04 13:09:37 +02:00
Daniel Saidi 54e2c2b994 Add keychain tools and device identifiers 2020-06-04 11:48:27 +02:00
Daniel Saidi bd0338edea Bump to 0.0.3 2020-06-03 17:46:58 +02:00
Daniel Saidi 8251b22c42 Add String extensions 2020-06-03 17:42:11 +02:00
Daniel Saidi f4e6b835dc Remove CGFloat from numeric extension, since SwiftKit should not have any UI-related logic 2020-06-03 16:24:18 +02:00
Daniel Saidi 4331e475bc Add numeric string representations 2020-06-03 16:18:45 +02:00
Daniel Saidi cbcbdb1cc1 Add comparable limit functionality 2020-06-03 13:49:11 +02:00
Daniel Saidi 43cc8cf0bf Add comparable limit extension 2020-06-03 13:28:05 +02:00
Daniel Saidi 60a6dffc4b Make isFailure return isSuccess inverse 2020-06-03 13:21:21 +02:00
Daniel Saidi ea050dbbcc Add result util tests 2020-06-03 12:09:16 +02:00
Daniel Saidi 1f27ead0ed Omit the interval parameter 2020-06-03 11:50:08 +02:00
Daniel Saidi 62f6c46303 Add dispatch queue extensions 2020-06-03 11:19:21 +02:00
Daniel Saidi 8bfe7a3e8b
Create FUNDING.yml 2020-06-02 09:48:07 +02:00
Daniel Saidi 6da30adc0d Add geo utils 2020-06-01 08:14:39 +02:00
Daniel Saidi d6a4b0d1f1 Adjust comparison functions 2020-05-29 00:12:39 +02:00
Daniel Saidi 1e1dd6f84d Add docs 2020-05-28 00:32:52 +02:00
Daniel Saidi 224dd665a3 Add date extension 2020-05-28 00:03:58 +02:00
Daniel Saidi c05013763a Adjust demo for 0.0.2 2020-05-10 09:37:15 +02:00
Daniel Saidi af8510736d Bump to 0.0.2 2020-04-29 23:05:14 +02:00
Daniel Saidi 53b3413131 Change coder signatures 2020-04-29 23:05:00 +02:00
Daniel Saidi 06e576418c Add Coding demo 2020-04-29 22:56:19 +02:00
Daniel Saidi c6a30a73b9 Add IoC demo 2020-04-29 22:46:53 +02:00
Daniel Saidi 962a8f6a97 Add IoC tools 2020-04-29 22:19:33 +02:00
Daniel Saidi b177577bbd Add coders 2020-04-29 21:53:49 +02:00
Daniel Saidi 0698b3a8db Update demo app 2020-04-29 19:23:31 +02:00
Daniel Saidi be7cd128ad Change authentication service signatures and add a standard auth type 2020-04-29 19:20:56 +02:00
Daniel Saidi c6e8719bf6 Update readme 2020-04-29 00:16:20 +02:00
Daniel Saidi 5cbf6f02cf Update readme 2020-04-29 00:13:36 +02:00
Daniel Saidi 7da59b9d32 Update release notes and readmes 2020-04-28 23:11:54 +02:00
Daniel Saidi 6fa9cd13c8 Exclude biometric authentication on watchOS 2020-04-28 23:05:05 +02:00
Daniel Saidi 2c8f971450 Bump to 0.0.1 2020-04-28 22:58:09 +02:00
Daniel Saidi 58c108ec68 Set version zero before bumping 2020-04-28 22:58:02 +02:00
Daniel Saidi 712279ea44 Add authentication demo screen 2020-04-28 22:44:35 +02:00
Daniel Saidi 25d67a90a0 Simplify authentication services 2020-04-28 22:35:46 +02:00
Daniel Saidi a7b352a4db Make result extensions public 2020-04-28 22:28:15 +02:00
Daniel Saidi 68ddb5f535 Add authentication services 2020-04-28 22:04:57 +02:00
Daniel Saidi e6ccc32972 Add SwiftKit dependency to demo 2020-04-28 14:02:42 +02:00
Daniel Saidi 45ed4f136a Add demo app 2020-04-28 13:05:12 +02:00
Daniel Saidi fe124846e5 Improve icon and logo 2020-04-28 11:10:18 +02:00
Daniel Saidi 2473cf950c Add project files 2020-04-28 10:50:03 +02:00
186 changed files with 8965 additions and 2 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: danielsaidi

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# SPM defaults
.DS_Store
/.build
/Packages
.swiftpm/
# Documentation
Docs
documentation
downloads
videos
# Fastlane
Fastlane/report.xml
Fastlane/Preview.html
Fastlane/screenshots
Fastlane/test_output
Fastlane/README.md

12
.swiftlint.yml Normal file
View File

@ -0,0 +1,12 @@
disabled_rules:
- function_body_length
- identifier_name
- line_length
- todo
- trailing_whitespace
- type_name
- vertical_whitespace
included:
- Sources
- Tests

26
DSSwiftKit.podspec Normal file
View File

@ -0,0 +1,26 @@
# Run `pod lib lint DSSwiftKit.podspec' to ensure this is a valid spec.
Pod::Spec.new do |s|
s.name = 'DSSwiftKit'
s.version = '1.3.0'
s.swift_versions = ['5.3']
s.summary = 'SwiftKit contains extra functionality for Swift.'
s.description = <<-DESC
SwiftKit contains extra functionality for Swift, like extensions, utils etc.
DESC
s.homepage = 'https://github.com/danielsaidi/SwiftKit'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Daniel Saidi' => 'daniel.saidi@gmail.com' }
s.source = { :git => 'https://github.com/danielsaidi/SwiftKit.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/danielsaidi'
s.swift_version = '5.6'
s.ios.deployment_target = '13.0'
s.macos.deployment_target = '11.0'
s.tvos.deployment_target = '13.0'
s.watchos.deployment_target = '6.0'
s.source_files = 'Sources/**/*.swift'
end

106
Fastlane/Fastfile Normal file
View File

@ -0,0 +1,106 @@
fastlane_version "2.129.0"
default_platform :ios
platform :ios do
# Documentation ==============
desc "Build Documentation"
lane :documentation do
docc_web
end
# Lint =======================
desc "Run SwiftLint"
lane :lint do
swiftlint(strict: true)
end
# Test =======================
desc "Run unit tests"
lane :test do
sh("swift test")
end
# Version ====================
desc "Create a new version"
lane :version do |options|
ensure_git_status_clean
ensure_git_branch(branch: 'master')
lint
test
documentation
bump_type = options[:type]
version = version_bump_podspec(
path: "DSSwiftKit.podspec",
bump_type: bump_type)
git_commit(path: "*", message: "Bump to #{version}")
add_git_tag(tag: version)
push_git_tags()
push_to_git_remote()
pod_push()
end
# Docs =======================
desc "Build documentation for all platforms"
lane :docc do
sh('cd .. && rm -rf Docs')
docc_platform(destination: 'iOS', name: 'ios')
docc_platform(destination: 'OS X', name: 'osx')
docc_platform(destination: 'tvOS', name: 'tvos')
docc_platform(destination: 'watchOS', name: 'watchos')
end
desc "Build documentation for a single platform"
lane :docc_platform do |values|
sh('cd .. && mkdir -p Docs')
docc_delete_derived_data
sh('cd .. && xcodebuild docbuild \
-scheme SwiftKit \
-destination \'generic/platform=' + values[:destination] + '\'')
sh('cd .. && \
find ~/Library/Developer/Xcode/DerivedData \
-name "SwiftKit.doccarchive" \
-exec cp -R {} Docs \;')
sh('cd .. && \
mv Docs/SwiftKit.doccarchive Docs/SwiftKit_' + values[:name] + '.doccarchive')
end
desc "Delete documentation derived data (may be historic duplicates)"
lane :docc_delete_derived_data do
sh('find ~/Library/Developer/Xcode/DerivedData \
-name "SwiftKit.doccarchive" \
-exec rm -Rf {} \; || true')
end
desc "Build static documentation websites for all platforms"
lane :docc_web do
docc
docc_web_platform(name: 'ios')
docc_web_platform(name: 'osx')
docc_web_platform(name: 'tvos')
docc_web_platform(name: 'watchos')
end
desc "Build static documentation website for a single platform"
lane :docc_web_platform do |values|
sh('cd .. && $(xcrun --find docc) process-archive \
transform-for-static-hosting Docs/SwiftKit_' + values[:name] + '.doccarchive \
--output-path Docs/web_' + values[:name] + ' \
--hosting-base-path SwiftKit')
end
end

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Daniel Saidi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
Package.resolved Normal file
View File

@ -0,0 +1,50 @@
{
"pins" : [
{
"identity" : "cwlcatchexception",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : {
"revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea",
"version" : "2.1.1"
}
},
{
"identity" : "cwlpreconditiontesting",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state" : {
"revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688",
"version" : "2.1.0"
}
},
{
"identity" : "mockingkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/danielsaidi/MockingKit.git",
"state" : {
"revision" : "3e51adb1a3922cdccbe84a3088b7fa4d67ae236d",
"version" : "1.1.0"
}
},
{
"identity" : "nimble",
"kind" : "remoteSourceControl",
"location" : "https://github.com/danielsaidi/Nimble.git",
"state" : {
"branch" : "main",
"revision" : "f76b83c051fb3e6c120a33ebac200efba883065a"
}
},
{
"identity" : "quick",
"kind" : "remoteSourceControl",
"location" : "https://github.com/danielsaidi/Quick.git",
"state" : {
"branch" : "main",
"revision" : "1efe9551db0ad6a6e979f33366969750123d14d9"
}
}
],
"version" : 2
}

32
Package.swift Normal file
View File

@ -0,0 +1,32 @@
// swift-tools-version:5.6
import PackageDescription
let package = Package(
name: "SwiftKit",
platforms: [
.iOS(.v13),
.macOS(.v11),
.tvOS(.v13),
.watchOS(.v6)
],
products: [
.library(
name: "SwiftKit",
targets: ["SwiftKit"]
),
],
dependencies: [
.package(url: "https://github.com/danielsaidi/Quick.git", branch: "main"), // .upToNextMajor(from: "4.0.0")),
.package(url: "https://github.com/danielsaidi/Nimble.git", branch: "main"), // .upToNextMajor(from: "9.0.0")),
.package(url: "https://github.com/danielsaidi/MockingKit.git", .upToNextMajor(from: "1.1.0"))
],
targets: [
.target(
name: "SwiftKit",
dependencies: []),
.testTarget(
name: "SwiftKitTests",
dependencies: ["SwiftKit", "Quick", "Nimble", "MockingKit"]),
]
)

View File

@ -1,2 +1,79 @@
# SwiftKit
SwiftUIKit contains additional functionality for Swift.
<p align="center">
<img src ="Resources/Logo.png" alt="SwiftKit Logo" title="SwiftKit" width=600 />
</p>
<p align="center">
<img src="https://img.shields.io/github/v/release/danielsaidi/SwiftKit?color=%2300550&sort=semver" alt="Version" />
<img src="https://img.shields.io/badge/Swift-5.6-orange.svg" alt="Swift 5.6" />
<img src="https://img.shields.io/github/license/danielsaidi/SwiftKit" alt="MIT License" />
<a href="https://twitter.com/danielsaidi">
<img src="https://img.shields.io/twitter/url?label=Twitter&style=social&url=https%3A%2F%2Ftwitter.com%2Fdanielsaidi" alt="Twitter: @danielsaidi" title="Twitter: @danielsaidi" />
</a>
<a href="https://mastodon.social/@danielsaidi">
<img src="https://img.shields.io/mastodon/follow/000253346?label=mastodon&style=social" alt="Mastodon: @danielsaidi@mastodon.social" title="Mastodon: @danielsaidi@mastodon.social" />
</a>
</p>
## About SwiftKit
SwiftKit adds extra functionality to the Swift programming language, like extensions to already existing types as well as completely new stuff.
## Installation
SwiftKit can be installed with the Swift Package Manager:
```
https://github.com/danielsaidi/SwiftKit.git
```
If you prefer to not have external dependencies, you can also just copy the source code into your app.
## Documentation
The [online documentation][Documentation] has more information, code examples, etc., and makes it easy to overview the various parts of the library.
## Support
I manage my various open-source projects in my free time and am really thankful for any help I can get from the community.
You can sponsor this project on [GitHub Sponsors][Sponsors] or get in touch for paid support.
## Contact
Feel free to reach out if you have questions or if you want to contribute in any way:
* Website: [danielsaidi.com][Website]
* Mastodon: [@danielsaidi@mastodon.social][Mastodon]
* Twitter: [@danielsaidi][Twitter]
* E-mail: [daniel.saidi@gmail.com][Email]
## Supported Platforms
SwiftKit supports `iOS 13`, `macOS 11`, `tvOS 13` and `watchOS 6`.
## License
SwiftKit is available under the MIT license. See the [LICENSE][License] file for more info.
[Email]: mailto:daniel.saidi@gmail.com
[Website]: https://www.danielsaidi.com
[Twitter]: https://www.twitter.com/danielsaidi
[Mastodon]: https://mastodon.social/@danielsaidi
[Sponsors]: https://github.com/sponsors/danielsaidi
[Documentation]: https://danielsaidi.github.io/SwiftKit/documentation/swiftkit/
[License]: https://github.com/danielsaidi/SwiftKit/blob/master/LICENSE

306
Release Notes.md Normal file
View File

@ -0,0 +1,306 @@
# Release notes
## 1.4
This version adjusts the library for Xcode 14 and deprecates some things.
This version contains a few breaking changes, that should be easy to fix.
### ✨ New features
* `DateFormatter+Init` has a new convenience initializer.
* `CsvParser` can now parse CSV files at urls as well.
* `CsvParserError` has a new convenience initializer.
### 💡 Behavior changes
* `StandardCsvParser` now throws native errors for file parsing.
### 🗑 Deprecations
* The `Network` namespace has been deprecated and moved to `ApiKit`.
### 💥 Breaking changes
* `String+Slugified` has been removed due to conflicts with TagKit.
## 1.3
This version adjusts the library for Xcode 14 and deprecates some things.
### ✨ New features
* `Collection+Async` is now available for all OS versions that are supported by the library.
### 💡 Behavior changes
* The library no longer uses the DocC package plugin.
* `DispatchQueue+Throttle` now uses `Double.random(in:)` instead of `arc4random()`.
### 🗑 Deprecations
* The `IoC` namespace has been deprecated and will be removed in the next major version.
* The `Messaging` namespace has been deprecated and will be removed in the next major version.
* The `StoreKit` namespace has been deprecated and moved to https://github.com/danielsaidi/StoreKitPlus
* `String+Slugified` has been deprecated and moved to https://github.com/danielsaidi/TagKit
### 💥 Breaking changes
* Due to the concurrency adjustments, macOS 11 is now needed.
## 1.2
This version bumps the iOS deployment target to 13.0 and adds new auth utils.
### ✨ New features
* `LAContext+Async` adds an async policy evaluation function.
* `LocalAuthenticationService` is a new service that lets you use any local authentication policy.
### 💡 Behavior changes
* `BiometricAuthenticationService` now inherits and specializes `LocalAuthenticationService`.
## 1.1
### ✨ New features
* `FileManager+UniqueFileName` contains functionality for generating a unique file name.
* `String+Capitalize` contains functionality for capitalizing the first char in a String.
* `String+Characters` contains single-char characters like `newLine` and `tab`.
* `String+Paragraph` contains functionality for finding paragraphs in the text.
* `String+Subscript` contains functionality for accessing chars in a String.
### 💡 Behavior changes
* `String+UrlEncode` now handles + as well.
## 1.0
I think it's finally time to push the major release button.
This version drastically improves documentation and ships with a DocC documentation archive.
This version also introduces a new `StoreKit` namespace with handy utils for managing StoreKit products and purchases.
### ✨ New features
* `Bundle` has a new `displayName` extension.
* `Collection` has new `asyncCompactMap` and `asyncMap` extensions.
* `Date` has a new `components` extension for retrieving year, month, hour etc.
* `NSAttributedString` has a new `init(keyedArchiveData:)` that can initialize an attributed string from `NSKeyedArchiver` generated data.
* `NSAttributedString` has a new `init(plainText:)` that can initialize an attributed string from plain .utf8 text data.
* `NSAttributedString` has a new `init(rtfData:)` that can initialize an attributed string from RTF data.
* `NSAttributedString` has a new `getKeyedArchiveData()` function that can be used to generate RTF formatted data from an attributed string.
* `NSAttributedString` has a new `getPlainTextData()` function that can be used to generate plain .utf8 formatted text data from an attributed string.
* `NSAttributedString` has a new `getRtfData()` function that can be used to generate RTF formatted data from an attributed string.
* `String` has new `boolValue` extension.
* `StoreService` is a new protocol for managing StoreKit products and purchases.
* `StoreContext` is a new class for managing StoreKit products and purchases.
* `StandardStoreService` is a new class that implements the `StoreService` protocol.
## 0.7.0
This version requires Xcode 13 and later, since it refers to the latest api:s.
This version also cleans up the code and makes changes to conform to the latest standards.
### ✨ New features
* `Calendar+Date` has new `same` functions to provide the comparison date.
* `DispatchQueue+Throttle` has new `throttle` and `debounce` functions.
* `String+Split` has a new `split(by:)` components splitting function.
* `Url+Global` has a new `userSubscriptions` url.
### 💥 Breaking changes
* All previously deprecated features have been removed.
* `ApiService` moves the `type` param before the `httpMethod`, since `httpMethod` now has a default value.
* `ApiRoute` and `ApiService` now use enum-based HTTP methods instead of string-based ones.
* `DispatchQueue+Async` now requires that you explicitly define `seconds` when using that `asyncAfter` function.
* `URL+Global` `appStoreUrl(forAppId:)` now returns an optional url.
## 0.6.1 - 0.6.2
These versions remove explicit url encoding of `ApiRoute` query params and always url encode form data params.
## 0.6.0
### ✨ New features
* `ApiRoute` has more explicit properties for working with post data.
* `ApiRoute` has a new `shouldUrlEncodeParams` parameter.
* `iCloudDocumentSync` is a new protocol for syncing iCloud document changes.
* `String+Slugify` is a new extension that can convert a string to a slugified version.
* `StandardiCloudDocumentSync` is a new class for syncing iCloud document changes.
* `URL+iCloud` contains iCloud-specific URLs and fallback URLs.
### 💡 Behavior changes
* `ApiRoute` has more required properties.
* `URL+setQueryParameter` no longer url encodes the strings you send in.
### 💥 Breaking changes
* `ApiRoute` requires new post properties to be defined.
## 0.5.0
This version adjusts code that made the demo not being able to use the SPM package instead of the source files.
### ✨ New features
* `ApiError` is a new enum that replaces the old `ApiServiceError`.
* `FileExporter` is a new protocol for exporting data to the file system.
* `MimeType` is a new enum for simplifying working with mime types.
* `MFMailComposeViewController` has a new `addAttachmentData` that uses the new `MimeType`.
* `StandardFileExporter` is a new `FileExporter` implementation.
### 🗑 Deprecations
* `ApiServiceError` has been deprecated in favor of `ApiError`.
### 💥 Breaking changes
* `ApiError` is a lot easier than before, with many cases gone and more info in the remaining ones.
## 0.4.4
This version adds a new `HttpMethod` enum that can be used with the network components.
## 0.4.3
This version adds new `ApiRoute` request functions and adjusts the url of form data requests.
## 0.4.2
This version adds missing initializers to map services.
## 0.4.1
This version updates dependencies, adjusts project setup, tweak icons and display names etc.
## 0.4.0
This version adds a new `String+Dictation` extension to cleanup dictation objects and spaces from a string.
## 0.3.3
This version adds a new `ExternalMapService` protocol as well as an Apple and a Google implementation.
This version also adds a new `Network` namespace, with api-specific protocols and errors for communicating with external REST apis.
## 0.3.2
This version adds a `UserDefaults+Codable` extension for persisting codable types in `UserDefaults`.
## 0.3.1
This version makes the standard cvs parser use paths instead of urls when parsing files.
## 0.3.0
This version adds improved support for watchOS and tvOS.
The bump version process has been improved to also add linting and a unit test confirmation.
## 0.2.0
This version adds:
* new `Localization` utilities, like `Translator`s and `LocalizationService`s.
* new `FileDirectoryService` utilities.
This version also adds macOS support.
## 0.1.0
This version adds:
* a new `Filter` type that simplifies filtering object collections.
* new `Date` [extensions][Extensions].
* new `Numeric` conversion [extensions][Extensions].
## 0.0.6
This version adds a bunch of [extensions][Extensions] and common utils and updates external test dependencies to the latest versions.
## 0.0.5
This version adds a bunch of [extensions][Extensions] and common types.
## 0.0.4
This version adds [device][Device] and [keychain][Keychain] utils.
## 0.0.3
This version adds a bunch of convenient [extensions][Extensions].
## 0.0.2
This version adjusts [authentication][Authentication] service signatures, as well as [coding][Coding] and [IoC][IoC] functionality
## 0.0.1
This version adds [authentication][Authentication] functionality to `SwiftKit`.
[Authentication]: Readmes/Authentication.md
[Coding]: Readmes/Coding.md
[Device]: Readmes/Device.md
[Extensions]: Readmes/Extensions.md
[IoC]: Readmes/IoC.md
[Keychain]: Readmes/Keychain.md

BIN
Resources/Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

BIN
Resources/Logo.sketch Normal file

Binary file not shown.

BIN
Resources/Logo_solid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -0,0 +1,42 @@
//
// Authentication.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-04-28.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This struct represents a unique authentication type.
The struct only has an ``id``, but is still used to improve
authentication without having to change any protocols.
*/
public struct Authentication: Identifiable, Equatable {
/**
Create a new authentication type.
- Parameters:
- id: The ID of the authentication.
*/
public init(id: String) {
self.id = id
}
/// The ID of the authentication.
public var id: String
}
public extension Authentication {
/**
This standard authentication type can be used if you do
not have many different authentications in your app.
*/
static var standard: Authentication {
Authentication(id: "com.swiftkit.auth.any")
}
}

View File

@ -0,0 +1,42 @@
//
// AuthenticationService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-01-18.
// Copyright © 2016 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by any classes that can be
used to authenticate the user.
*/
public protocol AuthenticationService: AnyObject {
typealias AuthCompletion = (_ result: AuthResult) -> Void
typealias AuthError = AuthenticationServiceError
typealias AuthResult = Result<Void, Error>
/**
Authenticate the user for a certain ``Authentication``.
- Parameters:
- auth: The authentication type to evaluate.
- reason: The localized reason to show to the user.
- completion: The completion block to call once authentication is done.
*/
func authenticateUser(
for auth: Authentication,
reason: String,
completion: @escaping AuthCompletion)
/**
Whether or not the service can authenticate the current
user for a certain ``Authentication`` type.
- Parameters:
- auth: The authentication type to evaluate.
*/
func canAuthenticateUser(for auth: Authentication) -> Bool
}

View File

@ -0,0 +1,31 @@
//
// AuthenticationServiceError.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-04-28.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This enum represents various authentication errors that can
occur while a user is being authenticated.
*/
public enum AuthenticationServiceError: Error, Equatable {
/**
The authentication failed.
*/
case authenticationFailed
/**
The authentication failed with a certain error message.
*/
case authenticationFailedWithErrorMessage(String)
/**
The requested authentication type is not supported.
*/
case unsupportedAuthentication
}

View File

@ -0,0 +1,25 @@
//
// BiometricAuthenticationService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-01-18.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
#if os(iOS) || os(macOS)
import LocalAuthentication
/**
This authentication service uses `LocalAuthentication` such
as `FaceID` or `TouchID` to authenticate the user.
*/
public class BiometricAuthenticationService: LocalAuthenticationService {
/**
Create a service instance.
*/
public init() {
super.init(policy: .deviceOwnerAuthenticationWithBiometrics)
}
}
#endif

View File

@ -0,0 +1,45 @@
//
// CachedAuthenticationService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-01-18.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by any classes that can be
used to authenticate the user and cache the result to avoid
having to perform a real authentication if a successful one
has already been performed.
For instance, you can reduce the number of times users have
to perform biometric authentication to access critical data.
Note that you can't rely on a cached authentication service
to clear its state. Call the ``resetUserAuthentications()``
or ``resetUserAuthentication(for:)`` function as soon as an
authenticated session becomes invalid, e.g. when the app is
sent to the background or new users log in.
*/
public protocol CachedAuthenticationService: AuthenticationService {
/**
Check if the service has already authenticated the user
for a certain authentication type.
*/
func isUserAuthenticated(for auth: Authentication) -> Bool
/**
Reset the service's cached authentication state for the
provided authentication type.
*/
func resetUserAuthentication(for auth: Authentication)
/**
Reset the service's cached authentication state for all
authentication types.
*/
func resetUserAuthentications()
}

View File

@ -0,0 +1,94 @@
//
// CachedAuthenticationService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-01-18.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This class wraps another ``AuthenticationService`` instance
and keeps authentication results in a cache.
*/
public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
// MARK: - Initialization
public init(baseService: AuthenticationService) {
self.baseService = baseService
}
// MARK: - Properties
private let baseService: AuthenticationService
private var cache = [String: Bool]()
// MARK: - Functions
/**
Authenticate the user for a certain authentication type.
`reason` can be used to display information to the user.
*/
public func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
if isUserAuthenticated(for: auth) { return completion(.success(())) }
baseService.authenticateUser(for: auth, reason: reason) { result in
self.handle(result, for: auth)
completion(result)
}
}
/**
Check if the service instance can authenticate the user.
*/
public func canAuthenticateUser(for auth: Authentication) -> Bool {
baseService.canAuthenticateUser(for: auth)
}
/**
Check if the service has already authenticated the user
for a certain authentication type.
*/
public func isUserAuthenticated(for auth: Authentication) -> Bool {
cache[auth.id] ?? false
}
/**
Reset the service's cached authentication state for the
provided authentication type.
*/
public func resetUserAuthentication(for auth: Authentication) {
setIsAuthenticated(false, for: auth)
}
/**
Reset the service's cached authentication state for all
authentication types.
*/
public func resetUserAuthentications() {
cache.removeAll()
}
}
// MARK: - Private Functions
private extension CachedAuthenticationServiceProxy {
func handle(_ result: AuthResult, for auth: Authentication) {
switch result {
case .failure: setIsAuthenticated(false, for: auth)
case .success: setIsAuthenticated(true, for: auth)
}
}
func setIsAuthenticated(_ isAuthenticated: Bool, for auth: Authentication) {
cache[auth.id] = isAuthenticated
}
}

View File

@ -0,0 +1,31 @@
//
// LAContext+Async.swift
// SwiftKit
//
// Created by Daniel Saidi on 2022-04-29.
// Copyright © 2016-2022 Daniel Saidi. All rights reserved.
//
#if os(iOS) || os(macOS)
import LocalAuthentication
@available(iOS 15.0, macOS 12.0, *)
extension LAContext {
/**
Evaluate a certain policy.
- Parameters:
- policy: The policy to evaluate.
- localizedReason: The localized reason to show to the user.
*/
func evaluatePolicy(_ policy: LAPolicy, localizedReason reason: String) async throws -> Bool {
try await withCheckedThrowingContinuation { cont in
LAContext().evaluatePolicy(policy, localizedReason: reason) { result, error in
if let error = error { return cont.resume(throwing: error) }
cont.resume(returning: result)
}
}
}
}
#endif

View File

@ -0,0 +1,78 @@
//
// LocalAuthenticationService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2022-04-29.
// Copyright © 2022 Daniel Saidi. All rights reserved.
//
#if os(iOS) || os(macOS)
import LocalAuthentication
/**
This service uses `LocalAuthentication` to authenticate the
current user.
*/
open class LocalAuthenticationService: AuthenticationService {
/**
Create a service instance.
- Parameters:
- policy: The authentication policy to use.
*/
public init(policy: LAPolicy) {
self.policy = policy
}
private let policy: LAPolicy
/**
Authenticate the user for a certain ``Authentication``.
- Parameters:
- auth: The authentication type to evaluate.
- reason: The localized reason to show to the user.
*/
open func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
guard canAuthenticateUser(for: auth) else { return completion(.failure(AuthError.unsupportedAuthentication)) }
performAuthentication(for: auth, reason: reason) { result in
DispatchQueue.main.async { completion(result) }
}
}
/**
Check if the service instance can authenticate the user
for a certain ``Authentication``.
For biometric authentication, a user can disable system
authentication for an app, which means that the service
can no longer fulfill it's intended use.
- Parameters:
- auth: The authentication type to evaluate.
*/
open func canAuthenticateUser(for auth: Authentication) -> Bool {
var error: NSError?
return LAContext().canEvaluatePolicy(policy, error: &error)
}
/**
Authenticate the user for a certain `` authentication type,
regardless of if this service can authenticate the user
for the provided authentication type or not.
This is a way to bypass any particular rules and states
of the service and can be used to e.g. mock .
*/
open func performAuthentication(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
LAContext().evaluatePolicy(policy, localizedReason: reason) { result, error in
if let error = error { return completion(.failure(error)) }
if result == false { return completion(.failure(AuthError.authenticationFailed)) }
completion(.success(()))
}
}
}
#endif

View File

@ -0,0 +1,40 @@
//
// Bundle+BundleInformation.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This extensions make `Bundle` implement ``BundleInformation``.
*/
extension Bundle: BundleInformation {
/**
Get the bundle build number, e.g. `42567`.
*/
public var buildNumber: String {
let key = String(kCFBundleVersionKey)
let version = infoDictionary?[key] as? String
return version ?? ""
}
/**
Get the bundle display name, if any.
*/
public var displayName: String {
infoDictionary?["CFBundleDisplayName"] as? String ?? "-"
}
/**
Get the bundle build number, e.g. `42567`.
*/
public var versionNumber: String {
let key = "CFBundleShortVersionString"
let version = infoDictionary?[key] as? String
return version ?? "0.0.0"
}
}

View File

@ -0,0 +1,30 @@
//
// BundleInformation.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by types that can provide information about the current bundle.
*/
public protocol BundleInformation {
/**
Get the bundle build number, e.g. `42567`.
*/
var buildNumber: String { get }
/**
Get the bundle display name, if any.
*/
var displayName: String { get }
/**
Get the bundle build number, e.g. `42567`.
*/
var versionNumber: String { get }
}

View File

@ -0,0 +1,52 @@
//
// Collection+Async.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-10.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Collection {
/**
Compact map a collection using an async transform.
*/
func asyncCompactMap<ResultType>(_ transform: (Element) async -> ResultType?) async -> [ResultType] {
await self
.asyncMap(transform)
.compactMap { $0 }
}
/**
Compact map a collection using an async transform.
*/
func asyncCompactMap<ResultType>(_ transform: (Element) async throws -> ResultType?) async throws -> [ResultType] {
try await self
.asyncMap(transform)
.compactMap { $0 }
}
/**
Map a collection using an async transform.
*/
func asyncMap<ResultType>(_ transform: (Element) async -> ResultType) async -> [ResultType] {
var result = [ResultType]()
for item in self {
await result.append(transform(item))
}
return result
}
/**
Map a collection using an async transform.
*/
func asyncMap<ResultType>(_ transform: (Element) async throws -> ResultType) async throws -> [ResultType] {
var result = [ResultType]()
for item in self {
try await result.append(transform(item))
}
return result
}
}

View File

@ -0,0 +1,59 @@
//
// CsvParser.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can handle
parsing of comma-separated value files and strings.
When parsing a csv file or string, every line will be split
up into components using the provided `componentSeparator`.
*/
public protocol CsvParser {
/**
Parse a csv file in a certain bundle.
- Parameters:
- fileName: The name of the file to parse.
- fileExtension: The extension of the file to parse.
- bundle: The bundle in which the file is located.
- componentSeparator: The separator that separates components on each line.
*/
func parseCsvFile(
named fileName: String,
withExtension fileExtension: String,
in bundle: Bundle,
componentSeparator: Character
) throws -> [[String]]
/**
Parse a csv file at a certain url.
- Parameters:
- url: The url of the file to parse.
- componentSeparator: The separator that separates components on each line.
*/
func parseCsvFile(
at url: URL,
componentSeparator: Character
) throws -> [[String]]
/**
Parse the provided csv string.
- Parameters:
- string: The string to parse.
- componentSeparator: The separator that separates components on each line.
*/
func parseCsvString(
_ string: String,
componentSeparator: Character
) -> [[String]]
}

View File

@ -0,0 +1,18 @@
//
// CsvParserError.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2018 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This error can be thrown while parsing a csv string or file.
*/
public enum CsvParserError: Error {
/// The requested file doesn't exist.
case noFileWithName(_ fileName: String, andExtension: String, inBundle: Bundle)
}

View File

@ -0,0 +1,86 @@
//
// StandardCsvParser.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This class can be used to parse comma-separated value files
and strings.
When parsing a csv file or string, every line will be split
up into components using the provided `componentSeparator`.
*/
public class StandardCsvParser: CsvParser {
/**
Create a parser instance.
*/
public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}
private let fileManager: FileManager
/**
Parse a csv file in a certain bundle.
- Parameters:
- fileName: The name of the file to parse.
- fileExtension: The extension of the file to parse.
- bundle: The bundle in which the file is located.
- componentSeparator: The separator that separates components on each line.
*/
public func parseCsvFile(
named fileName: String,
withExtension ext: String,
in bundle: Bundle,
componentSeparator: Character
) throws -> [[String]] {
guard let path = bundle.path(forResource: fileName, ofType: ext) else {
throw CsvParserError.noFileWithName(fileName, andExtension: ext, inBundle: bundle)
}
let string = try String(contentsOfFile: path, encoding: .utf8)
return parseCsvString(string, componentSeparator: componentSeparator)
}
/**
Parse a csv file at a certain url.
- Parameters:
- url: The url of the file to parse.
- componentSeparator: The separator that separates components on each line.
*/
public func parseCsvFile(
at url: URL,
componentSeparator: Character
) throws -> [[String]] {
let string = try String(contentsOf: url, encoding: .utf8)
return parseCsvString(string, componentSeparator: componentSeparator)
}
/**
Parse the provided csv string.
- Parameters:
- string: The string to parse.
- componentSeparator: The separator that separates components on each line.
*/
public func parseCsvString(
_ string: String,
componentSeparator: Character
) -> [[String]] {
string
.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
.map { $0.split(separator: componentSeparator)
.map { String($0).trimmingCharacters(in: .whitespaces) }
}
}
}

View File

@ -0,0 +1,35 @@
//
// Base64StringEncoder.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-03-21.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This coder can encode and decode strings to and from base64.
*/
public class Base64StringCoder: StringCoder {
public init() {}
/**
Decode a base64 encoded string.
*/
public func decode(_ string: String) -> String? {
guard let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters) else { return nil }
return String(data: data, encoding: .utf8)
}
/**
Encode a string to base64.
*/
public func encode(_ string: String) -> String? {
let data = string.data(using: .utf8)
let encoded = data?.base64EncodedData(options: .endLineWithLineFeed)
guard let encodedData = encoded else { return nil }
return String(data: encodedData, encoding: .utf8)
}
}

View File

@ -0,0 +1,67 @@
//
// Filter.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-08-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This struct lets you specify available and selected options
of a certain type.
*/
public struct Filter<T: FilterOption>: Equatable {
public init(available: [T], selected: [T]) {
self.available = available
self.selected = selected
}
public let available: [T]
public var selected: [T]
}
public extension Filter {
/**
Deselect a certain option.
*/
mutating func deselect(_ option: T) {
selected = selected.filter { $0 != option }
}
/**
Select a certain option.
*/
mutating func select(_ option: T) {
selected = Array(Set(selected + [option]))
}
/**
Whether or not the filter is identical to another value.
*/
func isIdentical(to filter: Filter<T>) -> Bool {
let isAvailableIdentical = available.sorted() == filter.available.sorted()
let isSelectedIdentical = selected.sorted() == filter.selected.sorted()
return isAvailableIdentical && isSelectedIdentical
}
}
/**
This protocol can be implemented by anything that can be used
*/
public protocol FilterOption: Hashable {
associatedtype SortValue: Comparable
var sortValue: SortValue { get }
}
public extension Sequence where Iterator.Element: FilterOption {
func sorted() -> [Element] {
sorted { $0.sortValue < $1.sortValue }
}
}

View File

@ -0,0 +1,161 @@
//
// MimeType.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-03-26.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
import UniformTypeIdentifiers
/**
This enum represents a set of different MIME and file types.
Note that some types may be expected to be a different type,
but are instead an `.application` type. For instance, `json`
is a text format, but the mime type is `application/json`.
*/
public enum MimeType: Identifiable {
case
application(Application),
audio(Audio),
image(Image),
text(Text),
video(Video)
public var identifier: String { id }
public var id: String {
switch self {
case .audio(let type): return "audio/\(type.id)"
case .application(let type): return "application/\(type.id)"
case .image(let type): return "image/\(type.id)"
case .text(let type): return "text/\(type.id)"
case .video(let type): return "video/\(type.id)"
}
}
public enum Application: CaseIterable, Identifiable {
case
ai, atom, bin, crt, cco, deb, der, dll, dmg, doc,
docx, ear, eot, eps, exe, hqx, img, iso, jar,
jardiff, jnlp, js, json, kml, kmz, m3u8, msi, msm,
msp, pdb, pdf, pem, pl, pm, ppt, pptx, prc, ps,
rar, rpm, rss, rtf, run, sea, sit, swf, war, tcl,
wmlc, woff, x7z, xhtml, xls, xlsx, xpi, xspf, zip
public var id: String {
switch self {
case .jar, .war, .ear: return "java-archive"
case .bin, .exe, .dll, .deb, .dmg, .iso, .img, .msi, .msp, .msm: return "octet-stream"
case .pl, .pm: return "x-perl"
case .pdb, .prc: return "x-pilot"
case .crt, .der, .pem: return "x-x509-ca-cert"
case .ai: return "postscript"
case .atom: return "atom+xml"
case .cco: return "x-cocoa"
case .doc: return "msword"
case .docx: return "vnd.openxmlformats-officedocument.wordprocessingml.document"
case .eot: return "vnd.ms-fontobject"
case .eps: return "postscript"
case .hqx: return "mac-binhex40"
case .jardiff: return "x-java-archive-diff"
case .jnlp: return "x-java-jnlp-file"
case .js: return "javascript"
case .json: return "json"
case .kml: return "vnd.google-earth.kml+xml"
case .kmz: return "vnd.google-earth.kmz"
case .m3u8: return "vnd.apple.mpegurl"
case .pdf: return "pdf"
case .ppt: return "vnd.ms-powerpoint"
case .pptx: return "vnd.openxmlformats-officedocument.presentationml.presentation"
case .ps: return "postscript"
case .rar: return "x-rar-compressed"
case .rpm: return "x-redhat-package-manager"
case .rss: return "rss+xml"
case .rtf: return "rtf"
case .run: return "x-makeself"
case .sea: return "x-sea"
case .sit: return "x-stuffit"
case .swf: return "x-shockwave-flash"
case .tcl: return "x-tcl"
case .woff: return "font-woff"
case .wmlc: return "vnd.wap.wmlc"
case .x7z: return "x-7z-compressed"
case .xhtml: return "xhtml+xml"
case .xls: return "vnd.ms-excel"
case .xlsx: return "vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case .xpi: return "x-xpinstall"
case .xspf: return "xspf+xml"
case .zip: return "zip"
}
}
}
public enum Audio: String, CaseIterable, Identifiable {
case kar, m4a, midi, mp3, ogg, ra
public var id: String {
switch self {
case .midi, .ogg: return rawValue
case .kar: return "midi"
case .m4a: return "x-m4a"
case .mp3: return "mpeg"
case .ra: return "x-realaudio"
}
}
}
public enum Image: String, CaseIterable, Identifiable {
case bmp, gif, ico, jpeg, jng, png, svg, tiff, wbmp, webp
public var id: String {
switch self {
case .gif, .jpeg, .png, .tiff, .webp: return rawValue
case .bmp: return "x-ms-bmp"
case .ico: return "x-icon"
case .jng: return "x-jng"
case .svg: return "svg+xml"
case .wbmp: return "vnd.wap.wbmp"
}
}
}
public enum Text: String, CaseIterable, Identifiable {
case plain, css, htc, html, jad, mathml, xml, wml
public var id: String {
switch self {
case .plain, .css, .html, .mathml, .xml: return rawValue
case .jad: return "vnd.sun.j2me.app-descriptor"
case .wml: return "vnd.wap.wml"
case .htc: return "x-component"
}
}
}
public enum Video: String, CaseIterable, Identifiable {
case asf, asx, avi, flv, m4v, mng, mp4, mpeg, mov, ts, video3gpp, webm, wmv
public var id: String {
switch self {
case .mp4, .mpeg: return rawValue
case .asf: return "x-ms-asf"
case .asx: return "x-ms-asf"
case .avi: return "x-msvideo"
case .flv: return "x-flv"
case .m4v: return "x-m4v"
case .mng: return "x-mng"
case .mov: return "quicktime"
case .ts: return "mp2t"
case .video3gpp: return "3gpp"
case .webm: return "webm"
case .wmv: return "x-ms-wmv"
}
}
}
}

View File

@ -0,0 +1,46 @@
//
// Persisted.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-04-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This property wrapper automatically persists any new values
to user defaults and sets the initial property value to the
last persisted value or a fallback value.
This type is internal, since the `SwiftUI` tyope is used in
more ways. This type only serves the library functionality.
*/
@propertyWrapper
struct Persisted<T: Codable> {
init(
key: String,
store: UserDefaults = .standard,
defaultValue: T) {
self.key = key
self.store = store
self.defaultValue = defaultValue
}
private let key: String
private let store: UserDefaults
private let defaultValue: T
var wrappedValue: T {
get {
guard let data = store.object(forKey: key) as? Data else { return defaultValue }
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
store.set(data, forKey: key)
}
}
}

View File

@ -0,0 +1,39 @@
//
// StringDecoder.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-03-21.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can encode
and decode strings.
*/
public protocol StringCoder: StringEncoder, StringDecoder {}
/**
This protocol can be implemented by classes that can decode
strings.
*/
public protocol StringDecoder: AnyObject {
/**
Decode a string to another string.
*/
func decode(_ string: String) -> String?
}
/**
This protocol can be implemented by classes that can encode
strings.
*/
public protocol StringEncoder: AnyObject {
/**
Encode a string to something else.
*/
func encode(_ string: String) -> String?
}

View File

@ -0,0 +1,76 @@
//
// Calendar+Date.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-04-29.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Calendar {
/**
Whether or not this calendar thinks that a certain date
is the same day as another date.
*/
func isDate(_ date1: Date, sameDayAs date2: Date) -> Bool {
isDate(date1, equalTo: date2, toGranularity: .day)
}
/**
Whether or not this calendar thinks that a certain date
is the same month as another date.
*/
func isDate(_ date1: Date, sameMonthAs date2: Date) -> Bool {
isDate(date1, equalTo: date2, toGranularity: .month)
}
/**
Whether or not this calendar thinks that a certain date
is the same week as another date.
*/
func isDate(_ date1: Date, sameWeekAs date2: Date) -> Bool {
isDate(date1, equalTo: date2, toGranularity: .weekOfYear)
}
/**
Whether or not this calendar thinks that a certain date
is the same year as another date.
*/
func isDate(_ date1: Date, sameYearAs date2: Date) -> Bool {
isDate(date1, equalTo: date2, toGranularity: .year)
}
/**
Whether or not this calendar thinks that a certain date
is this month.
*/
func isDateThisMonth(_ date: Date) -> Bool {
isDate(date, sameMonthAs: Date())
}
/**
Whether or not this calendar thinks that a certain date
is this week.
*/
func isDateThisWeek(_ date: Date) -> Bool {
isDate(date, sameWeekAs: Date())
}
/**
Whether or not this calendar thinks that a certain date
is this year.
*/
func isDateThisYear(_ date: Date) -> Bool {
isDate(date, sameYearAs: Date())
}
/**
Whether or not this calendar thinks that a certain date
is today.
*/
func isDateToday(_ date: Date) -> Bool {
isDate(date, sameDayAs: Date())
}
}

View File

@ -0,0 +1,71 @@
//
// Date+Adding.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-05-15.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Date {
/**
Add a certain number days days to the date.
*/
func adding(days: Double) -> Date {
let seconds = Double(days) * 60 * 60 * 24
return addingTimeInterval(seconds)
}
/**
Add a certain number hours days to the date.
*/
func adding(hours: Double) -> Date {
let seconds = Double(hours) * 60 * 60
return addingTimeInterval(seconds)
}
/**
Add a certain number minutes days to the date.
*/
func adding(minutes: Double) -> Date {
let seconds = Double(minutes) * 60
return addingTimeInterval(seconds)
}
/**
Add a certain number seconds days to the date.
*/
func adding(seconds: Double) -> Date {
addingTimeInterval(Double(seconds))
}
/**
Remove a certain number of days to the date.
*/
func removing(days: Double) -> Date {
adding(days: -days)
}
/**
Remove a certain number of hours to the date.
*/
func removing(hours: Double) -> Date {
adding(hours: -hours)
}
/**
Remove a certain number of minutes to the date.
*/
func removing(minutes: Double) -> Date {
adding(minutes: -minutes)
}
/**
Remove a certain number of seconds to the date.
*/
func removing(seconds: Double) -> Date {
adding(seconds: -seconds)
}
}

View File

@ -0,0 +1,37 @@
//
// Date+Compare.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-05-15.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
These extensions provide a semantic, more readable layer on
top of the raw comparisons.
*/
public extension Date {
/**
Whether or not the date occurs after the provided date.
*/
func isAfter(_ date: Date) -> Bool {
self > date
}
/**
Whether or not the date occurs before the provided date.
*/
func isBefore(_ date: Date) -> Bool {
self < date
}
/**
Whether or not the date is the same as the provided date.
*/
func isSame(as date: Date) -> Bool {
self == date
}
}

View File

@ -0,0 +1,86 @@
//
// Date+Components.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-03.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Date {
/**
Get the current day for the current calendar.
*/
var day: Int? { day() }
/**
Get the current hour for the current calendar.
*/
var hour: Int? { hour() }
/**
Get the current minute for the current calendar.
*/
var minute: Int? { minute() }
/**
Get the current month for the current calendar.
*/
var month: Int? { month() }
/**
Get the current second for the current calendar.
*/
var second: Int? { second() }
/**
Get the current year for the current calendar.
*/
var year: Int? { year() }
/**
Get the current day for the provided calendar.
*/
func day(for calendar: Calendar = .current) -> Int? {
calendar.dateComponents([.day], from: self).day
}
/**
Get the current hour for the provided calendar.
*/
func hour(for calendar: Calendar = .current) -> Int? {
calendar.dateComponents([.hour], from: self).hour
}
/**
Get the current minute for the provided calendar.
*/
func minute(for calendar: Calendar = .current) -> Int? {
calendar.dateComponents([.minute], from: self).minute
}
/**
Get the current month for the provided calendar.
*/
func month(for calendar: Calendar = .current) -> Int? {
calendar.dateComponents([.month], from: self).month
}
/**
Get the current second for the provided calendar.
*/
func second(for calendar: Calendar = .current) -> Int? {
calendar.dateComponents([.second], from: self).second
}
/**
Get the current year for the provided calendar.
*/
func year(for calendar: Calendar = .current) -> Int? {
calendar.dateComponents([.year], from: self).year
}
}

View File

@ -0,0 +1,61 @@
//
// Date+Difference.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-08-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Date {
/**
The number of years between this date and another one.
*/
func years(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.year], from: date, to: self).year ?? 0
}
/**
The number of months between this date and another one.
*/
func months(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.month], from: date, to: self).month ?? 0
}
/**
The number of weeks between this date and another one.
*/
func weeks(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.weekOfYear], from: date, to: self).weekOfYear ?? 0
}
/**
The number of days between this date and another one.
*/
func days(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.day], from: date, to: self).day ?? 0
}
/**
The number of hours between this date and another one.
*/
func hours(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.hour], from: date, to: self).hour ?? 0
}
/**
The number of minutes between this date and another one.
*/
func minutes(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.minute], from: date, to: self).minute ?? 0
}
/**
The number of seconds between this date and another one.
*/
func seconds(from date: Date, calendar: Calendar = .current) -> Int {
calendar.dateComponents([.second], from: date, to: self).second ?? 0
}
}

View File

@ -0,0 +1,32 @@
//
// Date+Init.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-08-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Date {
/**
Create a date value using the provided components. Year,
month and day are required, while the others are not.
*/
init?(
year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
calendar: Calendar = .current) {
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second)
guard let date = calendar.date(from: components) else {
assertionFailure("Invalid date")
return nil
}
self = date
}
}

View File

@ -0,0 +1,39 @@
//
// DateDecoders.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-09-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension JSONDecoder {
/**
Creates a `JSONDecoder` that can decode ISO8601 encoded
strings.
*/
static var iso8601: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .robustISO8601
return decoder
}
}
private extension JSONDecoder.DateDecodingStrategy {
/**
This strategy can be used to parse ISO8601 dates. It is
more robust than the standard strategy, and will try to
parse both milliseconds and seconds.
*/
static let robustISO8601 = custom { decoder throws -> Date in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
let msFormatter = DateFormatter.iso8601Milliseconds
let secFormatter = DateFormatter.iso8601Seconds
if let date = msFormatter.date(from: string) ?? secFormatter.date(from: string) { return date }
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}

View File

@ -0,0 +1,32 @@
//
// DateEncoders.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-09-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension JSONEncoder {
/**
Creates a `JSONEncoder` that can encode ISO8601 encoded
strings.
*/
static var iso8601: JSONEncoder {
let decoder = JSONEncoder()
decoder.dateEncodingStrategy = .customISO8601
return decoder
}
}
private extension JSONEncoder.DateEncodingStrategy {
static let customISO8601 = custom { (date, encoder) throws -> Void in
let formatter = DateFormatter.iso8601Milliseconds
let string = formatter.string(from: date)
var container = encoder.singleValueContainer()
try container.encode(string)
}
}

View File

@ -0,0 +1,71 @@
//
// DateFormatter+Init.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-09-05.
// Copyright © 2018 Daniel Saidi. All rights reserved.
//
import Foundation
public extension DateFormatter {
/**
Create a custom date formatter, that uses a custom date
format, calendar, locale and time zone.
- Parameters:
- dateStyle: The date style to use.
- timeStyle: The time style to use, by default `.none`.
- locale: The locale to use, by default `en_US_POSIX`.
- calendar: The calendar to use, by default `iso8601`.
*/
convenience init(
dateStyle: DateFormatter.Style,
timeStyle: DateFormatter.Style = .none,
locale: Locale = Locale(identifier: "en_US_POSIX"),
calendar: Calendar = Calendar(identifier: .iso8601)
) {
self.init()
self.dateStyle = dateStyle
self.timeStyle = timeStyle
self.locale = locale
self.calendar = calendar
}
/**
Create a custom date formatter, that uses a custom date
format, calendar, locale and time zone.
- Parameters:
- dateFormat: The date string format to use.
- calendar: The calendar to use, by default `iso8601`.
- locale: The locale to use, by default `en_US_POSIX`.
- timeZone: The time zone to use, by default `GMT`.
*/
convenience init(
dateFormat: String,
calendar: Calendar = Calendar(identifier: .iso8601),
locale: Locale = Locale(identifier: "en_US_POSIX"),
timeZone: TimeZone? = TimeZone(secondsFromGMT: 0)) {
self.init()
self.calendar = calendar
self.locale = locale
self.dateFormat = dateFormat
self.timeZone = timeZone
}
/**
Create a date formatter using the ISO8601 second format.
*/
static var iso8601Seconds: DateFormatter {
DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ssZ")
}
/**
Create a date formatter using the ISO8601 ms format.
*/
static var iso8601Milliseconds: DateFormatter {
DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
}
}

View File

@ -0,0 +1,26 @@
//
// DeviceIdentifier.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by anything that can get a
unique device identifier for the current device.
*/
public protocol DeviceIdentifier: AnyObject {
/**
Get a unique device identifier.
*/
func getDeviceIdentifier() -> String
}
extension DeviceIdentifier {
var key: String { "com.swiftkit.deviceidentifier" }
}

View File

@ -0,0 +1,45 @@
//
// KeychainBasedDeviceIdentifier.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This device identifier generates a unique device identifier
and stores it in keychain, to make it possible to reuse the
identifier, even if the app is uninstalled.
The user default fallback maximizes the chance that the app
can retrieve the identifier even if the keychain can not be
read at the time of retrieval.
*/
public class KeychainBasedDeviceIdentifier: DeviceIdentifier {
public init(
keychainService: KeychainService,
backupIdentifier: DeviceIdentifier = UserDefaultsBasedDeviceIdentifier()) {
self.keychainService = keychainService
self.backupIdentifier = backupIdentifier
}
private let backupIdentifier: DeviceIdentifier
private let keychainService: KeychainService
/**
Get a unique device identifier from the device keychain.
If no identifier exists in the keychain, the identifier
will use the provided `backupIdentifier` to generate an
identifier, then persist that id in the device keychain.
*/
public func getDeviceIdentifier() -> String {
if let id = keychainService.string(for: key, with: nil) { return id }
let id = backupIdentifier.getDeviceIdentifier()
keychainService.set(id, for: key, with: nil)
return id
}
}

View File

@ -0,0 +1,51 @@
//
// UserDefaultsBasedDeviceIdentifier.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This device identifier generates a unique device identifier
and stores it in user defaults, so that the same identifier
is used every time for each app installation.
If you want to use the same identifier between app installs,
The user default fallback maximizes the chance that the app
can retrieve the identifier even if the keychain can not be
read at the time of retrieval.
*/
public class UserDefaultsBasedDeviceIdentifier: DeviceIdentifier {
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}
private let defaults: UserDefaults
/**
Get a unique device identifier from the user defaults.
If no persisted identifier exists, this identifier will
generate a new identifier, then persist and return that
identifier.
*/
public func getDeviceIdentifier() -> String {
if let id = defaults.string(forKey: key) { return id }
return generateDeviceIdentifier()
}
}
private extension UserDefaultsBasedDeviceIdentifier {
func generateDeviceIdentifier() -> String {
let id = UUID().uuidString
defaults.set(id, forKey: key)
return id
}
}

View File

@ -0,0 +1,32 @@
//
// Array+RemoveObject.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-06-12.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Array where Element: Comparable & Strideable {
/**
Create an array using a set of values from the provided
`range`, stepping `stepSize` between each value.
*/
init(_ range: ClosedRange<Element>, stepSize: Element.Stride) {
self = Array(stride(from: range.lowerBound, through: range.upperBound, by: stepSize))
}
}
public extension Array where Element == Double {
/**
Create an array using a set of values from the provided
`range`, stepping `stepSize` between each value.
*/
init(_ range: ClosedRange<Element>, stepSize: Element.Stride) {
self = Array(stride(from: range.lowerBound, through: range.upperBound, by: stepSize))
.map { $0.roundedWithPrecision(from: stepSize) }
}
}

View File

@ -0,0 +1,17 @@
//
// Collection+Content.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Collection {
/**
Check whether or not the collection has any elements.
*/
var hasContent: Bool { !isEmpty }
}

View File

@ -0,0 +1,20 @@
//
// Collection+HasContent.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Collection where Element: Hashable {
/**
Get distinct values from the collection, preserving the
original order.
*/
func distinct() -> [Element] {
reduce([]) { $0.contains($1) ? $0 : $0 + [$1] }
}
}

View File

@ -0,0 +1,34 @@
//
// Sequence+Batch.swift
// SwiftKit
//
// Created by Daniel Saidi on 2017-05-10.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Sequence {
/**
Batch the sequence into groups of a certain batch size.
*/
func batched(withBatchSize size: Int) -> [[Element]] {
var result: [[Element]] = []
var batch: [Element] = []
forEach {
batch.append($0)
if batch.count == size {
result.append(batch)
batch = []
}
}
if !batch.isEmpty {
result.append(batch)
}
return result
}
}

View File

@ -0,0 +1,20 @@
//
// Sequence+Group.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-04.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Sequence {
/**
Group the sequence into a dictionary using any property
from the sequence item type.
*/
func grouped<T>(by grouper: (Element) -> T) -> [T: [Element]] {
Dictionary(grouping: self, by: grouper)
}
}

View File

@ -0,0 +1,33 @@
//
// Comparable+Closest.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public enum PreferredClosestValue {
case greater, smaller
}
public extension Comparable {
/**
Get the closest value in the provided `collection`. The
provided `preferred` value whether to prefer a `greater`
or a `lower` value if no exact match was found.
*/
func closest(in collection: [Self], preferred: PreferredClosestValue) -> Self? {
if collection.contains(self) { return self }
let sorted = collection.sorted()
let greater = sorted.first { $0 > self }
let smaller = sorted.last { $0 < self }
switch preferred {
case .greater: return greater ?? smaller
case .smaller: return smaller ?? greater
}
}
}

View File

@ -0,0 +1,31 @@
//
// Comparable+Limit.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-04.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Comparable {
/**
Limit the value to a closed range.
*/
mutating func limit(to range: ClosedRange<Self>) {
self = limited(to: range)
}
/**
Return the value limited to a closed range.
This could be implemented in a oneliner, but that would
make the code less readable.
*/
func limited(to range: ClosedRange<Self>) -> Self {
if self < range.lowerBound { return range.lowerBound }
if self > range.upperBound { return range.upperBound }
return self
}
}

View File

@ -0,0 +1,26 @@
//
// ComparisonResult+Shortcuts.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension ComparisonResult {
/**
This is a shorthand to `.orderedAscending`
*/
static var ascending: ComparisonResult {
.orderedAscending
}
/**
This is a shorthand to `.orderedDescending`
*/
static var descending: ComparisonResult {
.orderedDescending
}
}

View File

@ -0,0 +1,51 @@
//
// DispatchQueue+Async.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-02.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// https://danielsaidi.com/blog/2020/06/03/dispatch-queue
//
import Foundation
public extension DispatchQueue {
/**
Perform an operation after a time interval.
*/
func asyncAfter(
_ interval: DispatchTimeInterval,
execute: @escaping () -> Void) {
asyncAfter(
deadline: .now() + interval,
execute: execute)
}
/**
Perform an operation after a time interval.
*/
func asyncAfter(
seconds: TimeInterval,
execute: @escaping () -> Void) {
let milli = Int(seconds * 1000)
asyncAfter(.milliseconds(milli), execute: execute)
}
/**
Perform an async operation then call a completion block
on another queue (default `.main`) with the result from
the async operation being passed on.
*/
func async<T>(
execute: @escaping () -> T,
then completion: @escaping (T) -> Void,
on completionQueue: DispatchQueue = .main) {
async {
let result = execute()
completionQueue.async {
completion(result)
}
}
}
}

View File

@ -0,0 +1,61 @@
//
// DispatchQueue+Throttle.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-09-17.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
private var lastDebounceCallTimes = [AnyHashable: DispatchTime]()
private let nilContext: AnyHashable = Int.random(in: 0...100_000)
private var throttleWorkItems = [AnyHashable: DispatchWorkItem]()
public extension DispatchQueue {
/**
Try to perform a debounced operation.
Executes a closure and ensures that no other executions
will be made during the provided `interval`.
- parameters:
- interval: The time to delay a closure execution, in seconds
- context: The context in which the debounce should be executed
- action: The closure to be executed
*/
func debounce(interval: Double, context: AnyHashable? = nil, action: @escaping () -> Void) {
let worker = DispatchWorkItem {
defer { throttleWorkItems.removeValue(forKey: context ?? nilContext) }
action()
}
asyncAfter(deadline: .now() + interval, execute: worker)
throttleWorkItems[context ?? nilContext]?.cancel()
throttleWorkItems[context ?? nilContext] = worker
}
/**
Try to perform a throttled operation.
Performs the first performed operation, then delays any
further operations until the provided `interval` passes.
- parameters:
- interval: The time to delay a closure execution, in seconds
- context: The context in which the throttle should be executed
- action: The closure to be executed
*/
func throttle(interval: Double, context: AnyHashable? = nil, action: @escaping () -> Void) {
if let last = lastDebounceCallTimes[context ?? nilContext], last + interval > .now() {
return
}
lastDebounceCallTimes[context ?? nilContext] = .now()
async(execute: action)
debounce(interval: interval) {
lastDebounceCallTimes.removeValue(forKey: context ?? nilContext)
}
}
}

View File

@ -0,0 +1,33 @@
//
// NSAttributedString+Archive.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-22.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension NSAttributedString {
/**
Try to create an attributed string with `data` that was
created with an `NSKeyedArchiver`.
*/
convenience init?(keyedArchiveData data: Data) throws {
let res = try NSKeyedUnarchiver.unarchivedObject(
ofClass: NSAttributedString.self,
from: data)
guard let string = res else { return nil }
self.init(attributedString: string)
}
/**
Try to generate `NSKeyedArchiver` data from the string.
*/
func getKeyedArchiveData() throws -> Data {
try NSKeyedArchiver.archivedData(
withRootObject: self,
requiringSecureCoding: false)
}
}

View File

@ -0,0 +1,43 @@
//
// NSAttributedString+Rtf.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-22.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension NSAttributedString {
/**
Try to create an attributed string with `data` that has
RTF formatted string content.
This extension aims to simplify the chore of creating a
proper attributed string from RTF data, since the Swift
api:s are old and requires a lot or bridging.
*/
convenience init(rtfData data: Data) throws {
let docTypeKey = NSAttributedString.DocumentReadingOptionKey.documentType
let rtfDocument = NSAttributedString.DocumentType.rtf
var attributes = [docTypeKey: rtfDocument] as NSDictionary?
try self.init(
data: data,
options: [.characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: &attributes)
}
/**
Try to generate RTF data from the attributed string.
*/
func getRtfData() throws -> Data {
let docTypeKey = NSAttributedString.DocumentAttributeKey.documentType
let rtfDocument = NSAttributedString.DocumentType.rtf
let attributes = [docTypeKey: rtfDocument]
let data = try data(
from: NSRange(location: 0, length: length),
documentAttributes: attributes)
return data
}
}

View File

@ -0,0 +1,42 @@
//
// NSAttributedString+Text.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-22.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension NSAttributedString {
/**
This error can be thrown by `getPlainTextData()`.
*/
enum PlainTextError: Error {
case invalidPlainTextData(inString: String)
}
/**
Try to create an attributed string with `data` that has
plain, .utf8 encoded string content.
*/
convenience init?(plainTextData data: Data) throws {
let decoded = String(data: data, encoding: .utf8)
guard let string = decoded else { return nil }
let attributed = NSAttributedString(string: string)
self.init(attributedString: attributed)
}
/**
Try to generate plain text data from the string.
*/
func getPlainTextData() throws -> Data {
guard let data = string.data(using: .utf8) else {
throw PlainTextError
.invalidPlainTextData(inString: string)
}
return data
}
}

View File

@ -0,0 +1,22 @@
//
// Optional+IsSet.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-09.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension Optional {
/**
Whether or not the value is `nil`.
*/
var isNil: Bool { self == nil }
/**
Whether or not the value is set and not `nil`.
*/
var isSet: Bool { self != nil }
}

View File

@ -0,0 +1,49 @@
//
// Result+Utils.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-04-28.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// https://danielsaidi.com/blog/2020/06/03/result-utils
//
import Foundation
public extension Result {
/**
Get the failure error, if any.
*/
var failureError: Failure? {
switch self {
case .failure(let error): return error
case .success: return nil
}
}
/**
Check whether or not the result is a failure result.
*/
var isFailure: Bool { !isSuccess }
/**
Check whether or not the result is a success result.
*/
var isSuccess: Bool {
switch self {
case .failure: return false
case .success: return true
}
}
/**
Get the success result, if any.
*/
var successResult: Success? {
switch self {
case .failure: return nil
case .success(let value): return value
}
}
}

View File

@ -0,0 +1,29 @@
//
// String+Base64.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-12-12.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// https://danielsaidi.com/blog/2020/06/04/string-base64
//
import Foundation
public extension String {
/**
Base64 decode the string.
*/
func base64Decoded() -> String? {
guard let data = Data(base64Encoded: self) else { return nil }
return String(data: data, encoding: .utf8)
}
/**
Base64 encode the string.
*/
func base64Encoded() -> String? {
data(using: .utf8)?.base64EncodedString()
}
}

View File

@ -0,0 +1,20 @@
//
// String+Bool.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-03.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
Parse the potential bool value in the string.
This function handles 1/0, yes/no, YES/NO etc., so it's
a good alternative to use e.g. when parsing plist files.
*/
var boolValue: Bool { (self as NSString).boolValue }
}

View File

@ -0,0 +1,26 @@
//
// String+Capitalize.swift
// SwiftKit
//
// Created by Daniel Saidi on 2022-01-11.
// Copyright © 2022 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
Return a copy where the first letter is capitalized.
*/
func capitalizingFirstLetter() -> String {
prefix(1).capitalized + dropFirst()
}
/**
Capitalize the first letter in the string.
*/
mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}
}

View File

@ -0,0 +1,23 @@
//
// String+Characters.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-29.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String.Element {
static var carriageReturn: String.Element { "\r" }
static var newLine: String.Element { "\n" }
static var tab: String.Element { "\t" }
}
public extension String {
static let newLine = String(.newLine)
static let tab = String(.tab)
}

View File

@ -0,0 +1,23 @@
//
// String+Contains.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-02-17.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// https://danielsaidi.com/blog/2020/06/04/string-contains
//
import Foundation
public extension String {
/**
Check whether or not the string contains another string.
*/
func contains(_ string: String, caseSensitive: Bool) -> Bool {
caseSensitive
? contains(string)
: range(of: string, options: .caseInsensitive) != nil
}
}

View File

@ -0,0 +1,27 @@
//
// String+Content.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
Check whether or not the string has any content.
*/
var hasContent: Bool {
!isEmpty
}
/**
Check whether or not the string has any trimmed content
after leading and trailing whitespaces are removed.
*/
var hasTrimmedContent: Bool {
!trimmingCharacters(in: .whitespaces).isEmpty
}
}

View File

@ -0,0 +1,28 @@
//
// String+Dictation.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-11-14.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
This function cleans up the string from space and other
strange characters that can be added to the string when
the user performs a dictation.
This happens on the Apple TV, when a user uses a remote
to dictate text into a text field. The resulting string
contains a bunch of additional information and not just
the plain string.
*/
func cleanedUpAfterDictation() -> String {
self
.replacingOccurrences(of: "\u{fffc}", with: "")
.trimmingCharacters(in: .whitespaces)
}
}

View File

@ -0,0 +1,52 @@
//
// String+Paragraph.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-29.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
Backs to find the index of the first new line paragraph
before the provided location, if any.
A new paragraph is considered to start at the character
after the newline char, not the newline itself.
*/
func findIndexOfCurrentParagraph(from location: UInt) -> UInt {
if isEmpty { return 0 }
let count = UInt(count)
var index = min(location, count-1)
repeat {
guard index > 0, index < count else { break }
guard let char = character(at: index - 1) else { break }
if char == .newLine || char == .carriageReturn { break }
index -= 1
} while true
return max(index, 0)
}
/**
Looks forward to find the next new line paragraph after
the provided location, if any. If no next paragraph can
be found, the current is returned.
A new paragraph is considered to start at the character
after the newline char, not the newline itself.
*/
func findIndexOfNextParagraph(from location: UInt) -> UInt {
var index = location
repeat {
guard let char = character(at: index) else { break }
index += 1
guard index < count else { break }
if char == .newLine || char == .carriageReturn { break }
} while true
let found = index < count
return found ? index : findIndexOfCurrentParagraph(from: location)
}
}

View File

@ -0,0 +1,32 @@
//
// String+Replace.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-01-08.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// Read more here:
// https://danielsaidi.com/blog/2020/06/04/string-replace
//
import Foundation
public extension String {
/**
This is a shortcut to `replacingOccurrences(of:with:)`.
*/
func replacing(_ string: String, with: String) -> String {
replacingOccurrences(of: string, with: with)
}
/**
This is a shortcut to `replacingOccurrences(of:with:)`,
with a `caseInsensitive` option enabled.
*/
func replacing(_ string: String, with: String, caseSensitive: Bool) -> String {
caseSensitive
? replacing(string, with: with)
: replacingOccurrences(of: string, with: with, options: .caseInsensitive)
}
}

View File

@ -0,0 +1,20 @@
//
// String+Split.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-08-23.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
Split the string using a list of separators.
*/
func split(by separators: [String]) -> [String] {
let separators = CharacterSet(charactersIn: separators.joined())
return components(separatedBy: separators)
}
}

View File

@ -0,0 +1,69 @@
//
// String+Subscript.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-11-29.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This extension makes it possible to fetch characters from a
string, as discussed here:
https://stackoverflow.com/questions/24092884/get-nth-character-of-a-string-in-swift-programming-language
*/
public extension StringProtocol {
func character(at index: Int) -> String.Element? {
guard count > index else { return nil }
return self[index]
}
func character(at index: UInt) -> String.Element? {
character(at: Int(index))
}
subscript(_ offset: Int) -> Element {
self[index(startIndex, offsetBy: offset)]
}
subscript(_ range: Range<Int>) -> SubSequence {
prefix(range.lowerBound+range.count).suffix(range.count)
}
subscript(_ range: ClosedRange<Int>) -> SubSequence {
prefix(range.lowerBound+range.count).suffix(range.count)
}
subscript(_ range: PartialRangeThrough<Int>) -> SubSequence {
prefix(range.upperBound.advanced(by: 1))
}
subscript(_ range: PartialRangeUpTo<Int>) -> SubSequence {
prefix(range.upperBound)
}
subscript(_ range: PartialRangeFrom<Int>) -> SubSequence {
suffix(Swift.max(0, count-range.lowerBound))
}
}
private extension LosslessStringConvertible {
var string: String { .init(self) }
}
private extension BidirectionalCollection {
subscript(safe offset: Int) -> Element? {
if isEmpty { return nil }
guard let index = index(
startIndex,
offsetBy: offset,
limitedBy: index(before: endIndex))
else { return nil }
return self[index]
}
}

View File

@ -0,0 +1,19 @@
//
// String+Trimmed.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-11-15.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
/**
This is a `trimmingCharacters(in: .whitespaces)` alias.
*/
func trimmed() -> String {
self.trimmingCharacters(in: .whitespaces)
}
}

View File

@ -0,0 +1,37 @@
//
// String+UrlEncode.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-12-12.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// https://danielsaidi.com/blog/2020/06/04/string-urlencode
//
import Foundation
public extension String {
/**
Encode the string to work with `x-www-form-urlencoded`.
This will first call `urlEncoded()`, then replace every
`+` with `%2B`.
*/
func formEncoded() -> String? {
self.urlEncoded()?
.replacingOccurrences(of: "+", with: "%2B")
}
/**
Encode the string to work with quary parameters.
This will first call `addingPercentEncoding`, using the
`.urlPathAllowed` character set, then replace every `&`
with `%26`.
*/
func urlEncoded() -> String? {
self.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)?
.replacingOccurrences(of: "&", with: "%26")
}
}

View File

@ -0,0 +1,30 @@
//
// Url+Global.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-08-31.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
`TODO` Update this extension with the urls from this page:
https://github.com/FifiTheBulldog/ios-settings-urls/blob/master/settings-urls.md
*/
public extension URL {
/**
This url leads to the Apple subscription screen for the
currently logged in account.
*/
static let userSubscriptions = URL(string: "https://apps.apple.com/account/subscriptions")
/**
This url leads to the App Store page for a certain app.
*/
static func appStoreUrl(forAppId appId: Int) -> URL? {
URL(string: "https://itunes.apple.com/app/id\(appId)")
}
}

View File

@ -0,0 +1,85 @@
//
// Url+QueryParameters.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-12-12.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension URL {
/**
Get the url's query parameters.
*/
var queryParameters: [URLQueryItem] {
URLComponents(string: absoluteString)?.queryItems ?? [URLQueryItem]()
}
/**
Get the url's query parameters as a dictionary.
*/
var queryParametersDictionary: [String: String] {
var result = [String: String]()
queryParameters.forEach { result[$0.name] = $0.value ?? "" }
return result
}
/**
Get a certain query parameter by name.
*/
func queryParameter(named name: String) -> URLQueryItem? {
queryParameters.first { $0.isNamed(name) }
}
/**
Set the value of a certain query parameter.
This will return a new url where the query parameter is
either updated or added.
*/
func setQueryParameter(name: String, value: String, urlEncode: Bool = true) -> URL? {
guard let urlString = absoluteString.components(separatedBy: "?").first else { return self }
let param = queryParameter(named: name)
let name = param?.name ?? name
var dictionary = queryParametersDictionary
dictionary[name] = urlEncode ? value.urlEncoded() : value
return URL(string: "\(urlString)?\(dictionary.queryString)")
}
/**
Set the value of a certain set of query parameters.
This will return a new url, where every query parameter
in the dictionary is either updated or added.
*/
func setQueryParameters(_ dict: [String: String], urlEncode: Bool = true) -> URL? {
var result = self
dict.forEach {
result = result.setQueryParameter(name: $0, value: $1) ?? result
}
return result
}
}
// MARK: - Dictionary Extensions
private extension Dictionary where Key == String, Value == String {
var queryString: String {
let parameters = map { "\($0)=\($1)" }
return parameters.joined(separator: "&")
}
}
// MARK: - URLQueryItem Extensions
private extension URLQueryItem {
func isNamed(_ name: String) -> Bool {
self.name.lowercased() == name.lowercased()
}
}

View File

@ -0,0 +1,30 @@
//
// UserDefaults+Codable.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-09-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension UserDefaults {
/**
Returns the codable object associated with the provided
key, provided that the persisted value can be decoded.
*/
func codable<T: Codable>(forKey key: String) -> T? {
guard let data = object(forKey: key) as? Data else { return nil }
let value = try? JSONDecoder().decode(T.self, from: data)
return value
}
/**
Persist a codable item.
*/
func setCodable<T: Codable>(_ codable: T, forKey key: String) {
let data = try? JSONEncoder().encode(codable)
set(data, forKey: key)
}
}

View File

@ -0,0 +1,47 @@
import Foundation
/**
This class can be used to find files witin a certain bundle.
*/
public class BundleFileFinder: FileFinder {
public init(bundle: Bundle = .main) {
self.bundle = bundle
}
private let bundle: Bundle
/**
Find files with names that start with a certain prefix.
*/
public func findFilesWithFileNamePrefix(_ prefix: String) -> [String] {
let format = "self BEGINSWITH %@"
let predicate = NSPredicate(format: format, argumentArray: [prefix])
return findFilesWithPredicate(predicate)
}
/**
Find files with names that end with a certain suffix.
*/
public func findFilesWithFileNameSuffix(_ suffix: String) -> [String] {
let format = "self ENDSWITH %@"
let predicate = NSPredicate(format: format, argumentArray: [suffix])
return findFilesWithPredicate(predicate)
}
}
private extension BundleFileFinder {
func findFilesWithPredicate(_ predicate: NSPredicate) -> [String] {
do {
let path = bundle.bundlePath
let fileManager = FileManager.default
let files = try fileManager.contentsOfDirectory(atPath: path)
let array = files as NSArray
let filteredFiles = array.filtered(using: predicate)
return filteredFiles as? [String] ?? []
} catch {
return [String]()
}
}
}

View File

@ -0,0 +1,28 @@
//
// FileDirectoryService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-12-19.
// Copyright © 2020 Daniel Saidi. All rights reserved.
import Foundation
/**
This service can be implemented by classes that can be used
to handle files within a certain local file directory.
*/
public protocol DirectoryService: AnyObject {
var directoryUrl: URL { get }
func createFile(named name: String, contents: Data?) -> Bool
func fileExists(withName name: String) -> Bool
func getAttributesForFile(named name: String) -> [FileAttributeKey: Any]?
func getFileNames() -> [String]
func getFileNames(matching fileNamePatterns: [String]) -> [String]
func getSizeOfAllFiles() -> UInt64
func getSizeOfFile(named name: String) -> UInt64?
func getUrlForFile(named name: String) -> URL?
func getUrlsForAllFiles() -> [URL]
func removeFile(named name: String) throws
}

View File

@ -0,0 +1,36 @@
//
// FileExporter.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-02-02.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by any classes that can be
used to export data to the file system.
*/
public protocol FileExporter {
typealias Completion = (Result<URL, Error>) -> Void
/**
Delete a previously exported file.
This function should be called when you are done with a
file, to avoid that the file system fills up with files
that are no longer used.
*/
func deleteFile(named fileName: String)
/**
Export the provided data to a certain file.
The resulting file url will depend on the file exporter
implementation. For instance, the `StandardFileExporter`
will store the file in the specified directory.
*/
func export(data: Data, to fileName: String, completion: @escaping Completion)
}

View File

@ -0,0 +1,18 @@
import Foundation
/**
This protocol can be implemented by types that can look for
files in various ways.
*/
public protocol FileFinder {
/**
Find files with names that start with a certain prefix.
*/
func findFilesWithFileNamePrefix(_ prefix: String) -> [String]
/**
Find files with names that end with a certain suffix.
*/
func findFilesWithFileNameSuffix(_ suffix: String) -> [String]
}

View File

@ -0,0 +1,40 @@
//
// FileManager+UniqueFileName.swift
// SwiftKit
//
// Created by Daniel Saidi on 2022-01-18.
// Copyright © 2022 Daniel Saidi. All rights reserved.
//
import Foundation
public extension FileManager {
/**
Get a unique destination for a certain destination file
URL, to ensure that no existing files are replaced.
For instance, if you have a destination url, and a file
already exists at that url, this function will add `-1`
to the file name and check if such a file exists. If it
doesn't the function will return the new url, otherwise
try with `-2`, `-3` etc. until no file exists.
*/
func getUniqueDestinationUrl(
for destinationUrl: URL,
separator: String = "-") -> URL {
if !fileExists(atPath: destinationUrl.path) { return destinationUrl }
let fileExtension = destinationUrl.pathExtension
let noExtension = destinationUrl.deletingPathExtension()
let fileName = noExtension.lastPathComponent
var counter = 1
repeat {
let newUrl = noExtension
.deletingLastPathComponent()
.appendingPathComponent(fileName.appending("\(separator)\(counter)"))
.appendingPathExtension(fileExtension)
if !fileExists(atPath: newUrl.path) { return newUrl }
counter += 1
} while true
}
}

View File

@ -0,0 +1,96 @@
//
// StandardFileDirectoryService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-12-19.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This is a standard implementation of the `DirectoryService`.
You can inherit and override any parts of it.
*/
open class StandardDirectoryService: DirectoryService {
// MARK: - Initialization
public init?(
directory: FileManager.SearchPathDirectory,
fileManager: FileManager = .default) {
guard let dir = fileManager.urls(for: directory, in: .userDomainMask).last else { return nil }
self.directoryUrl = dir
self.fileManager = fileManager
}
public init(
fileManager: FileManager = .default,
directoryUrl: URL) {
self.directoryUrl = directoryUrl
self.fileManager = fileManager
}
// MARK: - Properties
public let directoryUrl: URL
private let fileManager: FileManager
// MARK: - Public Functions
open func createFile(named name: String, contents: Data?) -> Bool {
let url = directoryUrl.appendingPathComponent(name)
return fileManager.createFile(atPath: url.path, contents: contents, attributes: nil)
}
open func fileExists(withName name: String) -> Bool {
getUrlForFile(named: name) != nil
}
open func getAttributesForFile(named name: String) -> [FileAttributeKey: Any]? {
guard let url = getUrlForFile(named: name) else { return nil }
return try? fileManager.attributesOfItem(atPath: url.path)
}
open func getFileNames() -> [String] {
guard let urls = try? fileManager.contentsOfDirectory(at: directoryUrl, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { return [] }
return urls.map { $0.lastPathComponent }
}
open func getFileNames(matching fileNamePatterns: [String]) -> [String] {
let patterns = fileNamePatterns.map { $0.lowercased() }
return getFileNames().filter {
let fileName = $0.lowercased()
return patterns.filter { fileName.contains($0) }.first != nil
}
}
open func getSizeOfAllFiles() -> UInt64 {
getFileNames().reduce(0) { $0 + (getSizeOfFile(named: $1) ?? 0) }
}
open func getSizeOfFile(named name: String) -> UInt64? {
guard let attributes = getAttributesForFile(named: name) else { return nil }
let number = attributes[FileAttributeKey.size] as? NSNumber
return number?.uint64Value
}
open func getUrlForFile(named name: String) -> URL? {
let urls = try? fileManager.contentsOfDirectory(at: directoryUrl, includingPropertiesForKeys: nil)
return urls?.first { $0.lastPathComponent == name }
}
open func getUrlsForAllFiles() -> [URL] {
getFileNames().compactMap {
getUrlForFile(named: $0)
}
}
open func removeFile(named name: String) throws {
guard let url = getUrlForFile(named: name) else { return }
try fileManager.removeItem(at: url)
}
}

View File

@ -0,0 +1,70 @@
//
// StandardFileExporter.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-02-02.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This file exporter can export data to the file system using
a file manager and a certain directory.
*/
public class StandardFileExporter: FileExporter {
public init(
fileManager: FileManager = .default,
directory: FileManager.SearchPathDirectory = .documentDirectory) {
self.fileManager = fileManager
self.directory = directory
}
private let fileManager: FileManager
private let directory: FileManager.SearchPathDirectory
public enum ExportError: Error {
case invalidUrl
}
/**
Delete a previously exported file.
This function should be called when you are done with a
file, to avoid that the file system fills up with files
that are no longer used.
*/
public func deleteFile(named fileName: String) {
guard let url = getFileUrl(forFileName: fileName) else { return }
try? fileManager.removeItem(at: url)
}
/**
Export the provided data to a certain file.
The resulting file url will depend on the file exporter
implementation. For instance, the `StandardFileExporter`
will store the file in the specified directory.
*/
public func export(data: Data, to fileName: String, completion: @escaping Completion) {
guard let url = getFileUrl(forFileName: fileName) else { return completion(.failure(ExportError.invalidUrl)) }
tryWrite(data: data, to: url, completion: completion)
}
}
private extension StandardFileExporter {
func getFileUrl(forFileName fileName: String) -> URL? {
fileManager.urls(for: directory, in: .userDomainMask).first?.appendingPathComponent(fileName)
}
func tryWrite(data: Data, to url: URL, completion: @escaping Completion) {
do {
try data.write(to: url, options: .atomicWrite)
completion(.success(url))
} catch {
completion(.failure(error))
}
}
}

View File

@ -0,0 +1,30 @@
//
// AppleMapsService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-02-18.
// Copyright © 2015 Daniel Saidi. All rights reserved.
//
import CoreLocation
public class AppleMapsService: ExternalMapService {
public init() {}
/**
Get the external url of a certain coordinate.
*/
public func getUrl(for coordinate: CLLocationCoordinate2D) -> URL? {
let string = "http://maps.apple.com/maps?ll=\(coordinate.latitude),\(coordinate.longitude)"
return URL(string: string)
}
/**
Get the external url of a certain navigation.
*/
public func getUrl(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> URL? {
let string = "http://maps.apple.com/maps?saddr=\(from.latitude),\(from.longitude)&daddr=\(to.latitude),\(to.longitude)"
return URL(string: string)
}
}

View File

@ -0,0 +1,16 @@
//
// CLLocationCoordinate2D+Equatable.swift
// SwiftKit
//
// Created by Daniel Saidi on 2021-09-08.
// Copyright © 2021 Daniel Saidi. All rights reserved.
//
import CoreLocation
extension CLLocationCoordinate2D: Equatable {
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}

View File

@ -0,0 +1,28 @@
//
// CLLocationCoordinate2D+Valid.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-09-18.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import CoreLocation
public extension CLLocationCoordinate2D {
/**
Check if the coordinate is valid. This is a best effort
that checks so that not both the latitude and longitude
are not or any extremes.
*/
var isValid: Bool {
isValid(latitude) && isValid(longitude)
}
}
private extension CLLocationCoordinate2D {
func isValid(_ degrees: CLLocationDegrees) -> Bool {
degrees != 0 && degrees != 180 && degrees != -180
}
}

View File

@ -0,0 +1,27 @@
//
// ExternalMapService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-02-18.
// Copyright © 2015 Daniel Saidi. All rights reserved.
//
import CoreLocation
/**
This protocol can be implemented by services that provide a
set of urls to coordinates or navigation paths, that can be
opened in an external map application.
*/
public protocol ExternalMapService {
/**
Get the external url of a certain coordinate.
*/
func getUrl(for coordinate: CLLocationCoordinate2D) -> URL?
/**
Get the external url of a certain navigation.
*/
func getUrl(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> URL?
}

View File

@ -0,0 +1,30 @@
//
// GoogleMapsService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-02-18.
// Copyright © 2015 Daniel Saidi. All rights reserved.
//
import CoreLocation
public class GoogleMapsService: ExternalMapService {
public init() {}
/**
Get the external url of a certain coordinate.
*/
public func getUrl(for coordinate: CLLocationCoordinate2D) -> URL? {
let string = "comgooglemaps://?center=\(coordinate.latitude),\(coordinate.longitude)"
return URL(string: string)
}
/**
Get the external url of a certain navigation.
*/
public func getUrl(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> URL? {
let string = "comgooglemaps://?saddr=\(from.latitude),\(from.longitude)&daddr=\(to.latitude),\(to.longitude)"
return URL(string: string)
}
}

View File

@ -0,0 +1,44 @@
//
// WorldCoordinate.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-10-04.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import CoreLocation
/**
This struct can be used to represent world coordinates, but
without bloating `CLLocationCoordinate2D` with static props.
The reason to why this is a struct and not an enum, is that
it simplifies extending it with new coordinates in any apps
that use custom coordinates.
*/
public struct WorldCoordinate: Hashable, Equatable, Identifiable {
public var id: String { name }
/**
The name of the coordinate.
*/
public let name: String
/**
The coordinate value.
*/
public let coordinate: CLLocationCoordinate2D
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
public extension WorldCoordinate {
static var manhattan: WorldCoordinate = .init(name: "Manhattan", coordinate: CLLocationCoordinate2D(latitude: 40.7590615, longitude: -73.969231))
static var newYork: WorldCoordinate = .init(name: "New York", coordinate: CLLocationCoordinate2D(latitude: 40.7033127, longitude: -73.979681))
static var sanFrancisco: WorldCoordinate = .init(name: "San Francisco", coordinate: CLLocationCoordinate2D(latitude: 37.7796828, longitude: -122.4000062))
static var tokyo: WorldCoordinate = .init(name: "Tokyo", coordinate: CLLocationCoordinate2D(latitude: 35.673, longitude: 139.710))
}

View File

@ -0,0 +1,75 @@
//
// KeychainItemAccessibility.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// Based on https://github.com/jrendel/SwiftKeychainWrapper
// Created by James Blair on 4/24/16.
// Copyright © 2016 Jason Rendel. All rights reserved.
import Foundation
public protocol KeychainAttrRepresentable {
var keychainAttrValue: CFString { get }
}
/**
This enum defines the various access scopes that a keychain
item can use. The names follow certain conventions that are
defined in the list below:
* `afterFirstUnlock`
The attribute cannot be accessed after a restart, until the
device has been unlocked once by the user. After this first
unlock, the items remains accessible until the next restart.
This is recommended for items that must be available to any
background applications or processes.
* `ThisDeviceOnly`
The attribute will not be included in encrypted backup, and
are thus not available after restoring apps from backups on
a different device.
* `whenPasscodeSet`
The attribute can only be accessed when the device has been
unlocked by the user and a device passcode is set. No items
can be stored on device if a passcode is not set. Disabling
the passcode will delete all items.
* `whenUnlocked`
The attribute can only be accessed when the device has been
unlocked by the user. This is recommended for items that we
only mean to use when the application is active.
*/
public enum KeychainItemAccessibility {
case afterFirstUnlock
case afterFirstUnlockThisDeviceOnly
case whenPasscodeSetThisDeviceOnly
case whenUnlocked
case whenUnlockedThisDeviceOnly
static func accessibilityForAttributeValue(_ keychainAttrValue: CFString) -> KeychainItemAccessibility? {
keychainItemAccessibilityLookup.first { $0.value == keychainAttrValue }?.key
}
}
private let keychainItemAccessibilityLookup: [KeychainItemAccessibility: CFString] = [
.afterFirstUnlock: kSecAttrAccessibleAfterFirstUnlock,
.afterFirstUnlockThisDeviceOnly: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
.whenPasscodeSetThisDeviceOnly: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.whenUnlocked: kSecAttrAccessibleWhenUnlocked,
.whenUnlockedThisDeviceOnly: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
extension KeychainItemAccessibility: KeychainAttrRepresentable {
public var keychainAttrValue: CFString {
keychainItemAccessibilityLookup[self]!
}
}

View File

@ -0,0 +1,26 @@
//
// KeychainReader.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by keychain-based services
that can read from the device keychain.
*/
public protocol KeychainReader: AnyObject {
func accessibility(for key: String) -> KeychainItemAccessibility?
func bool(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool?
func data(for key: String, with accessibility: KeychainItemAccessibility?) -> Data?
func dataRef(for key: String, with accessibility: KeychainItemAccessibility?) -> Data?
func double(for key: String, with accessibility: KeychainItemAccessibility?) -> Double?
func float(for key: String, with accessibility: KeychainItemAccessibility?) -> Float?
func hasValue(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
func integer(for key: String, with accessibility: KeychainItemAccessibility?) -> Int?
func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String?
}

View File

@ -0,0 +1,15 @@
//
// KeychainService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by keychain-based services
that can read from and write to the device keychain.
*/
public protocol KeychainService: KeychainReader, KeychainWriter {}

View File

@ -0,0 +1,310 @@
//
// KeychainWrapper.swift
// SwiftKit
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
// Based on https://github.com/jrendel/SwiftKeychainWrapper
// Created by Jason Rendel on 9/23/14.
// Copyright © 2014 Jason Rendel. All rights reserved.
import Foundation
private let paramSecMatchLimit = kSecMatchLimit as String
private let paramSecReturnData = kSecReturnData as String
private let paramSecReturnPersistentRef = kSecReturnPersistentRef as String
private let paramSecValueData = kSecValueData as String
private let paramSecAttrAccessible = kSecAttrAccessible as String
private let paramSecClass = kSecClass as String
private let paramSecAttrService = kSecAttrService as String
private let paramSecAttrGeneric = kSecAttrGeneric as String
private let paramSecAttrAccount = kSecAttrAccount as String
private let paramSecAttrAccessGroup = kSecAttrAccessGroup as String
private let paramSecReturnAttributes = kSecReturnAttributes as String
/**
This class help make device keychain access easier in Swift.
It is designed to make accessing the Keychain services more
like using `NSUserDefaults`, which is much more familiar to
developers in general.
`serviceName` is used for `kSecAttrService`, which uniquely
identifies keychain accessors. If no name is specified, the
value defaults to the current bundle identifier.
`accessGroup` is used for `kSecAttrAccessGroup`. This value
is used to identify which keychain access group an entry is
belonging to. This allows you to use `KeychainWrapper` with
shared keychain access between different applications.
`NOTE` In SwiftKit, you can use a `StandardKeychainService`
to isolate keychain access from contract design.
*/
open class KeychainWrapper {
// MARK: - Initialization
/**
Create a standard instance of this class.
*/
private convenience init() {
let id = Bundle.main.bundleIdentifier
let fallback = "com.swiftkit.keychain"
self.init(serviceName: id ?? fallback)
}
/**
Create a custom instance of this class.
- parameter serviceName: The service name for this instance. Used to uniquely identify all keys stored using this keychain wrapper instance.
- parameter accessGroup: An optional, unique access group for this instance. Use a matching AccessGroup between applications to allow shared keychain access.
*/
public init(serviceName: String, accessGroup: String? = nil) {
self.serviceName = serviceName
self.accessGroup = accessGroup
}
// MARK: - Properties
/**
The standard keychain wrapper instance.
*/
public static let standard = KeychainWrapper()
/**
This is used to uniquely identify the keychain wrapper.
*/
private let serviceName: String
/**
This is used to identify to which Keychain Access Group
this entry belongs. This allows you to use this wrapper
with shared access between applications.
*/
private let accessGroup: String?
// MARK: - KeychainReader
open func accessibility(for key: String) -> KeychainItemAccessibility? {
var dict = setupKeychainQueryDictionary(forKey: key)
var result: AnyObject?
dict.removeValue(forKey: paramSecAttrAccessible)
dict[paramSecMatchLimit] = kSecMatchLimitOne
dict[paramSecReturnAttributes] = kCFBooleanTrue
let status = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
}
if status == noErr,
let dict = result as? [String: AnyObject],
let val = dict[paramSecAttrAccessible] as? String {
return KeychainItemAccessibility.accessibilityForAttributeValue(val as CFString)
}
return nil
}
open func bool(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool? {
number(for: key, with: accessibility)?.boolValue
}
open func data(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Data? {
var dict = setupKeychainQueryDictionary(forKey: key, with: accessibility)
var result: AnyObject?
dict[paramSecMatchLimit] = kSecMatchLimitOne
dict[paramSecReturnData] = kCFBooleanTrue
let status = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
}
return status == noErr ? result as? Data: nil
}
open func double(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Double? {
number(for: key, with: accessibility)?.doubleValue
}
open func float(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Float? {
number(for: key, with: accessibility)?.floatValue
}
open func hasValue(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
data(for: key, with: accessibility) != nil
}
open func integer(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Int? {
number(for: key, with: accessibility)?.intValue
}
open func number(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> NSNumber? {
object(for: key, with: accessibility)
}
open func object<T: NSObject & NSCoding>(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> T? {
guard let keychainData = data(for: key, with: accessibility) else { return nil }
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: keychainData)
}
open func string(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> String? {
guard let keychainData = data(for: key, with: accessibility) else { return nil }
return String(data: keychainData, encoding: String.Encoding.utf8) as String?
}
open func dataRef(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Data? {
var dict = setupKeychainQueryDictionary(forKey: key, with: accessibility)
var result: AnyObject?
dict[paramSecMatchLimit] = kSecMatchLimitOne
dict[paramSecReturnPersistentRef] = kCFBooleanTrue
let status = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
}
return status == noErr ? result as? Data: nil
}
// MARK: - KeychainWriter
@discardableResult
open func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
set(NSNumber(value: value), for: key, with: accessibility)
}
@discardableResult
open func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
set(NSNumber(value: value), for: key, with: accessibility)
}
@discardableResult
open func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
set(NSNumber(value: value), for: key, with: accessibility)
}
@discardableResult
open func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
set(NSNumber(value: value), for: key, with: accessibility)
}
@discardableResult
open func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
return set(data, for: key, with: accessibility)
}
@discardableResult
open func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false) else { return false }
return set(data, for: key, with: accessibility)
}
@discardableResult
open func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
var dict: [String: Any] = setupKeychainQueryDictionary(forKey: key, with: accessibility)
dict[paramSecValueData] = value
if let accessibility = accessibility {
dict[paramSecAttrAccessible] = accessibility.keychainAttrValue
} else {
// Assign default protection - Protect the keychain entry so it's only valid when the device is unlocked
dict[paramSecAttrAccessible] = KeychainItemAccessibility.whenUnlocked.keychainAttrValue
}
let status = SecItemAdd(dict as CFDictionary, nil)
if status == errSecDuplicateItem {
return update(value, forKey: key, with: accessibility)
}
return status == errSecSuccess
}
@discardableResult
open func removeObject(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
let keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key, with: accessibility)
let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
return status == errSecSuccess
}
/**
Remove all keychain items added with this wrapper. This
will only delete items matching the current ServiceName
and AccessGroup, if one is set.
*/
open func removeAllKeys() -> Bool {
var dict: [String: Any] = [paramSecClass: kSecClassGenericPassword]
dict[paramSecAttrService] = serviceName
if let accessGroup = self.accessGroup {
dict[paramSecAttrAccessGroup] = accessGroup
}
let status = SecItemDelete(dict as CFDictionary)
return status == errSecSuccess
}
/**
Remove all keychain data, including data not added with
this keychain wrapper.
- Warning: This may remove custom keychain entries that
you did not add via this wrapper.
*/
open class func wipeKeychain() {
deleteKeychainSecClass(kSecClassGenericPassword) // Generic password items
deleteKeychainSecClass(kSecClassInternetPassword) // Internet password items
deleteKeychainSecClass(kSecClassCertificate) // Certificate items
deleteKeychainSecClass(kSecClassKey) // Cryptographic key items
deleteKeychainSecClass(kSecClassIdentity) // Identity items
}
}
// MARK: - Private Methods
private extension KeychainWrapper {
/**
Remove all items for a given Keychain Item Class
*/
@discardableResult
class func deleteKeychainSecClass(_ secClass: AnyObject) -> Bool {
let query = [paramSecClass: secClass]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
}
/**
Update ayn existing data associated with a specific key
name. The existing data will be overwritten by new data.
*/
func update(_ value: Data, forKey key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
var keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key, with: accessibility)
let updateDictionary = [paramSecValueData: value]
if let accessibility = accessibility {
keychainQueryDictionary[paramSecAttrAccessible] = accessibility.keychainAttrValue
}
let status = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary)
return status == errSecSuccess
}
/**
Setup the keychain query dictionary, used to access the
keychain on iOS for a specific key name and taking into
account the Service Name and Access Group if one is set.
- parameter forKey: The key this query is for
- parameter with: Optional accessibility to use when setting the keychain item. If none is provided, will default to .WhenUnlocked
- returns: A dictionary with all the needed properties setup to access the keychain on iOS
*/
func setupKeychainQueryDictionary(forKey key: String, with accessibility: KeychainItemAccessibility? = nil) -> [String: Any] {
var dict: [String: Any] = [paramSecClass: kSecClassGenericPassword]
dict[paramSecAttrService] = serviceName
if let accessibility = accessibility {
dict[paramSecAttrAccessible] = accessibility.keychainAttrValue
}
if let accessGroup = self.accessGroup {
dict[paramSecAttrAccessGroup] = accessGroup
}
let encodedIdentifier = key.data(using: String.Encoding.utf8)
dict[paramSecAttrGeneric] = encodedIdentifier
dict[paramSecAttrAccount] = encodedIdentifier
return dict
}
}

View File

@ -0,0 +1,43 @@
//
// KeychainWriter.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by keychain-based services
that can write to the user's keychain.
*/
public protocol KeychainWriter: AnyObject {
@discardableResult
func removeObject(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func removeAllKeys() -> Bool
@discardableResult
func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
@discardableResult
func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
}

View File

@ -0,0 +1,114 @@
//
// StandardKeychainService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-11-24.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This is a standard implementation of `KeychainService` that
uses a `KeychainWrapper` to sync data with the keychain.
*/
public class StandardKeychainService: KeychainService {
public init(wrapper: KeychainWrapper = .standard) {
self.wrapper = wrapper
}
private let wrapper: KeychainWrapper
}
// MARK: - KeychainReader
extension StandardKeychainService {
public func accessibility(for key: String) -> KeychainItemAccessibility? {
wrapper.accessibility(for: key)
}
public func bool(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool? {
wrapper.bool(for: key, with: accessibility)
}
public func data(for key: String, with accessibility: KeychainItemAccessibility?) -> Data? {
wrapper.data(for: key, with: accessibility)
}
public func dataRef(for key: String, with accessibility: KeychainItemAccessibility?) -> Data? {
wrapper.dataRef(for: key, with: accessibility)
}
public func double(for key: String, with accessibility: KeychainItemAccessibility?) -> Double? {
wrapper.double(for: key, with: accessibility)
}
public func float(for key: String, with accessibility: KeychainItemAccessibility?) -> Float? {
wrapper.float(for: key, with: accessibility)
}
public func hasValue(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.hasValue(for: key, with: accessibility)
}
public func integer(for key: String, with accessibility: KeychainItemAccessibility?) -> Int? {
wrapper.integer(for: key, with: accessibility)
}
public func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String? {
wrapper.string(for: key, with: accessibility)
}
}
// MARK: - KeychainWriter
extension StandardKeychainService {
@discardableResult
public func removeObject(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.removeObject(for: key, with: accessibility)
}
public func removeAllKeys() -> Bool {
wrapper.removeAllKeys()
}
@discardableResult
public func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
@discardableResult
public func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
@discardableResult
public func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
@discardableResult
public func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
@discardableResult
public func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
@discardableResult
public func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
@discardableResult
public func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
wrapper.set(value, for: key, with: accessibility)
}
}

View File

@ -0,0 +1,28 @@
//
// BundleTranslator.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-04-15.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This `Translator` translates keys using a certain `Bundle`.
*/
public class BundleTranslator: Translator {
public init(bundle: Bundle) {
self.bundle = bundle
}
private let bundle: Bundle
/**
Translate the provided key to a localized string.
*/
public func translate(_ key: String) -> String {
bundle.localizedString(forKey: key, value: "", table: nil)
}
}

View File

@ -0,0 +1,34 @@
//
// LocalizationNotification.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-03-19.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension NSNotification.Name {
/**
Gets a localization-specific notification.
*/
static func localization(_ notification: LocalizationNotification) -> NSNotification.Name {
notification.name
}
}
/**
This enum has localization-specific notifications.
*/
public enum LocalizationNotification: String {
case
localeWillChange,
localeDidChange
public var name: NSNotification.Name {
NSNotification.Name(rawValue: "com.danielsaidi.swiftkit.\(rawValue)")
}
}

View File

@ -0,0 +1,24 @@
//
// LocalizationService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-03-06.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented any ``Translator`` that is
also capable of changing the app's current locale.
Implementations of this protocol should make sure to post a
``LocalizationNotification`` when the app locale changes.
*/
public protocol LocalizationService: Translator {
/**
Change the service's locale.
*/
func setLocale(_ locale: Locale) throws
}

View File

@ -0,0 +1,74 @@
//
// StandardLanguageService.swift
// SwiftKit
//
// Created by Daniel Saidi on 2016-03-06.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This service lets you change the locale of your app without
having to restart the app for the change to be applied.
This service wraps a translator, which is uses to translate
language keys. A `StandardTranslator` will be used at first,
but as soon as you call `setLocale` the standard translator
will replaced with a `BundleTranslator`, that will used the
bundle of the new locale.
*/
open class StandardLocalizationService: LocalizationService {
public init(
translator: Translator = StandardTranslator(),
bundle: Bundle = .main,
notificationCenter: NotificationCenter = .default,
userDefaults: UserDefaults = .standard) {
self.translator = translator
self.bundle = bundle
self.notificationCenter = notificationCenter
self.userDefaults = userDefaults
}
private let bundle: Bundle
private let notificationCenter: NotificationCenter
private var translator: Translator
private let userDefaults: UserDefaults
public enum LocaleError: Error {
case languageCodeIsMissing(for: Locale)
case lprojFileDoesNotExist(for: Locale)
}
/**
Change the service's locale.
*/
open func setLocale(_ locale: Locale) throws {
guard let languageCode = locale.languageCode else { throw LocaleError.languageCodeIsMissing(for: locale) }
guard loadBundle(for: languageCode) else { throw LocaleError.lprojFileDoesNotExist(for: locale) }
notificationCenter.post(name: .localization(.localeWillChange), object: nil)
userDefaults.set([languageCode], forKey: "AppleLanguages")
notificationCenter.post(name: .localization(.localeDidChange), object: nil)
}
/**
Translate the provided key to a localized string.
*/
open func translate(_ key: String) -> String {
translator.translate(key)
}
}
// MARK: - Private Functions
private extension StandardLocalizationService {
func loadBundle(for locale: String) -> Bool {
guard let path = bundle.path(forResource: locale, ofType: "lproj") else { return false }
guard let bundle = Bundle(path: path) else { return false }
translator = BundleTranslator(bundle: bundle)
return true
}
}

View File

@ -0,0 +1,25 @@
//
// StandardTranslator.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-04-15.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This standard ``Translator`` implementation translates keys
using `NSLocalizedString`.
*/
public class StandardTranslator: Translator {
public init() {}
/**
Translate the provided key to a localized string.
*/
public func translate(_ key: String) -> String {
NSLocalizedString(key, comment: "")
}
}

View File

@ -0,0 +1,21 @@
//
// Translator.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-04-15.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by any classes that can be
used to translate a localized string synchronously.
*/
public protocol Translator: AnyObject {
/**
Translate the provided key to a localized string.
*/
func translate(_ key: String) -> String
}

Some files were not shown because too many files have changed in this diff Show More