Compare commits
248 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
6f74774953 | |
![]() |
4913287915 | |
![]() |
7a9e8f3234 | |
![]() |
32a752b640 | |
![]() |
68d7bcd272 | |
![]() |
7f8c9c2db5 | |
![]() |
f61e2fdd81 | |
![]() |
86753bf3ed | |
![]() |
3d092ab2c2 | |
![]() |
66589c6ccb | |
![]() |
97c265f431 | |
![]() |
5a64443221 | |
![]() |
a6b1077441 | |
![]() |
651d94a048 | |
![]() |
30cae98daf | |
![]() |
8e0dc7e858 | |
![]() |
12ce18a036 | |
![]() |
0082707c69 | |
![]() |
d86a241982 | |
![]() |
1f9db1283e | |
![]() |
9f9d8e45f2 | |
![]() |
dbb1df6942 | |
![]() |
0b99113342 | |
![]() |
e28cf84d3c | |
![]() |
bab5074eec | |
![]() |
b0720dbe58 | |
![]() |
708a060067 | |
![]() |
12d9cc5758 | |
![]() |
d0aa08edf2 | |
![]() |
5d064644e9 | |
![]() |
80ec239c65 | |
![]() |
3be09bb381 | |
![]() |
2700b1aada | |
![]() |
298c086262 | |
![]() |
df8fbaba07 | |
![]() |
8de53563f7 | |
![]() |
b7bbe6acb3 | |
![]() |
b23977cf0e | |
![]() |
a0645690dc | |
![]() |
e2d0307932 | |
![]() |
f9d316aeb1 | |
![]() |
cc589f16f1 | |
![]() |
af91f7dc5a | |
![]() |
c3d401059a | |
![]() |
ed6ced3f9d | |
![]() |
7d9a661080 | |
![]() |
9fe09382b5 | |
![]() |
f30b5ea14b | |
![]() |
c7fe7b20e0 | |
![]() |
ee4e321e40 | |
![]() |
96b7b3de6a | |
![]() |
bdd9c0544c | |
![]() |
91b96cb9f6 | |
![]() |
a2bd714cbb | |
![]() |
9505a4ff73 | |
![]() |
05d92c0a85 | |
![]() |
53e77cddf0 | |
![]() |
214069fa72 | |
![]() |
08d7761955 | |
![]() |
3be8310ebb | |
![]() |
8616c3fc76 | |
![]() |
3cd5182ee8 | |
![]() |
b2a33fecc5 | |
![]() |
e1b0530520 | |
![]() |
c8885efa1a | |
![]() |
c046f62bcd | |
![]() |
f373cce6c7 | |
![]() |
a8b55ba005 | |
![]() |
9a1c070e3e | |
![]() |
46de9ae0d4 | |
![]() |
8bc1f18e12 | |
![]() |
7410d89f87 | |
![]() |
a453a281e9 | |
![]() |
9521f80e38 | |
![]() |
ce65d545d0 | |
![]() |
50e30153bc | |
![]() |
9df955bebc | |
![]() |
df38e174fb | |
![]() |
139578e442 | |
![]() |
a7e4653ab0 | |
![]() |
a65f0bc37c | |
![]() |
b69f53eb4e | |
![]() |
ed57117c86 | |
![]() |
efe908f21c | |
![]() |
fd381d8bb8 | |
![]() |
fb62848007 | |
![]() |
952a48f951 | |
![]() |
f25f2447a7 | |
![]() |
45d9ad185c | |
![]() |
41c3a2afc3 | |
![]() |
4969f38b19 | |
![]() |
790da03409 | |
![]() |
13d0f1b5d6 | |
![]() |
ea289e2277 | |
![]() |
dcd67d8312 | |
![]() |
b7202e1c11 | |
![]() |
caa93fc529 | |
![]() |
2fbcc30ee2 | |
![]() |
3d15b0264b | |
![]() |
1d22874ba7 | |
![]() |
5724a90ad5 | |
![]() |
2915042fa5 | |
![]() |
dc5a29a2ba | |
![]() |
c729229885 | |
![]() |
e9409cde36 | |
![]() |
f1222a771e | |
![]() |
32a8bf3442 | |
![]() |
7fcd149811 | |
![]() |
b02590d35f | |
![]() |
f1c498f5eb | |
![]() |
5aab278e14 | |
![]() |
3b5f6659b6 | |
![]() |
cb435f766b | |
![]() |
228bb96419 | |
![]() |
029048200c | |
![]() |
e97241cfce | |
![]() |
0462d1641e | |
![]() |
407a514dec | |
![]() |
f04eba2a98 | |
![]() |
7c12a3eb75 | |
![]() |
b3893beb06 | |
![]() |
efae05445e | |
![]() |
51783fafbf | |
![]() |
81aec5eabe | |
![]() |
4e17afb0e5 | |
![]() |
4032b16bc1 | |
![]() |
0f3baef0d1 | |
![]() |
13b566ed58 | |
![]() |
6b35ac7a0a | |
![]() |
7ee1f8f35e | |
![]() |
8a01717584 | |
![]() |
384e971cd9 | |
![]() |
1f00b20516 | |
![]() |
fa183f6a5f | |
![]() |
7603153b6d | |
![]() |
3d824ad307 | |
![]() |
d2ee34b984 | |
![]() |
6a08e1dd80 | |
![]() |
8a34cb5b01 | |
![]() |
5f9c6b74f2 | |
![]() |
6d140b722d | |
![]() |
501c8bcd29 | |
![]() |
4be491079f | |
![]() |
62530ac8cf | |
![]() |
22a8be3a41 | |
![]() |
55ebaea6de | |
![]() |
1493632952 | |
![]() |
7c7a14a718 | |
![]() |
bf404dbf39 | |
![]() |
9225e5429f | |
![]() |
e457ebeafb | |
![]() |
b79cda2244 | |
![]() |
e7d6232aaa | |
![]() |
f8c467e551 | |
![]() |
18d40cd26b | |
![]() |
e6893fd86c | |
![]() |
772cdd4aa7 | |
![]() |
1b5adb146c | |
![]() |
fa23c5f7ad | |
![]() |
63d3c7537e | |
![]() |
2c684072f9 | |
![]() |
e9dc84041d | |
![]() |
e7f8988857 | |
![]() |
a562a58ace | |
![]() |
df4902c6b6 | |
![]() |
ff296a0567 | |
![]() |
ac0aed1aa4 | |
![]() |
7784c7176f | |
![]() |
4a516761b8 | |
![]() |
510411b9fb | |
![]() |
6df33336a3 | |
![]() |
0560a7fa49 | |
![]() |
d3610b1e1b | |
![]() |
82e2191808 | |
![]() |
dfb1e33ad4 | |
![]() |
940c19e519 | |
![]() |
79d809647e | |
![]() |
1ca3184fd8 | |
![]() |
e56f64daa3 | |
![]() |
db828e1869 | |
![]() |
dcf76bc471 | |
![]() |
f73e266fd0 | |
![]() |
d4fd16231a | |
![]() |
583aca1047 | |
![]() |
561f919871 | |
![]() |
bd9fffe8a7 | |
![]() |
f7d52552a0 | |
![]() |
44892b2655 | |
![]() |
f9dc1d7a72 | |
![]() |
8e19d14373 | |
![]() |
145f48e986 | |
![]() |
7ba2279fb8 | |
![]() |
626e30463c | |
![]() |
8883057b55 | |
![]() |
b10c14496b | |
![]() |
230276c161 | |
![]() |
9b8f126d36 | |
![]() |
d10adc321e | |
![]() |
26bd51fd81 | |
![]() |
cffc3b0e97 | |
![]() |
942f6b6afe | |
![]() |
08f1d44d4f | |
![]() |
ec9d974dbe | |
![]() |
fe4d02359d | |
![]() |
e8d6bdaa6f | |
![]() |
dc04f69547 | |
![]() |
757d86dc64 | |
![]() |
e348b9a6b7 | |
![]() |
a427a047cc | |
![]() |
54e2c2b994 | |
![]() |
bd0338edea | |
![]() |
8251b22c42 | |
![]() |
f4e6b835dc | |
![]() |
4331e475bc | |
![]() |
cbcbdb1cc1 | |
![]() |
43cc8cf0bf | |
![]() |
60a6dffc4b | |
![]() |
ea050dbbcc | |
![]() |
1f27ead0ed | |
![]() |
62f6c46303 | |
![]() |
8bfe7a3e8b | |
![]() |
6da30adc0d | |
![]() |
d6a4b0d1f1 | |
![]() |
1e1dd6f84d | |
![]() |
224dd665a3 | |
![]() |
c05013763a | |
![]() |
af8510736d | |
![]() |
53b3413131 | |
![]() |
06e576418c | |
![]() |
c6a30a73b9 | |
![]() |
962a8f6a97 | |
![]() |
b177577bbd | |
![]() |
0698b3a8db | |
![]() |
be7cd128ad | |
![]() |
c6e8719bf6 | |
![]() |
5cbf6f02cf | |
![]() |
7da59b9d32 | |
![]() |
6fa9cd13c8 | |
![]() |
2c8f971450 | |
![]() |
58c108ec68 | |
![]() |
712279ea44 | |
![]() |
25d67a90a0 | |
![]() |
a7b352a4db | |
![]() |
68ddb5f535 | |
![]() |
e6ccc32972 | |
![]() |
45ed4f136a | |
![]() |
fe124846e5 | |
![]() |
2473cf950c |
|
@ -0,0 +1 @@
|
|||
github: danielsaidi
|
|
@ -1,4 +1,18 @@
|
|||
.build/
|
||||
# SPM defaults
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
.swiftpm/
|
||||
Demo/
|
||||
Fastlane/
|
||||
|
||||
# Documentation
|
||||
Docs
|
||||
documentation
|
||||
downloads
|
||||
videos
|
||||
|
||||
# Fastlane
|
||||
Fastlane/report.xml
|
||||
Fastlane/Preview.html
|
||||
Fastlane/screenshots
|
||||
Fastlane/test_output
|
||||
Fastlane/README.md
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
disabled_rules:
|
||||
- function_body_length
|
||||
- identifier_name
|
||||
- line_length
|
||||
- todo
|
||||
- trailing_whitespace
|
||||
- type_name
|
||||
- vertical_whitespace
|
||||
|
||||
included:
|
||||
- Sources
|
||||
- Tests
|
|
@ -0,0 +1,26 @@
|
|||
# Run `pod lib lint DSSwiftKit.podspec' to ensure this is a valid spec.
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'DSSwiftKit'
|
||||
s.version = '1.3.0'
|
||||
s.swift_versions = ['5.3']
|
||||
s.summary = 'SwiftKit contains extra functionality for Swift.'
|
||||
|
||||
s.description = <<-DESC
|
||||
SwiftKit contains extra functionality for Swift, like extensions, utils etc.
|
||||
DESC
|
||||
|
||||
s.homepage = 'https://github.com/danielsaidi/SwiftKit'
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'Daniel Saidi' => 'daniel.saidi@gmail.com' }
|
||||
s.source = { :git => 'https://github.com/danielsaidi/SwiftKit.git', :tag => s.version.to_s }
|
||||
s.social_media_url = 'https://twitter.com/danielsaidi'
|
||||
|
||||
s.swift_version = '5.6'
|
||||
s.ios.deployment_target = '13.0'
|
||||
s.macos.deployment_target = '11.0'
|
||||
s.tvos.deployment_target = '13.0'
|
||||
s.watchos.deployment_target = '6.0'
|
||||
|
||||
s.source_files = 'Sources/**/*.swift'
|
||||
end
|
|
@ -0,0 +1,106 @@
|
|||
fastlane_version "2.129.0"
|
||||
|
||||
default_platform :ios
|
||||
|
||||
platform :ios do
|
||||
|
||||
|
||||
# Documentation ==============
|
||||
|
||||
desc "Build Documentation"
|
||||
lane :documentation do
|
||||
docc_web
|
||||
end
|
||||
|
||||
|
||||
# Lint =======================
|
||||
|
||||
desc "Run SwiftLint"
|
||||
lane :lint do
|
||||
swiftlint(strict: true)
|
||||
end
|
||||
|
||||
|
||||
# Test =======================
|
||||
|
||||
desc "Run unit tests"
|
||||
lane :test do
|
||||
sh("swift test")
|
||||
end
|
||||
|
||||
|
||||
# Version ====================
|
||||
|
||||
desc "Create a new version"
|
||||
lane :version do |options|
|
||||
ensure_git_status_clean
|
||||
ensure_git_branch(branch: 'master')
|
||||
|
||||
lint
|
||||
test
|
||||
documentation
|
||||
|
||||
bump_type = options[:type]
|
||||
version = version_bump_podspec(
|
||||
path: "DSSwiftKit.podspec",
|
||||
bump_type: bump_type)
|
||||
|
||||
git_commit(path: "*", message: "Bump to #{version}")
|
||||
add_git_tag(tag: version)
|
||||
push_git_tags()
|
||||
push_to_git_remote()
|
||||
pod_push()
|
||||
end
|
||||
|
||||
|
||||
# Docs =======================
|
||||
|
||||
desc "Build documentation for all platforms"
|
||||
lane :docc do
|
||||
sh('cd .. && rm -rf Docs')
|
||||
docc_platform(destination: 'iOS', name: 'ios')
|
||||
docc_platform(destination: 'OS X', name: 'osx')
|
||||
docc_platform(destination: 'tvOS', name: 'tvos')
|
||||
docc_platform(destination: 'watchOS', name: 'watchos')
|
||||
end
|
||||
|
||||
desc "Build documentation for a single platform"
|
||||
lane :docc_platform do |values|
|
||||
sh('cd .. && mkdir -p Docs')
|
||||
docc_delete_derived_data
|
||||
sh('cd .. && xcodebuild docbuild \
|
||||
-scheme SwiftKit \
|
||||
-destination \'generic/platform=' + values[:destination] + '\'')
|
||||
sh('cd .. && \
|
||||
find ~/Library/Developer/Xcode/DerivedData \
|
||||
-name "SwiftKit.doccarchive" \
|
||||
-exec cp -R {} Docs \;')
|
||||
sh('cd .. && \
|
||||
mv Docs/SwiftKit.doccarchive Docs/SwiftKit_' + values[:name] + '.doccarchive')
|
||||
end
|
||||
|
||||
desc "Delete documentation derived data (may be historic duplicates)"
|
||||
lane :docc_delete_derived_data do
|
||||
sh('find ~/Library/Developer/Xcode/DerivedData \
|
||||
-name "SwiftKit.doccarchive" \
|
||||
-exec rm -Rf {} \; || true')
|
||||
end
|
||||
|
||||
desc "Build static documentation websites for all platforms"
|
||||
lane :docc_web do
|
||||
docc
|
||||
docc_web_platform(name: 'ios')
|
||||
docc_web_platform(name: 'osx')
|
||||
docc_web_platform(name: 'tvos')
|
||||
docc_web_platform(name: 'watchos')
|
||||
end
|
||||
|
||||
desc "Build static documentation website for a single platform"
|
||||
lane :docc_web_platform do |values|
|
||||
sh('cd .. && $(xcrun --find docc) process-archive \
|
||||
transform-for-static-hosting Docs/SwiftKit_' + values[:name] + '.doccarchive \
|
||||
--output-path Docs/web_' + values[:name] + ' \
|
||||
--hosting-base-path SwiftKit')
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Daniel Saidi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "cwlcatchexception",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
|
||||
"state" : {
|
||||
"revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea",
|
||||
"version" : "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cwlpreconditiontesting",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
|
||||
"state" : {
|
||||
"revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "mockingkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/danielsaidi/MockingKit.git",
|
||||
"state" : {
|
||||
"revision" : "3e51adb1a3922cdccbe84a3088b7fa4d67ae236d",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nimble",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/danielsaidi/Nimble.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "f76b83c051fb3e6c120a33ebac200efba883065a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "quick",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/danielsaidi/Quick.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "1efe9551db0ad6a6e979f33366969750123d14d9"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// swift-tools-version:5.6
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftKit",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
.macOS(.v11),
|
||||
.tvOS(.v13),
|
||||
.watchOS(.v6)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "SwiftKit",
|
||||
targets: ["SwiftKit"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/danielsaidi/Quick.git", branch: "main"), // .upToNextMajor(from: "4.0.0")),
|
||||
.package(url: "https://github.com/danielsaidi/Nimble.git", branch: "main"), // .upToNextMajor(from: "9.0.0")),
|
||||
.package(url: "https://github.com/danielsaidi/MockingKit.git", .upToNextMajor(from: "1.1.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftKit",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "SwiftKitTests",
|
||||
dependencies: ["SwiftKit", "Quick", "Nimble", "MockingKit"]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,79 @@
|
|||
<p align="center">
|
||||
<img src ="Resources/Logo.png" alt="SwiftKit Logo" title="SwiftKit" width=600 />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/github/v/release/danielsaidi/SwiftKit?color=%2300550&sort=semver" alt="Version" />
|
||||
<img src="https://img.shields.io/badge/Swift-5.6-orange.svg" alt="Swift 5.6" />
|
||||
<img src="https://img.shields.io/github/license/danielsaidi/SwiftKit" alt="MIT License" />
|
||||
<a href="https://twitter.com/danielsaidi">
|
||||
<img src="https://img.shields.io/twitter/url?label=Twitter&style=social&url=https%3A%2F%2Ftwitter.com%2Fdanielsaidi" alt="Twitter: @danielsaidi" title="Twitter: @danielsaidi" />
|
||||
</a>
|
||||
<a href="https://mastodon.social/@danielsaidi">
|
||||
<img src="https://img.shields.io/mastodon/follow/000253346?label=mastodon&style=social" alt="Mastodon: @danielsaidi@mastodon.social" title="Mastodon: @danielsaidi@mastodon.social" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## About SwiftKit
|
||||
|
||||
SwiftKit adds extra functionality to the Swift programming language, like extensions to already existing types as well as completely new stuff.
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
SwiftKit can be installed with the Swift Package Manager:
|
||||
|
||||
```
|
||||
https://github.com/danielsaidi/SwiftKit.git
|
||||
```
|
||||
|
||||
If you prefer to not have external dependencies, you can also just copy the source code into your app.
|
||||
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
The [online documentation][Documentation] has more information, code examples, etc., and makes it easy to overview the various parts of the library.
|
||||
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
I manage my various open-source projects in my free time and am really thankful for any help I can get from the community.
|
||||
|
||||
You can sponsor this project on [GitHub Sponsors][Sponsors] or get in touch for paid support.
|
||||
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
Feel free to reach out if you have questions or if you want to contribute in any way:
|
||||
|
||||
* Website: [danielsaidi.com][Website]
|
||||
* Mastodon: [@danielsaidi@mastodon.social][Mastodon]
|
||||
* Twitter: [@danielsaidi][Twitter]
|
||||
* E-mail: [daniel.saidi@gmail.com][Email]
|
||||
|
||||
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
SwiftKit supports `iOS 13`, `macOS 11`, `tvOS 13` and `watchOS 6`.
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
SwiftKit is available under the MIT license. See the [LICENSE][License] file for more info.
|
||||
|
||||
|
||||
[Email]: mailto:daniel.saidi@gmail.com
|
||||
[Website]: https://www.danielsaidi.com
|
||||
[Twitter]: https://www.twitter.com/danielsaidi
|
||||
[Mastodon]: https://mastodon.social/@danielsaidi
|
||||
[Sponsors]: https://github.com/sponsors/danielsaidi
|
||||
|
||||
[Documentation]: https://danielsaidi.github.io/SwiftKit/documentation/swiftkit/
|
||||
[License]: https://github.com/danielsaidi/SwiftKit/blob/master/LICENSE
|
|
@ -0,0 +1,306 @@
|
|||
# Release notes
|
||||
|
||||
|
||||
## 1.4
|
||||
|
||||
This version adjusts the library for Xcode 14 and deprecates some things.
|
||||
|
||||
This version contains a few breaking changes, that should be easy to fix.
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `DateFormatter+Init` has a new convenience initializer.
|
||||
* `CsvParser` can now parse CSV files at urls as well.
|
||||
* `CsvParserError` has a new convenience initializer.
|
||||
|
||||
### 💡 Behavior changes
|
||||
|
||||
* `StandardCsvParser` now throws native errors for file parsing.
|
||||
|
||||
### 🗑 Deprecations
|
||||
|
||||
* The `Network` namespace has been deprecated and moved to `ApiKit`.
|
||||
|
||||
### 💥 Breaking changes
|
||||
|
||||
* `String+Slugified` has been removed due to conflicts with TagKit.
|
||||
|
||||
|
||||
|
||||
## 1.3
|
||||
|
||||
This version adjusts the library for Xcode 14 and deprecates some things.
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `Collection+Async` is now available for all OS versions that are supported by the library.
|
||||
|
||||
### 💡 Behavior changes
|
||||
|
||||
* The library no longer uses the DocC package plugin.
|
||||
* `DispatchQueue+Throttle` now uses `Double.random(in:)` instead of `arc4random()`.
|
||||
|
||||
### 🗑 Deprecations
|
||||
|
||||
* The `IoC` namespace has been deprecated and will be removed in the next major version.
|
||||
* The `Messaging` namespace has been deprecated and will be removed in the next major version.
|
||||
* The `StoreKit` namespace has been deprecated and moved to https://github.com/danielsaidi/StoreKitPlus
|
||||
* `String+Slugified` has been deprecated and moved to https://github.com/danielsaidi/TagKit
|
||||
|
||||
### 💥 Breaking changes
|
||||
|
||||
* Due to the concurrency adjustments, macOS 11 is now needed.
|
||||
|
||||
|
||||
|
||||
## 1.2
|
||||
|
||||
This version bumps the iOS deployment target to 13.0 and adds new auth utils.
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `LAContext+Async` adds an async policy evaluation function.
|
||||
* `LocalAuthenticationService` is a new service that lets you use any local authentication policy.
|
||||
|
||||
### 💡 Behavior changes
|
||||
|
||||
* `BiometricAuthenticationService` now inherits and specializes `LocalAuthenticationService`.
|
||||
|
||||
|
||||
|
||||
## 1.1
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `FileManager+UniqueFileName` contains functionality for generating a unique file name.
|
||||
* `String+Capitalize` contains functionality for capitalizing the first char in a String.
|
||||
* `String+Characters` contains single-char characters like `newLine` and `tab`.
|
||||
* `String+Paragraph` contains functionality for finding paragraphs in the text.
|
||||
* `String+Subscript` contains functionality for accessing chars in a String.
|
||||
|
||||
### 💡 Behavior changes
|
||||
|
||||
* `String+UrlEncode` now handles + as well.
|
||||
|
||||
|
||||
|
||||
## 1.0
|
||||
|
||||
I think it's finally time to push the major release button.
|
||||
|
||||
This version drastically improves documentation and ships with a DocC documentation archive.
|
||||
|
||||
This version also introduces a new `StoreKit` namespace with handy utils for managing StoreKit products and purchases.
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `Bundle` has a new `displayName` extension.
|
||||
* `Collection` has new `asyncCompactMap` and `asyncMap` extensions.
|
||||
* `Date` has a new `components` extension for retrieving year, month, hour etc.
|
||||
* `NSAttributedString` has a new `init(keyedArchiveData:)` that can initialize an attributed string from `NSKeyedArchiver` generated data.
|
||||
* `NSAttributedString` has a new `init(plainText:)` that can initialize an attributed string from plain .utf8 text data.
|
||||
* `NSAttributedString` has a new `init(rtfData:)` that can initialize an attributed string from RTF data.
|
||||
* `NSAttributedString` has a new `getKeyedArchiveData()` function that can be used to generate RTF formatted data from an attributed string.
|
||||
* `NSAttributedString` has a new `getPlainTextData()` function that can be used to generate plain .utf8 formatted text data from an attributed string.
|
||||
* `NSAttributedString` has a new `getRtfData()` function that can be used to generate RTF formatted data from an attributed string.
|
||||
* `String` has new `boolValue` extension.
|
||||
|
||||
* `StoreService` is a new protocol for managing StoreKit products and purchases.
|
||||
* `StoreContext` is a new class for managing StoreKit products and purchases.
|
||||
* `StandardStoreService` is a new class that implements the `StoreService` protocol.
|
||||
|
||||
|
||||
|
||||
## 0.7.0
|
||||
|
||||
This version requires Xcode 13 and later, since it refers to the latest api:s.
|
||||
|
||||
This version also cleans up the code and makes changes to conform to the latest standards.
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `Calendar+Date` has new `same` functions to provide the comparison date.
|
||||
* `DispatchQueue+Throttle` has new `throttle` and `debounce` functions.
|
||||
* `String+Split` has a new `split(by:)` components splitting function.
|
||||
* `Url+Global` has a new `userSubscriptions` url.
|
||||
|
||||
### 💥 Breaking changes
|
||||
|
||||
* All previously deprecated features have been removed.
|
||||
* `ApiService` moves the `type` param before the `httpMethod`, since `httpMethod` now has a default value.
|
||||
* `ApiRoute` and `ApiService` now use enum-based HTTP methods instead of string-based ones.
|
||||
* `DispatchQueue+Async` now requires that you explicitly define `seconds` when using that `asyncAfter` function.
|
||||
* `URL+Global` `appStoreUrl(forAppId:)` now returns an optional url.
|
||||
|
||||
|
||||
|
||||
## 0.6.1 - 0.6.2
|
||||
|
||||
These versions remove explicit url encoding of `ApiRoute` query params and always url encode form data params.
|
||||
|
||||
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `ApiRoute` has more explicit properties for working with post data.
|
||||
* `ApiRoute` has a new `shouldUrlEncodeParams` parameter.
|
||||
* `iCloudDocumentSync` is a new protocol for syncing iCloud document changes.
|
||||
* `String+Slugify` is a new extension that can convert a string to a slugified version.
|
||||
* `StandardiCloudDocumentSync` is a new class for syncing iCloud document changes.
|
||||
* `URL+iCloud` contains iCloud-specific URLs and fallback URLs.
|
||||
|
||||
### 💡 Behavior changes
|
||||
|
||||
* `ApiRoute` has more required properties.
|
||||
* `URL+setQueryParameter` no longer url encodes the strings you send in.
|
||||
|
||||
### 💥 Breaking changes
|
||||
|
||||
* `ApiRoute` requires new post properties to be defined.
|
||||
|
||||
|
||||
|
||||
## 0.5.0
|
||||
|
||||
This version adjusts code that made the demo not being able to use the SPM package instead of the source files.
|
||||
|
||||
### ✨ New features
|
||||
|
||||
* `ApiError` is a new enum that replaces the old `ApiServiceError`.
|
||||
* `FileExporter` is a new protocol for exporting data to the file system.
|
||||
* `MimeType` is a new enum for simplifying working with mime types.
|
||||
* `MFMailComposeViewController` has a new `addAttachmentData` that uses the new `MimeType`.
|
||||
* `StandardFileExporter` is a new `FileExporter` implementation.
|
||||
|
||||
### 🗑 Deprecations
|
||||
|
||||
* `ApiServiceError` has been deprecated in favor of `ApiError`.
|
||||
|
||||
### 💥 Breaking changes
|
||||
|
||||
* `ApiError` is a lot easier than before, with many cases gone and more info in the remaining ones.
|
||||
|
||||
|
||||
|
||||
## 0.4.4
|
||||
|
||||
This version adds a new `HttpMethod` enum that can be used with the network components.
|
||||
|
||||
|
||||
|
||||
## 0.4.3
|
||||
|
||||
This version adds new `ApiRoute` request functions and adjusts the url of form data requests.
|
||||
|
||||
|
||||
|
||||
## 0.4.2
|
||||
|
||||
This version adds missing initializers to map services.
|
||||
|
||||
|
||||
|
||||
## 0.4.1
|
||||
|
||||
This version updates dependencies, adjusts project setup, tweak icons and display names etc.
|
||||
|
||||
|
||||
|
||||
## 0.4.0
|
||||
|
||||
This version adds a new `String+Dictation` extension to cleanup dictation objects and spaces from a string.
|
||||
|
||||
|
||||
|
||||
## 0.3.3
|
||||
|
||||
This version adds a new `ExternalMapService` protocol as well as an Apple and a Google implementation.
|
||||
|
||||
This version also adds a new `Network` namespace, with api-specific protocols and errors for communicating with external REST apis.
|
||||
|
||||
|
||||
|
||||
## 0.3.2
|
||||
|
||||
This version adds a `UserDefaults+Codable` extension for persisting codable types in `UserDefaults`.
|
||||
|
||||
|
||||
|
||||
## 0.3.1
|
||||
|
||||
This version makes the standard cvs parser use paths instead of urls when parsing files.
|
||||
|
||||
|
||||
|
||||
## 0.3.0
|
||||
|
||||
This version adds improved support for watchOS and tvOS.
|
||||
|
||||
The bump version process has been improved to also add linting and a unit test confirmation.
|
||||
|
||||
|
||||
|
||||
## 0.2.0
|
||||
|
||||
This version adds:
|
||||
|
||||
* new `Localization` utilities, like `Translator`s and `LocalizationService`s.
|
||||
* new `FileDirectoryService` utilities.
|
||||
|
||||
This version also adds macOS support.
|
||||
|
||||
|
||||
|
||||
## 0.1.0
|
||||
|
||||
This version adds:
|
||||
|
||||
* a new `Filter` type that simplifies filtering object collections.
|
||||
* new `Date` [extensions][Extensions].
|
||||
* new `Numeric` conversion [extensions][Extensions].
|
||||
|
||||
|
||||
|
||||
## 0.0.6
|
||||
|
||||
This version adds a bunch of [extensions][Extensions] and common utils and updates external test dependencies to the latest versions.
|
||||
|
||||
|
||||
|
||||
## 0.0.5
|
||||
|
||||
This version adds a bunch of [extensions][Extensions] and common types.
|
||||
|
||||
|
||||
|
||||
## 0.0.4
|
||||
|
||||
This version adds [device][Device] and [keychain][Keychain] utils.
|
||||
|
||||
|
||||
|
||||
## 0.0.3
|
||||
|
||||
This version adds a bunch of convenient [extensions][Extensions].
|
||||
|
||||
|
||||
|
||||
## 0.0.2
|
||||
|
||||
This version adjusts [authentication][Authentication] service signatures, as well as [coding][Coding] and [IoC][IoC] functionality
|
||||
|
||||
|
||||
|
||||
## 0.0.1
|
||||
|
||||
This version adds [authentication][Authentication] functionality to `SwiftKit`.
|
||||
|
||||
|
||||
[Authentication]: Readmes/Authentication.md
|
||||
[Coding]: Readmes/Coding.md
|
||||
[Device]: Readmes/Device.md
|
||||
[Extensions]: Readmes/Extensions.md
|
||||
[IoC]: Readmes/IoC.md
|
||||
[Keychain]: Readmes/Keychain.md
|
Before Width: | Height: | Size: 380 KiB After Width: | Height: | Size: 380 KiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 376 KiB |
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Authentication.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2020-04-28.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This struct represents a unique authentication type.
|
||||
|
||||
The struct only has an ``id``, but is still used to improve
|
||||
authentication without having to change any protocols.
|
||||
*/
|
||||
public struct Authentication: Identifiable, Equatable {
|
||||
|
||||
/**
|
||||
Create a new authentication type.
|
||||
|
||||
- Parameters:
|
||||
- id: The ID of the authentication.
|
||||
*/
|
||||
public init(id: String) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
/// The ID of the authentication.
|
||||
public var id: String
|
||||
}
|
||||
|
||||
public extension Authentication {
|
||||
|
||||
/**
|
||||
This standard authentication type can be used if you do
|
||||
not have many different authentications in your app.
|
||||
*/
|
||||
static var standard: Authentication {
|
||||
Authentication(id: "com.swiftkit.auth.any")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// AuthenticationService.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-01-18.
|
||||
// Copyright © 2016 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by any classes that can be
|
||||
used to authenticate the user.
|
||||
*/
|
||||
public protocol AuthenticationService: AnyObject {
|
||||
|
||||
typealias AuthCompletion = (_ result: AuthResult) -> Void
|
||||
typealias AuthError = AuthenticationServiceError
|
||||
typealias AuthResult = Result<Void, Error>
|
||||
|
||||
/**
|
||||
Authenticate the user for a certain ``Authentication``.
|
||||
|
||||
- Parameters:
|
||||
- auth: The authentication type to evaluate.
|
||||
- reason: The localized reason to show to the user.
|
||||
- completion: The completion block to call once authentication is done.
|
||||
*/
|
||||
func authenticateUser(
|
||||
for auth: Authentication,
|
||||
reason: String,
|
||||
completion: @escaping AuthCompletion)
|
||||
|
||||
/**
|
||||
Whether or not the service can authenticate the current
|
||||
user for a certain ``Authentication`` type.
|
||||
|
||||
- Parameters:
|
||||
- auth: The authentication type to evaluate.
|
||||
*/
|
||||
func canAuthenticateUser(for auth: Authentication) -> Bool
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// AuthenticationServiceError.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2020-04-28.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This enum represents various authentication errors that can
|
||||
occur while a user is being authenticated.
|
||||
*/
|
||||
public enum AuthenticationServiceError: Error, Equatable {
|
||||
|
||||
/**
|
||||
The authentication failed.
|
||||
*/
|
||||
case authenticationFailed
|
||||
|
||||
/**
|
||||
The authentication failed with a certain error message.
|
||||
*/
|
||||
case authenticationFailedWithErrorMessage(String)
|
||||
|
||||
/**
|
||||
The requested authentication type is not supported.
|
||||
*/
|
||||
case unsupportedAuthentication
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// BiometricAuthenticationService.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-01-18.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
import LocalAuthentication
|
||||
|
||||
/**
|
||||
This authentication service uses `LocalAuthentication` such
|
||||
as `FaceID` or `TouchID` to authenticate the user.
|
||||
*/
|
||||
public class BiometricAuthenticationService: LocalAuthenticationService {
|
||||
|
||||
/**
|
||||
Create a service instance.
|
||||
*/
|
||||
public init() {
|
||||
super.init(policy: .deviceOwnerAuthenticationWithBiometrics)
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// CachedAuthenticationService.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-01-18.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by any classes that can be
|
||||
used to authenticate the user and cache the result to avoid
|
||||
having to perform a real authentication if a successful one
|
||||
has already been performed.
|
||||
|
||||
For instance, you can reduce the number of times users have
|
||||
to perform biometric authentication to access critical data.
|
||||
|
||||
Note that you can't rely on a cached authentication service
|
||||
to clear its state. Call the ``resetUserAuthentications()``
|
||||
or ``resetUserAuthentication(for:)`` function as soon as an
|
||||
authenticated session becomes invalid, e.g. when the app is
|
||||
sent to the background or new users log in.
|
||||
*/
|
||||
public protocol CachedAuthenticationService: AuthenticationService {
|
||||
|
||||
/**
|
||||
Check if the service has already authenticated the user
|
||||
for a certain authentication type.
|
||||
*/
|
||||
func isUserAuthenticated(for auth: Authentication) -> Bool
|
||||
|
||||
/**
|
||||
Reset the service's cached authentication state for the
|
||||
provided authentication type.
|
||||
*/
|
||||
func resetUserAuthentication(for auth: Authentication)
|
||||
|
||||
/**
|
||||
Reset the service's cached authentication state for all
|
||||
authentication types.
|
||||
*/
|
||||
func resetUserAuthentications()
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
//
|
||||
// CachedAuthenticationService.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-01-18.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This class wraps another ``AuthenticationService`` instance
|
||||
and keeps authentication results in a cache.
|
||||
*/
|
||||
public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(baseService: AuthenticationService) {
|
||||
self.baseService = baseService
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let baseService: AuthenticationService
|
||||
|
||||
private var cache = [String: Bool]()
|
||||
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
/**
|
||||
Authenticate the user for a certain authentication type.
|
||||
|
||||
`reason` can be used to display information to the user.
|
||||
*/
|
||||
public func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
|
||||
if isUserAuthenticated(for: auth) { return completion(.success(())) }
|
||||
baseService.authenticateUser(for: auth, reason: reason) { result in
|
||||
self.handle(result, for: auth)
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the service instance can authenticate the user.
|
||||
*/
|
||||
public func canAuthenticateUser(for auth: Authentication) -> Bool {
|
||||
baseService.canAuthenticateUser(for: auth)
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the service has already authenticated the user
|
||||
for a certain authentication type.
|
||||
*/
|
||||
public func isUserAuthenticated(for auth: Authentication) -> Bool {
|
||||
cache[auth.id] ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
Reset the service's cached authentication state for the
|
||||
provided authentication type.
|
||||
*/
|
||||
public func resetUserAuthentication(for auth: Authentication) {
|
||||
setIsAuthenticated(false, for: auth)
|
||||
}
|
||||
|
||||
/**
|
||||
Reset the service's cached authentication state for all
|
||||
authentication types.
|
||||
*/
|
||||
public func resetUserAuthentications() {
|
||||
cache.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private Functions
|
||||
|
||||
private extension CachedAuthenticationServiceProxy {
|
||||
|
||||
func handle(_ result: AuthResult, for auth: Authentication) {
|
||||
switch result {
|
||||
case .failure: setIsAuthenticated(false, for: auth)
|
||||
case .success: setIsAuthenticated(true, for: auth)
|
||||
}
|
||||
}
|
||||
|
||||
func setIsAuthenticated(_ isAuthenticated: Bool, for auth: Authentication) {
|
||||
cache[auth.id] = isAuthenticated
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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]]
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// Base64StringEncoder.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-03-21.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This coder can encode and decode strings to and from base64.
|
||||
*/
|
||||
public class Base64StringCoder: StringCoder {
|
||||
|
||||
public init() {}
|
||||
|
||||
/**
|
||||
Decode a base64 encoded string.
|
||||
*/
|
||||
public func decode(_ string: String) -> String? {
|
||||
guard let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
/**
|
||||
Encode a string to base64.
|
||||
*/
|
||||
public func encode(_ string: String) -> String? {
|
||||
let data = string.data(using: .utf8)
|
||||
let encoded = data?.base64EncodedData(options: .endLineWithLineFeed)
|
||||
guard let encodedData = encoded else { return nil }
|
||||
return String(data: encodedData, encoding: .utf8)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// Date+Adding.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-05-15.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Date {
|
||||
|
||||
/**
|
||||
Add a certain number days days to the date.
|
||||
*/
|
||||
func adding(days: Double) -> Date {
|
||||
let seconds = Double(days) * 60 * 60 * 24
|
||||
return addingTimeInterval(seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
Add a certain number hours days to the date.
|
||||
*/
|
||||
func adding(hours: Double) -> Date {
|
||||
let seconds = Double(hours) * 60 * 60
|
||||
return addingTimeInterval(seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
Add a certain number minutes days to the date.
|
||||
*/
|
||||
func adding(minutes: Double) -> Date {
|
||||
let seconds = Double(minutes) * 60
|
||||
return addingTimeInterval(seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
Add a certain number seconds days to the date.
|
||||
*/
|
||||
func adding(seconds: Double) -> Date {
|
||||
addingTimeInterval(Double(seconds))
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a certain number of days to the date.
|
||||
*/
|
||||
func removing(days: Double) -> Date {
|
||||
adding(days: -days)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a certain number of hours to the date.
|
||||
*/
|
||||
func removing(hours: Double) -> Date {
|
||||
adding(hours: -hours)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a certain number of minutes to the date.
|
||||
*/
|
||||
func removing(minutes: Double) -> Date {
|
||||
adding(minutes: -minutes)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a certain number of seconds to the date.
|
||||
*/
|
||||
func removing(seconds: Double) -> Date {
|
||||
adding(seconds: -seconds)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Date+Compare.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-05-15.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
These extensions provide a semantic, more readable layer on
|
||||
top of the raw comparisons.
|
||||
*/
|
||||
public extension Date {
|
||||
|
||||
/**
|
||||
Whether or not the date occurs after the provided date.
|
||||
*/
|
||||
func isAfter(_ date: Date) -> Bool {
|
||||
self > date
|
||||
}
|
||||
|
||||
/**
|
||||
Whether or not the date occurs before the provided date.
|
||||
*/
|
||||
func isBefore(_ date: Date) -> Bool {
|
||||
self < date
|
||||
}
|
||||
|
||||
/**
|
||||
Whether or not the date is the same as the provided date.
|
||||
*/
|
||||
func isSame(as date: Date) -> Bool {
|
||||
self == date
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// DeviceIdentifier.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by anything that can get a
|
||||
unique device identifier for the current device.
|
||||
*/
|
||||
public protocol DeviceIdentifier: AnyObject {
|
||||
|
||||
/**
|
||||
Get a unique device identifier.
|
||||
*/
|
||||
func getDeviceIdentifier() -> String
|
||||
}
|
||||
|
||||
extension DeviceIdentifier {
|
||||
|
||||
var key: String { "com.swiftkit.deviceidentifier" }
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// KeychainBasedDeviceIdentifier.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This device identifier generates a unique device identifier
|
||||
and stores it in keychain, to make it possible to reuse the
|
||||
identifier, even if the app is uninstalled.
|
||||
|
||||
The user default fallback maximizes the chance that the app
|
||||
can retrieve the identifier even if the keychain can not be
|
||||
read at the time of retrieval.
|
||||
*/
|
||||
public class KeychainBasedDeviceIdentifier: DeviceIdentifier {
|
||||
|
||||
public init(
|
||||
keychainService: KeychainService,
|
||||
backupIdentifier: DeviceIdentifier = UserDefaultsBasedDeviceIdentifier()) {
|
||||
self.keychainService = keychainService
|
||||
self.backupIdentifier = backupIdentifier
|
||||
}
|
||||
|
||||
private let backupIdentifier: DeviceIdentifier
|
||||
private let keychainService: KeychainService
|
||||
|
||||
/**
|
||||
Get a unique device identifier from the device keychain.
|
||||
|
||||
If no identifier exists in the keychain, the identifier
|
||||
will use the provided `backupIdentifier` to generate an
|
||||
identifier, then persist that id in the device keychain.
|
||||
*/
|
||||
public func getDeviceIdentifier() -> String {
|
||||
if let id = keychainService.string(for: key, with: nil) { return id }
|
||||
let id = backupIdentifier.getDeviceIdentifier()
|
||||
keychainService.set(id, for: key, with: nil)
|
||||
return id
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// UserDefaultsBasedDeviceIdentifier.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This device identifier generates a unique device identifier
|
||||
and stores it in user defaults, so that the same identifier
|
||||
is used every time for each app installation.
|
||||
|
||||
If you want to use the same identifier between app installs,
|
||||
|
||||
|
||||
The user default fallback maximizes the chance that the app
|
||||
can retrieve the identifier even if the keychain can not be
|
||||
read at the time of retrieval.
|
||||
*/
|
||||
public class UserDefaultsBasedDeviceIdentifier: DeviceIdentifier {
|
||||
|
||||
public init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
|
||||
/**
|
||||
Get a unique device identifier from the user defaults.
|
||||
|
||||
If no persisted identifier exists, this identifier will
|
||||
generate a new identifier, then persist and return that
|
||||
identifier.
|
||||
*/
|
||||
public func getDeviceIdentifier() -> String {
|
||||
if let id = defaults.string(forKey: key) { return id }
|
||||
return generateDeviceIdentifier()
|
||||
}
|
||||
}
|
||||
|
||||
private extension UserDefaultsBasedDeviceIdentifier {
|
||||
|
||||
func generateDeviceIdentifier() -> String {
|
||||
let id = UUID().uuidString
|
||||
defaults.set(id, forKey: key)
|
||||
return id
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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] }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Comparable+Limit.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2018-10-04.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Comparable {
|
||||
|
||||
/**
|
||||
Limit the value to a closed range.
|
||||
*/
|
||||
mutating func limit(to range: ClosedRange<Self>) {
|
||||
self = limited(to: range)
|
||||
}
|
||||
|
||||
/**
|
||||
Return the value limited to a closed range.
|
||||
|
||||
This could be implemented in a oneliner, but that would
|
||||
make the code less readable.
|
||||
*/
|
||||
func limited(to range: ClosedRange<Self>) -> Self {
|
||||
if self < range.lowerBound { return range.lowerBound }
|
||||
if self > range.upperBound { return range.upperBound }
|
||||
return self
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// DispatchQueue+Async.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2020-06-02.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// https://danielsaidi.com/blog/2020/06/03/dispatch-queue
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension DispatchQueue {
|
||||
|
||||
/**
|
||||
Perform an operation after a time interval.
|
||||
*/
|
||||
func asyncAfter(
|
||||
_ interval: DispatchTimeInterval,
|
||||
execute: @escaping () -> Void) {
|
||||
asyncAfter(
|
||||
deadline: .now() + interval,
|
||||
execute: execute)
|
||||
}
|
||||
/**
|
||||
Perform an operation after a time interval.
|
||||
*/
|
||||
func asyncAfter(
|
||||
seconds: TimeInterval,
|
||||
execute: @escaping () -> Void) {
|
||||
let milli = Int(seconds * 1000)
|
||||
asyncAfter(.milliseconds(milli), execute: execute)
|
||||
}
|
||||
|
||||
/**
|
||||
Perform an async operation then call a completion block
|
||||
on another queue (default `.main`) with the result from
|
||||
the async operation being passed on.
|
||||
*/
|
||||
func async<T>(
|
||||
execute: @escaping () -> T,
|
||||
then completion: @escaping (T) -> Void,
|
||||
on completionQueue: DispatchQueue = .main) {
|
||||
async {
|
||||
let result = execute()
|
||||
completionQueue.async {
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// Result+Utils.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2020-04-28.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// https://danielsaidi.com/blog/2020/06/03/result-utils
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Result {
|
||||
|
||||
/**
|
||||
Get the failure error, if any.
|
||||
*/
|
||||
var failureError: Failure? {
|
||||
switch self {
|
||||
case .failure(let error): return error
|
||||
case .success: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Check whether or not the result is a failure result.
|
||||
*/
|
||||
var isFailure: Bool { !isSuccess }
|
||||
|
||||
/**
|
||||
Check whether or not the result is a success result.
|
||||
*/
|
||||
var isSuccess: Bool {
|
||||
switch self {
|
||||
case .failure: return false
|
||||
case .success: return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Get the success result, if any.
|
||||
*/
|
||||
var successResult: Success? {
|
||||
switch self {
|
||||
case .failure: return nil
|
||||
case .success(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// String+Base64.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-12-12.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// https://danielsaidi.com/blog/2020/06/04/string-base64
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
/**
|
||||
Base64 decode the string.
|
||||
*/
|
||||
func base64Decoded() -> String? {
|
||||
guard let data = Data(base64Encoded: self) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
/**
|
||||
Base64 encode the string.
|
||||
*/
|
||||
func base64Encoded() -> String? {
|
||||
data(using: .utf8)?.base64EncodedString()
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// String+Contains.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-02-17.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// https://danielsaidi.com/blog/2020/06/04/string-contains
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
/**
|
||||
Check whether or not the string contains another string.
|
||||
*/
|
||||
func contains(_ string: String, caseSensitive: Bool) -> Bool {
|
||||
caseSensitive
|
||||
? contains(string)
|
||||
: range(of: string, options: .caseInsensitive) != nil
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// String+Replace.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-01-08.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// Read more here:
|
||||
// https://danielsaidi.com/blog/2020/06/04/string-replace
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
/**
|
||||
This is a shortcut to `replacingOccurrences(of:with:)`.
|
||||
*/
|
||||
func replacing(_ string: String, with: String) -> String {
|
||||
replacingOccurrences(of: string, with: with)
|
||||
}
|
||||
|
||||
/**
|
||||
This is a shortcut to `replacingOccurrences(of:with:)`,
|
||||
with a `caseInsensitive` option enabled.
|
||||
*/
|
||||
func replacing(_ string: String, with: String, caseSensitive: Bool) -> String {
|
||||
caseSensitive
|
||||
? replacing(string, with: with)
|
||||
: replacingOccurrences(of: string, with: with, options: .caseInsensitive)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// String+UrlEncode.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-12-12.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// https://danielsaidi.com/blog/2020/06/04/string-urlencode
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
/**
|
||||
Encode the string to work with `x-www-form-urlencoded`.
|
||||
|
||||
This will first call `urlEncoded()`, then replace every
|
||||
`+` with `%2B`.
|
||||
*/
|
||||
func formEncoded() -> String? {
|
||||
self.urlEncoded()?
|
||||
.replacingOccurrences(of: "+", with: "%2B")
|
||||
}
|
||||
|
||||
/**
|
||||
Encode the string to work with quary parameters.
|
||||
|
||||
This will first call `addingPercentEncoding`, using the
|
||||
`.urlPathAllowed` character set, then replace every `&`
|
||||
with `%26`.
|
||||
*/
|
||||
func urlEncoded() -> String? {
|
||||
self.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)?
|
||||
.replacingOccurrences(of: "&", with: "%26")
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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]()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// CLLocationCoordinate2D+Valid.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-09-18.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
|
||||
public extension CLLocationCoordinate2D {
|
||||
|
||||
/**
|
||||
Check if the coordinate is valid. This is a best effort
|
||||
that checks so that not both the latitude and longitude
|
||||
are not or any extremes.
|
||||
*/
|
||||
var isValid: Bool {
|
||||
isValid(latitude) && isValid(longitude)
|
||||
}
|
||||
}
|
||||
|
||||
private extension CLLocationCoordinate2D {
|
||||
|
||||
func isValid(_ degrees: CLLocationDegrees) -> Bool {
|
||||
degrees != 0 && degrees != 180 && degrees != -180
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// WorldCoordinate.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-10-04.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
|
||||
/**
|
||||
This struct can be used to represent world coordinates, but
|
||||
without bloating `CLLocationCoordinate2D` with static props.
|
||||
|
||||
The reason to why this is a struct and not an enum, is that
|
||||
it simplifies extending it with new coordinates in any apps
|
||||
that use custom coordinates.
|
||||
*/
|
||||
public struct WorldCoordinate: Hashable, Equatable, Identifiable {
|
||||
|
||||
public var id: String { name }
|
||||
|
||||
/**
|
||||
The name of the coordinate.
|
||||
*/
|
||||
public let name: String
|
||||
|
||||
/**
|
||||
The coordinate value.
|
||||
*/
|
||||
public let coordinate: CLLocationCoordinate2D
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
public extension WorldCoordinate {
|
||||
|
||||
static var manhattan: WorldCoordinate = .init(name: "Manhattan", coordinate: CLLocationCoordinate2D(latitude: 40.7590615, longitude: -73.969231))
|
||||
static var newYork: WorldCoordinate = .init(name: "New York", coordinate: CLLocationCoordinate2D(latitude: 40.7033127, longitude: -73.979681))
|
||||
static var sanFrancisco: WorldCoordinate = .init(name: "San Francisco", coordinate: CLLocationCoordinate2D(latitude: 37.7796828, longitude: -122.4000062))
|
||||
static var tokyo: WorldCoordinate = .init(name: "Tokyo", coordinate: CLLocationCoordinate2D(latitude: 35.673, longitude: 139.710))
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// KeychainItemAccessibility.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// Based on https://github.com/jrendel/SwiftKeychainWrapper
|
||||
// Created by James Blair on 4/24/16.
|
||||
// Copyright © 2016 Jason Rendel. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public protocol KeychainAttrRepresentable {
|
||||
|
||||
var keychainAttrValue: CFString { get }
|
||||
}
|
||||
|
||||
/**
|
||||
This enum defines the various access scopes that a keychain
|
||||
item can use. The names follow certain conventions that are
|
||||
defined in the list below:
|
||||
|
||||
* `afterFirstUnlock`
|
||||
The attribute cannot be accessed after a restart, until the
|
||||
device has been unlocked once by the user. After this first
|
||||
unlock, the items remains accessible until the next restart.
|
||||
This is recommended for items that must be available to any
|
||||
background applications or processes.
|
||||
|
||||
* `ThisDeviceOnly`
|
||||
The attribute will not be included in encrypted backup, and
|
||||
are thus not available after restoring apps from backups on
|
||||
a different device.
|
||||
|
||||
* `whenPasscodeSet`
|
||||
The attribute can only be accessed when the device has been
|
||||
unlocked by the user and a device passcode is set. No items
|
||||
can be stored on device if a passcode is not set. Disabling
|
||||
the passcode will delete all items.
|
||||
|
||||
* `whenUnlocked`
|
||||
The attribute can only be accessed when the device has been
|
||||
unlocked by the user. This is recommended for items that we
|
||||
only mean to use when the application is active.
|
||||
*/
|
||||
public enum KeychainItemAccessibility {
|
||||
|
||||
case afterFirstUnlock
|
||||
case afterFirstUnlockThisDeviceOnly
|
||||
case whenPasscodeSetThisDeviceOnly
|
||||
case whenUnlocked
|
||||
case whenUnlockedThisDeviceOnly
|
||||
|
||||
static func accessibilityForAttributeValue(_ keychainAttrValue: CFString) -> KeychainItemAccessibility? {
|
||||
keychainItemAccessibilityLookup.first { $0.value == keychainAttrValue }?.key
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let keychainItemAccessibilityLookup: [KeychainItemAccessibility: CFString] = [
|
||||
.afterFirstUnlock: kSecAttrAccessibleAfterFirstUnlock,
|
||||
.afterFirstUnlockThisDeviceOnly: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
.whenPasscodeSetThisDeviceOnly: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
||||
.whenUnlocked: kSecAttrAccessibleWhenUnlocked,
|
||||
.whenUnlockedThisDeviceOnly: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
]
|
||||
|
||||
extension KeychainItemAccessibility: KeychainAttrRepresentable {
|
||||
|
||||
public var keychainAttrValue: CFString {
|
||||
keychainItemAccessibilityLookup[self]!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// KeychainReader.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by keychain-based services
|
||||
that can read from the device keychain.
|
||||
*/
|
||||
public protocol KeychainReader: AnyObject {
|
||||
|
||||
func accessibility(for key: String) -> KeychainItemAccessibility?
|
||||
func bool(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool?
|
||||
func data(for key: String, with accessibility: KeychainItemAccessibility?) -> Data?
|
||||
func dataRef(for key: String, with accessibility: KeychainItemAccessibility?) -> Data?
|
||||
func double(for key: String, with accessibility: KeychainItemAccessibility?) -> Double?
|
||||
func float(for key: String, with accessibility: KeychainItemAccessibility?) -> Float?
|
||||
func hasValue(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
func integer(for key: String, with accessibility: KeychainItemAccessibility?) -> Int?
|
||||
func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String?
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// KeychainService.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by keychain-based services
|
||||
that can read from and write to the device keychain.
|
||||
*/
|
||||
public protocol KeychainService: KeychainReader, KeychainWriter {}
|
|
@ -0,0 +1,310 @@
|
|||
//
|
||||
// KeychainWrapper.swift
|
||||
// SwiftKit
|
||||
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
// Based on https://github.com/jrendel/SwiftKeychainWrapper
|
||||
// Created by Jason Rendel on 9/23/14.
|
||||
// Copyright © 2014 Jason Rendel. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
private let paramSecMatchLimit = kSecMatchLimit as String
|
||||
private let paramSecReturnData = kSecReturnData as String
|
||||
private let paramSecReturnPersistentRef = kSecReturnPersistentRef as String
|
||||
private let paramSecValueData = kSecValueData as String
|
||||
private let paramSecAttrAccessible = kSecAttrAccessible as String
|
||||
private let paramSecClass = kSecClass as String
|
||||
private let paramSecAttrService = kSecAttrService as String
|
||||
private let paramSecAttrGeneric = kSecAttrGeneric as String
|
||||
private let paramSecAttrAccount = kSecAttrAccount as String
|
||||
private let paramSecAttrAccessGroup = kSecAttrAccessGroup as String
|
||||
private let paramSecReturnAttributes = kSecReturnAttributes as String
|
||||
|
||||
|
||||
/**
|
||||
This class help make device keychain access easier in Swift.
|
||||
It is designed to make accessing the Keychain services more
|
||||
like using `NSUserDefaults`, which is much more familiar to
|
||||
developers in general.
|
||||
|
||||
`serviceName` is used for `kSecAttrService`, which uniquely
|
||||
identifies keychain accessors. If no name is specified, the
|
||||
value defaults to the current bundle identifier.
|
||||
|
||||
`accessGroup` is used for `kSecAttrAccessGroup`. This value
|
||||
is used to identify which keychain access group an entry is
|
||||
belonging to. This allows you to use `KeychainWrapper` with
|
||||
shared keychain access between different applications.
|
||||
|
||||
`NOTE` In SwiftKit, you can use a `StandardKeychainService`
|
||||
to isolate keychain access from contract design.
|
||||
*/
|
||||
open class KeychainWrapper {
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
Create a standard instance of this class.
|
||||
*/
|
||||
private convenience init() {
|
||||
let id = Bundle.main.bundleIdentifier
|
||||
let fallback = "com.swiftkit.keychain"
|
||||
self.init(serviceName: id ?? fallback)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a custom instance of this class.
|
||||
|
||||
- parameter serviceName: The service name for this instance. Used to uniquely identify all keys stored using this keychain wrapper instance.
|
||||
- parameter accessGroup: An optional, unique access group for this instance. Use a matching AccessGroup between applications to allow shared keychain access.
|
||||
*/
|
||||
public init(serviceName: String, accessGroup: String? = nil) {
|
||||
self.serviceName = serviceName
|
||||
self.accessGroup = accessGroup
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/**
|
||||
The standard keychain wrapper instance.
|
||||
*/
|
||||
public static let standard = KeychainWrapper()
|
||||
|
||||
/**
|
||||
This is used to uniquely identify the keychain wrapper.
|
||||
*/
|
||||
private let serviceName: String
|
||||
|
||||
/**
|
||||
This is used to identify to which Keychain Access Group
|
||||
this entry belongs. This allows you to use this wrapper
|
||||
with shared access between applications.
|
||||
*/
|
||||
private let accessGroup: String?
|
||||
|
||||
|
||||
// MARK: - KeychainReader
|
||||
|
||||
open func accessibility(for key: String) -> KeychainItemAccessibility? {
|
||||
var dict = setupKeychainQueryDictionary(forKey: key)
|
||||
var result: AnyObject?
|
||||
dict.removeValue(forKey: paramSecAttrAccessible)
|
||||
dict[paramSecMatchLimit] = kSecMatchLimitOne
|
||||
dict[paramSecReturnAttributes] = kCFBooleanTrue
|
||||
let status = withUnsafeMutablePointer(to: &result) {
|
||||
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
|
||||
}
|
||||
if status == noErr,
|
||||
let dict = result as? [String: AnyObject],
|
||||
let val = dict[paramSecAttrAccessible] as? String {
|
||||
return KeychainItemAccessibility.accessibilityForAttributeValue(val as CFString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func bool(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool? {
|
||||
number(for: key, with: accessibility)?.boolValue
|
||||
}
|
||||
|
||||
open func data(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Data? {
|
||||
var dict = setupKeychainQueryDictionary(forKey: key, with: accessibility)
|
||||
var result: AnyObject?
|
||||
dict[paramSecMatchLimit] = kSecMatchLimitOne
|
||||
dict[paramSecReturnData] = kCFBooleanTrue
|
||||
let status = withUnsafeMutablePointer(to: &result) {
|
||||
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
|
||||
}
|
||||
return status == noErr ? result as? Data: nil
|
||||
}
|
||||
|
||||
open func double(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Double? {
|
||||
number(for: key, with: accessibility)?.doubleValue
|
||||
}
|
||||
|
||||
open func float(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Float? {
|
||||
number(for: key, with: accessibility)?.floatValue
|
||||
}
|
||||
|
||||
open func hasValue(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
data(for: key, with: accessibility) != nil
|
||||
}
|
||||
|
||||
open func integer(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Int? {
|
||||
number(for: key, with: accessibility)?.intValue
|
||||
}
|
||||
|
||||
open func number(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> NSNumber? {
|
||||
object(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
open func object<T: NSObject & NSCoding>(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> T? {
|
||||
guard let keychainData = data(for: key, with: accessibility) else { return nil }
|
||||
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: keychainData)
|
||||
}
|
||||
|
||||
open func string(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> String? {
|
||||
guard let keychainData = data(for: key, with: accessibility) else { return nil }
|
||||
return String(data: keychainData, encoding: String.Encoding.utf8) as String?
|
||||
}
|
||||
|
||||
open func dataRef(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Data? {
|
||||
var dict = setupKeychainQueryDictionary(forKey: key, with: accessibility)
|
||||
var result: AnyObject?
|
||||
dict[paramSecMatchLimit] = kSecMatchLimitOne
|
||||
dict[paramSecReturnPersistentRef] = kCFBooleanTrue
|
||||
let status = withUnsafeMutablePointer(to: &result) {
|
||||
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
|
||||
}
|
||||
return status == noErr ? result as? Data: nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - KeychainWriter
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
set(NSNumber(value: value), for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
set(NSNumber(value: value), for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
set(NSNumber(value: value), for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
set(NSNumber(value: value), for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
guard let data = value.data(using: .utf8) else { return false }
|
||||
return set(data, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false) else { return false }
|
||||
return set(data, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
var dict: [String: Any] = setupKeychainQueryDictionary(forKey: key, with: accessibility)
|
||||
dict[paramSecValueData] = value
|
||||
|
||||
if let accessibility = accessibility {
|
||||
dict[paramSecAttrAccessible] = accessibility.keychainAttrValue
|
||||
} else {
|
||||
// Assign default protection - Protect the keychain entry so it's only valid when the device is unlocked
|
||||
dict[paramSecAttrAccessible] = KeychainItemAccessibility.whenUnlocked.keychainAttrValue
|
||||
}
|
||||
|
||||
let status = SecItemAdd(dict as CFDictionary, nil)
|
||||
if status == errSecDuplicateItem {
|
||||
return update(value, forKey: key, with: accessibility)
|
||||
}
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeObject(for key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
let keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key, with: accessibility)
|
||||
let status = SecItemDelete(keychainQueryDictionary as CFDictionary)
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all keychain items added with this wrapper. This
|
||||
will only delete items matching the current ServiceName
|
||||
and AccessGroup, if one is set.
|
||||
*/
|
||||
open func removeAllKeys() -> Bool {
|
||||
var dict: [String: Any] = [paramSecClass: kSecClassGenericPassword]
|
||||
dict[paramSecAttrService] = serviceName
|
||||
if let accessGroup = self.accessGroup {
|
||||
dict[paramSecAttrAccessGroup] = accessGroup
|
||||
}
|
||||
let status = SecItemDelete(dict as CFDictionary)
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all keychain data, including data not added with
|
||||
this keychain wrapper.
|
||||
|
||||
- Warning: This may remove custom keychain entries that
|
||||
you did not add via this wrapper.
|
||||
*/
|
||||
open class func wipeKeychain() {
|
||||
deleteKeychainSecClass(kSecClassGenericPassword) // Generic password items
|
||||
deleteKeychainSecClass(kSecClassInternetPassword) // Internet password items
|
||||
deleteKeychainSecClass(kSecClassCertificate) // Certificate items
|
||||
deleteKeychainSecClass(kSecClassKey) // Cryptographic key items
|
||||
deleteKeychainSecClass(kSecClassIdentity) // Identity items
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private extension KeychainWrapper {
|
||||
|
||||
/**
|
||||
Remove all items for a given Keychain Item Class
|
||||
*/
|
||||
@discardableResult
|
||||
class func deleteKeychainSecClass(_ secClass: AnyObject) -> Bool {
|
||||
let query = [paramSecClass: secClass]
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
/**
|
||||
Update ayn existing data associated with a specific key
|
||||
name. The existing data will be overwritten by new data.
|
||||
*/
|
||||
func update(_ value: Data, forKey key: String, with accessibility: KeychainItemAccessibility? = nil) -> Bool {
|
||||
var keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key, with: accessibility)
|
||||
let updateDictionary = [paramSecValueData: value]
|
||||
if let accessibility = accessibility {
|
||||
keychainQueryDictionary[paramSecAttrAccessible] = accessibility.keychainAttrValue
|
||||
}
|
||||
let status = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary)
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
/**
|
||||
Setup the keychain query dictionary, used to access the
|
||||
keychain on iOS for a specific key name and taking into
|
||||
account the Service Name and Access Group if one is set.
|
||||
|
||||
- parameter forKey: The key this query is for
|
||||
- parameter with: Optional accessibility to use when setting the keychain item. If none is provided, will default to .WhenUnlocked
|
||||
- returns: A dictionary with all the needed properties setup to access the keychain on iOS
|
||||
*/
|
||||
func setupKeychainQueryDictionary(forKey key: String, with accessibility: KeychainItemAccessibility? = nil) -> [String: Any] {
|
||||
var dict: [String: Any] = [paramSecClass: kSecClassGenericPassword]
|
||||
dict[paramSecAttrService] = serviceName
|
||||
if let accessibility = accessibility {
|
||||
dict[paramSecAttrAccessible] = accessibility.keychainAttrValue
|
||||
}
|
||||
if let accessGroup = self.accessGroup {
|
||||
dict[paramSecAttrAccessGroup] = accessGroup
|
||||
}
|
||||
let encodedIdentifier = key.data(using: String.Encoding.utf8)
|
||||
dict[paramSecAttrGeneric] = encodedIdentifier
|
||||
dict[paramSecAttrAccount] = encodedIdentifier
|
||||
return dict
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// KeychainWriter.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by keychain-based services
|
||||
that can write to the user's keychain.
|
||||
*/
|
||||
public protocol KeychainWriter: AnyObject {
|
||||
|
||||
@discardableResult
|
||||
func removeObject(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func removeAllKeys() -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
|
||||
@discardableResult
|
||||
func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// StandardKeychainService.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2016-11-24.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This is a standard implementation of `KeychainService` that
|
||||
uses a `KeychainWrapper` to sync data with the keychain.
|
||||
*/
|
||||
public class StandardKeychainService: KeychainService {
|
||||
|
||||
public init(wrapper: KeychainWrapper = .standard) {
|
||||
self.wrapper = wrapper
|
||||
}
|
||||
|
||||
private let wrapper: KeychainWrapper
|
||||
}
|
||||
|
||||
|
||||
// MARK: - KeychainReader
|
||||
|
||||
extension StandardKeychainService {
|
||||
|
||||
public func accessibility(for key: String) -> KeychainItemAccessibility? {
|
||||
wrapper.accessibility(for: key)
|
||||
}
|
||||
|
||||
public func bool(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool? {
|
||||
wrapper.bool(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func data(for key: String, with accessibility: KeychainItemAccessibility?) -> Data? {
|
||||
wrapper.data(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func dataRef(for key: String, with accessibility: KeychainItemAccessibility?) -> Data? {
|
||||
wrapper.dataRef(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func double(for key: String, with accessibility: KeychainItemAccessibility?) -> Double? {
|
||||
wrapper.double(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func float(for key: String, with accessibility: KeychainItemAccessibility?) -> Float? {
|
||||
wrapper.float(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func hasValue(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.hasValue(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func integer(for key: String, with accessibility: KeychainItemAccessibility?) -> Int? {
|
||||
wrapper.integer(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String? {
|
||||
wrapper.string(for: key, with: accessibility)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - KeychainWriter
|
||||
|
||||
extension StandardKeychainService {
|
||||
|
||||
@discardableResult
|
||||
public func removeObject(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.removeObject(for: key, with: accessibility)
|
||||
}
|
||||
|
||||
public func removeAllKeys() -> Bool {
|
||||
wrapper.removeAllKeys()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool {
|
||||
wrapper.set(value, for: key, with: accessibility)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// StandardTranslator.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-04-15.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This standard ``Translator`` implementation translates keys
|
||||
using `NSLocalizedString`.
|
||||
*/
|
||||
public class StandardTranslator: Translator {
|
||||
|
||||
public init() {}
|
||||
|
||||
/**
|
||||
Translate the provided key to a localized string.
|
||||
*/
|
||||
public func translate(_ key: String) -> String {
|
||||
NSLocalizedString(key, comment: "")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Translator.swift
|
||||
// SwiftKit
|
||||
//
|
||||
// Created by Daniel Saidi on 2015-04-15.
|
||||
// Copyright © 2020 Daniel Saidi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This protocol can be implemented by any classes that can be
|
||||
used to translate a localized string synchronously.
|
||||
*/
|
||||
public protocol Translator: AnyObject {
|
||||
|
||||
/**
|
||||
Translate the provided key to a localized string.
|
||||
*/
|
||||
func translate(_ key: String) -> String
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue