Compare commits

...

206 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
219 changed files with 6181 additions and 1876 deletions

32
.gitignore vendored
View File

@ -4,33 +4,11 @@
/Packages
.swiftpm/
# Build generated
build/
DerivedData/
# Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
# Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
# Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# Bundler
.bundle
# Documentation
Docs
documentation
downloads
videos
# Fastlane
Fastlane/report.xml

View File

@ -1,4 +1,8 @@
disabled_rules:
- function_body_length
- identifier_name
- line_length
- todo
- trailing_whitespace
- type_name
- vertical_whitespace

View File

@ -1,9 +1,9 @@
# Run `pod lib lint SwiftKit.podspec' to ensure this is a valid spec.
# Run `pod lib lint DSSwiftKit.podspec' to ensure this is a valid spec.
Pod::Spec.new do |s|
s.name = 'DSSwiftKit'
s.version = '0.0.4'
s.swift_versions = ['5.2']
s.version = '1.3.0'
s.swift_versions = ['5.3']
s.summary = 'SwiftKit contains extra functionality for Swift.'
s.description = <<-DESC
@ -16,14 +16,11 @@ Pod::Spec.new do |s|
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.2'
s.ios.deployment_target = '11.0'
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'
s.watchos.exclude_files = [
'Sources/SwiftKit/Authentication/BiometricAuthenticationService.swift',
]
end

View File

@ -4,28 +4,103 @@ 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]
if bump_type == nil or bump_type.empty?
bump_type = "patch"
end
version = version_bump_podspec(path: "DSSwiftKit.podspec", bump_type: bump_type)
# increment_version_number(version_number: version)
git_commit(
path: "*",
message: "Bump to #{version}"
)
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

View File

@ -1,34 +1,50 @@
{
"object": {
"pins": [
{
"package": "Mockery",
"repositoryURL": "https://github.com/danielsaidi/Mockery.git",
"state": {
"branch": null,
"revision": "873da92ef23789e0065fa25dff234f5cde03500b",
"version": "0.3.2"
}
},
{
"package": "Nimble",
"repositoryURL": "https://github.com/Quick/Nimble.git",
"state": {
"branch": null,
"revision": "7fd118ec8795888bcbbebc1a41f6984454c4cd6f",
"version": "8.0.7"
}
},
{
"package": "Quick",
"repositoryURL": "https://github.com/Quick/Quick.git",
"state": {
"branch": null,
"revision": "33682c2f6230c60614861dfc61df267e11a1602f",
"version": "2.2.0"
}
"pins" : [
{
"identity" : "cwlcatchexception",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : {
"revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea",
"version" : "2.1.1"
}
]
},
"version": 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
}

View File

@ -1,23 +1,25 @@
// swift-tools-version:5.2
// swift-tools-version:5.6
import PackageDescription
let package = Package(
name: "SwiftKit",
platforms: [
.iOS(.v11),
.iOS(.v13),
.macOS(.v11),
.tvOS(.v13),
.watchOS(.v6)
],
products: [
.library(
name: "SwiftKit",
targets: ["SwiftKit"]),
targets: ["SwiftKit"]
),
],
dependencies: [
.package(url: "https://github.com/Quick/Quick.git", from: "2.2.0"),
.package(url: "https://github.com/Quick/Nimble.git", from: "8.0.0"),
.package(url: "https://github.com/danielsaidi/Mockery.git", from: "0.3.0")
.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(
@ -25,6 +27,6 @@ let package = Package(
dependencies: []),
.testTarget(
name: "SwiftKitTests",
dependencies: ["SwiftKit", "Quick", "Nimble", "Mockery"]),
dependencies: ["SwiftKit", "Quick", "Nimble", "MockingKit"]),
]
)

View File

@ -1,88 +1,79 @@
<p align="center">
<img src ="Resources/Logo.png" width=500 />
<img src ="Resources/Logo.png" alt="SwiftKit Logo" title="SwiftKit" width=600 />
</p>
<p align="center">
<a href="https://github.com/danielsaidi/SwiftKit">
<img src="https://badge.fury.io/gh/danielsaidi%2FSwiftKit.svg?style=flat" alt="Version" />
<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>
<img src="https://img.shields.io/badge/platform-SwiftUI-red.svg" alt="Swift UI" />
<img src="https://img.shields.io/badge/Swift-5.2-orange.svg" alt="Swift 5.2" />
<img src="https://badges.frapsoft.com/os/mit/mit.svg?style=flat&v=102" alt="License" />
<a href="https://twitter.com/danielsaidi">
<img src="https://img.shields.io/badge/contact-@danielsaidi-blue.svg?style=flat" alt="Twitter: @danielsaidi" />
<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 functionality to `Swift`, both extensions to existing types and components, as well as custom useful functionality.
SwiftKit adds extra functionality to the Swift programming language, like extensions to already existing types as well as completely new stuff.
You can read more about the different parts of `SwiftKit` in separate readmes:
* [Authentication][Authentication]
* [Coding][Coding]
* [IoC][IoC]
* [Extensions][Extensions]
If things that exist in this library are added to `Swift`, the corresponding functionality in this library will be deprecated and refer to those new features.
## Demo App
This project contains a demo app that demonstrates most things from the library. To run the demo app, just open and run the `SwiftKit.xcodeproj` project.
## Installation
### Swift Package Manager
SwiftKit can be installed with the Swift Package Manager:
```
https://github.com/danielsaidi/SwiftKit.git
```
### CocoaPods
```
pod DSSwiftKit
```
Note that you have to import `SwiftKit` with `import DSSwiftKit` if you add it with CocoaPods. The name `SwiftKit` was (not surprising) already taken.
If you prefer to not have external dependencies, you can also just copy the source code into your app.
## <a name="why"></a>Why a big "Kit"?
Having a big "kit" is a bad idea, since it may become too generic and hard to overview. For instance, if I want to modify dates, I'm probably not going to dig around in SwiftKit looking for extensions, but rather use the best single-purpose date library.
## Documentation
The GitHub stars confirm this as well. Many years ago, I started putting reusable iOS functionality into a library I call [iExtra](https://github.com/danielsaidi/iExtra). I think it's an amazing library and I use it in most of my apps. To this day, it has 12 stars.
However, each project comes with overhead. If I decided to create a new library for each little thing I make, I'd spend a lot of time setting up each new project. There are funny names to come up with, icons and logos to be made, maintenance etc.
SO instead of honoring every little idea with a new library, I'm going to put most here until they are either too large or too good to be a part of this library. When that happens, they'll break out of their little cage and move into a library of their own.
The [online documentation][Documentation] has more information, code examples, etc., and makes it easy to overview the various parts of the library.
## Contact me
## 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:
* E-mail: [daniel.saidi@gmail.com][Email]
* Website: [danielsaidi.com][Website]
* Mastodon: [@danielsaidi@mastodon.social][Mastodon]
* Twitter: [@danielsaidi][Twitter]
* Web site: [danielsaidi.com][Website]
* 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 [LICENSE][License] file for more info.
SwiftKit is available under the MIT license. See the [LICENSE][License] file for more info.
[Email]: mailto:daniel.saidi@gmail.com
[Twitter]: http://www.twitter.com/danielsaidi
[Website]: http://www.danielsaidi.com
[Website]: https://www.danielsaidi.com
[Twitter]: https://www.twitter.com/danielsaidi
[Mastodon]: https://mastodon.social/@danielsaidi
[Sponsors]: https://github.com/sponsors/danielsaidi
[GitHub]: https://github.com/danielsaidi/SwiftKit
[Documentation]: https://danielsaidi.github.io/SwiftKit/documentation/swiftkit/
[License]: https://github.com/danielsaidi/SwiftKit/blob/master/LICENSE
[Authentication]: Readmes/Authentication.md
[Coding]: Readmes/Coding.md
[Extensions]: Readmes/Extensions.md
[IoC]: Readmes/IoC.md

View File

@ -1,11 +0,0 @@
# Authentication
`SwiftKit` contains services that lets you perform user authentication in varios ways.
* `AuthenticationService` is a protocol that can be implemented by services that can authenticate the current user.
* `BiometricAuthenticationService` performs user authentication using either `FaceID` or `TouchID`.
* `CachedAuthenticationService` is a protocol that can be implemented by authentication services that can cache the authentication result.
* `CachedAuthenticationServiceProxy` wraps any other authentication service and caches the latest successful authentication result.
* `...and more` - this namespace contains more authentication tools as well.
You can create your own authentication services by impementing the prototocols in this namespace, and extend your implementations with the existing proxy implementation.

View File

@ -1,9 +0,0 @@
# Coding
`SwiftKit` contains coders that lets you encode and decode data. In most cases, you probably want to use `Codable` but there are other cases where you may want to inject an abstract coder, e.g. when you have larger systems where coupling should be reduced.
* `StringEncoder/StringDecoder` are protocols that have a single coding function each.
* `StringCoder` implements both `StringEncoder` and `StringDecoder`.
* `Base64StringCoder` can encode strings to base64 and deode base4 strings.
You can create your own string coders by impementing the prototocols in this namespace.

View File

@ -1,9 +0,0 @@
# Device
`SwiftKit` contains device-specific tools.
* `DeviceIdentifier` is a protocol that can be implemented by classes that can uniquely identify the current device.
* `KeychainBasedDeviceIdentifier` persists device identifiation information in the keychain.
* `UserDefaultsBasedDeviceIdentifier` persists device identifiation information in user defaults.
Note that this namespace doesn't know anything about `UIDevice`, since it is `UIKit` agnostic.

View File

@ -1,5 +0,0 @@
# Extension
`SwiftKit` contains extensions to native `Swift` types, e.g. `Date`.
Since this namespace will grow much, most extensions are not listed or demonstrated here. Instead, have a look at the source code. It should be pretty well documented.

View File

@ -1,9 +0,0 @@
# IoC
Inversion of Control and Dependency Injection are important tools to help break coupling within your code base.
`SwiftKit` contains a `IoCContainer` protocol that can be implemented by classes that can resolve dependencies dynamically, based on no or several arguments.
`SwiftKit` also contains two commented out `IoCContainer` implementations: one for `Dip` and one for `Swinject`. Simply copy the commented out code to an app that adds either library as an external dependency, then uncomment the code to get a complete IoC container.
You can create your own IoC container by impementing the prototocol in this namespace.

View File

@ -1,11 +0,0 @@
# Keychain
`SwiftKit` contains tools for working with the device keychain.
* `KeychainReader` is a protocol that can be implemented by services that can read from the keychain.
* `KeychainWriter` is a protocol that can be implemented by services that can write to the keychain.
* `KeychainService` is a protocol that implements both `KeychainReader` and `KeychainWriter`.
* `KeychainWrapper` is a util class that can read from and write to the keychain.
* `StandardKeychainService` implements `KeychainService` and uses a wrapper to read from and write to the keychain.
These tools are used in other parts of the library, e.g. to persist unique device identifiers.

View File

@ -1,6 +1,278 @@
# Release notes
I will bump revision by revision, until SwiftKit has all functionality that it should have from iExtra. I will then bump it to `1.0.0`.
## 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
@ -8,16 +280,19 @@ I will bump revision by revision, until SwiftKit has all functionality that it s
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`.

Binary file not shown.

BIN
Resources/Logo_solid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -9,27 +9,32 @@
import Foundation
/**
This struct represents a unique authentication.
This struct represents a unique authentication type.
This struct currently only has an `id` but it is still used
to be able to extend the authentication information without
having to change any authentication protocols.
The struct only has an ``id``, but is still used to improve
authentication without having to change any protocols.
*/
public struct Authentication {
public init (id: String) {
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 a "standard" authentication. It can be used if you
don't have a bunch of different authentication types in
your app.
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

@ -3,30 +3,40 @@
// SwiftKit
//
// Created by Daniel Saidi on 2016-01-18.
// Copyright © 2020 Daniel Saidi. All rights reserved.
// Copyright © 2016 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by authenticating services
that can be used to authenticate the current user.
This protocol can be implemented by any classes that can be
used to authenticate the user.
*/
public protocol AuthenticationService: AnyObject {
typealias AuthCompletion = (_ result: AuthResult) -> ()
typealias AuthCompletion = (_ result: AuthResult) -> Void
typealias AuthError = AuthenticationServiceError
typealias AuthResult = Result<Void, Error>
/**
Authenticate the user for a certain authentication type.
`reason` can be used as display information to the user.
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)
func authenticateUser(
for auth: Authentication,
reason: String,
completion: @escaping AuthCompletion)
/**
Check if the service instance can authenticate the user
for a certain authentication type.
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

@ -9,10 +9,23 @@
import Foundation
/**
This enum represents possible authentication service errors.
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

@ -6,44 +6,20 @@
// 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.
as `FaceID` or `TouchID` to authenticate the user.
*/
public class BiometricAuthenticationService: AuthenticationService {
public class BiometricAuthenticationService: LocalAuthenticationService {
// MARK: - Initialization
public init() {}
// MARK: - Properties
private let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
// MARK: - Public functions
public 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) }
}
}
public func canAuthenticateUser(for auth: Authentication) -> Bool {
var error: NSError?
return LAContext().canEvaluatePolicy(policy, error: &error)
}
public 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(()))
}
/**
Create a service instance.
*/
public init() {
super.init(policy: .deviceOwnerAuthenticationWithBiometrics)
}
}
#endif

View File

@ -9,36 +9,37 @@
import Foundation
/**
This protocol can be implemented by services that can cache
an authentication result, to avoid having to perform a real
authentication operation if a successful authentication has
already been performed.
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 cached state. Call `resetUserAuthentication()`
or `resetUserAuthentication(for:)` as soon as this state is
considered to be invalid, e.g. when your app is send to the
background and a new user can open the app at a later time.
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 user has already been authenticated for an
authentication type.
Check if the service has already authenticated the user
for a certain authentication type.
*/
func isUserAuthenticated(for auth: Authentication) -> Bool
/**
Reset the user's entire authentication state.
*/
func resetUserAuthentication()
/**
Reset the user's authentication state for a single type
of authentication.
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

@ -9,8 +9,8 @@
import Foundation
/**
This service wraps another authentication service and keeps
its authentication result in a cache.
This class wraps another ``AuthenticationService`` instance
and keeps authentication results in a cache.
*/
public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
@ -31,6 +31,11 @@ public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
// 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
@ -39,21 +44,36 @@ public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
}
}
/**
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
}
public func resetUserAuthentication() {
cache.removeAll()
}
/**
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()
}
}

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

@ -1,15 +0,0 @@
//
// 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 {}

View File

@ -1,18 +0,0 @@
//
// 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 decode
strings.
*/
public protocol StringDecoder: AnyObject {
func decode(_ string: String) -> String?
}

View File

@ -1,18 +0,0 @@
//
// StringEncoder.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
strings.
*/
public protocol StringEncoder: AnyObject {
func encode(_ string: String) -> String?
}

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,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

@ -9,9 +9,8 @@
import Foundation
/**
`TODO` These extensions made more sense when you had to use
comparison results. Now, it's just semantics over syntax. I
may remove these later.
These extensions provide a semantic, more readable layer on
top of the raw comparisons.
*/
public extension 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

@ -14,6 +14,9 @@ import Foundation
*/
public protocol DeviceIdentifier: AnyObject {
/**
Get a unique device identifier.
*/
func getDeviceIdentifier() -> String
}

View File

@ -29,6 +29,13 @@ public class KeychainBasedDeviceIdentifier: DeviceIdentifier {
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()

View File

@ -28,6 +28,13 @@ public class UserDefaultsBasedDeviceIdentifier: DeviceIdentifier {
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()
@ -39,7 +46,6 @@ private extension UserDefaultsBasedDeviceIdentifier {
func generateDeviceIdentifier() -> String {
let id = UUID().uuidString
defaults.set(id, forKey: key)
defaults.synchronize()
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

@ -11,14 +11,14 @@ import Foundation
public extension Comparable {
/**
Limit the value to a range.
Limit the value to a closed range.
*/
mutating func limit(to range: ClosedRange<Self>) {
self = limited(to: range)
}
/**
Return the value limited to a range.
Return the value limited to a closed range.
This could be implemented in a oneliner, but that would
make the code less readable.

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

@ -22,6 +22,15 @@ public extension DispatchQueue {
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

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

@ -12,11 +12,17 @@ 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

@ -12,6 +12,9 @@ import Foundation
public extension String {
/**
Check whether or not the string contains another string.
*/
func contains(_ string: String, caseSensitive: Bool) -> Bool {
caseSensitive
? contains(string)

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

@ -1,16 +0,0 @@
//
// String+HasContent.swift
// SwiftKit
//
// Created by Daniel Saidi on 2020-06-05.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public extension String {
var hasContent: Bool {
count > 0
}
}

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

@ -5,6 +5,7 @@
// 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
//
@ -12,10 +13,17 @@ 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)

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

@ -11,9 +11,27 @@
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? {
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)?
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,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

@ -16,15 +16,29 @@ import CoreLocation
it simplifies extending it with new coordinates in any apps
that use custom coordinates.
*/
public struct WorldCoordinate {
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(coordinate: CLLocationCoordinate2D(latitude: 40.7590615, longitude: -73.969231)) }
static var newYork: WorldCoordinate { .init(coordinate: CLLocationCoordinate2D(latitude: 40.7033127, longitude: -73.979681)) }
static var sanFransisco: WorldCoordinate { .init(coordinate: CLLocationCoordinate2D(latitude: 37.7796828, longitude: -122.4000062)) }
static var tokyo: WorldCoordinate { .init(coordinate: CLLocationCoordinate2D(latitude: 35.673, longitude: 139.710)) }
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

@ -29,10 +29,6 @@ public protocol KeychainAttrRepresentable {
This is recommended for items that must be available to any
background applications or processes.
* `always`
The attribute can always be accessed, whether the device is
locked or not. This is not recommended for production apps.
* `ThisDeviceOnly`
The attribute will not be included in encrypted backup, and
are thus not available after restoring apps from backups on
@ -42,7 +38,7 @@ public protocol KeychainAttrRepresentable {
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 passcode protected items.
the passcode will delete all items.
* `whenUnlocked`
The attribute can only be accessed when the device has been
@ -53,8 +49,6 @@ public enum KeychainItemAccessibility {
case afterFirstUnlock
case afterFirstUnlockThisDeviceOnly
case always
case alwaysThisDeviceOnly
case whenPasscodeSetThisDeviceOnly
case whenUnlocked
case whenUnlockedThisDeviceOnly
@ -68,9 +62,7 @@ public enum KeychainItemAccessibility {
private let keychainItemAccessibilityLookup: [KeychainItemAccessibility: CFString] = [
.afterFirstUnlock: kSecAttrAccessibleAfterFirstUnlock,
.afterFirstUnlockThisDeviceOnly: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
.always: kSecAttrAccessibleAlways,
.whenPasscodeSetThisDeviceOnly: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.alwaysThisDeviceOnly: kSecAttrAccessibleAlwaysThisDeviceOnly,
.whenUnlocked: kSecAttrAccessibleWhenUnlocked,
.whenUnlockedThisDeviceOnly: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]

View File

@ -22,6 +22,5 @@ public protocol KeychainReader: AnyObject {
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 object(for key: String, with accessibility: KeychainItemAccessibility?) -> NSCoding?
func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String?
}

View File

@ -139,12 +139,12 @@ open class KeychainWrapper {
}
open func number(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> NSNumber? {
object(for: key, with: accessibility) as? NSNumber
object(for: key, with: accessibility)
}
open func object(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> NSCoding? {
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 NSKeyedUnarchiver.unarchiveObject(with: keychainData) as? NSCoding
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: keychainData)
}
open func string(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> String? {
@ -194,7 +194,7 @@ open class KeychainWrapper {
@discardableResult
open func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
let data = NSKeyedArchiver.archivedData(withRootObject: value)
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false) else { return false }
return set(data, for: key, with: accessibility)
}

View File

@ -58,10 +58,6 @@ extension StandardKeychainService {
wrapper.integer(for: key, with: accessibility)
}
public func object(for key: String, with accessibility: KeychainItemAccessibility?) -> NSCoding? {
wrapper.object(for: key, with: accessibility)
}
public func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String? {
wrapper.string(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
}
}

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