Compare commits

..

No commits in common. "master" and "0.0.4" have entirely different histories.

219 changed files with 1874 additions and 6179 deletions

32
.gitignore vendored
View File

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

View File

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

View File

@ -1,9 +1,9 @@
# Run `pod lib lint DSSwiftKit.podspec' to ensure this is a valid spec.
# Run `pod lib lint SwiftKit.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.version = '0.0.4'
s.swift_versions = ['5.2']
s.summary = 'SwiftKit contains extra functionality for Swift.'
s.description = <<-DESC
@ -16,11 +16,14 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/danielsaidi/SwiftKit.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/danielsaidi'
s.swift_version = '5.6'
s.ios.deployment_target = '13.0'
s.macos.deployment_target = '11.0'
s.swift_version = '5.2'
s.ios.deployment_target = '11.0'
s.tvos.deployment_target = '13.0'
s.watchos.deployment_target = '6.0'
s.source_files = 'Sources/**/*.swift'
s.watchos.exclude_files = [
'Sources/SwiftKit/Authentication/BiometricAuthenticationService.swift',
]
end

View File

@ -4,103 +4,28 @@ 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)
if bump_type == nil or bump_type.empty?
bump_type = "patch"
end
version = version_bump_podspec(path: "DSSwiftKit.podspec", bump_type: bump_type)
# increment_version_number(version_number: version)
git_commit(
path: "*",
message: "Bump to #{version}"
)
git_commit(path: "*", message: "Bump to #{version}")
add_git_tag(tag: version)
push_git_tags()
push_to_git_remote()
pod_push()
end
# Docs =======================
desc "Build documentation for all platforms"
lane :docc do
sh('cd .. && rm -rf Docs')
docc_platform(destination: 'iOS', name: 'ios')
docc_platform(destination: 'OS X', name: 'osx')
docc_platform(destination: 'tvOS', name: 'tvos')
docc_platform(destination: 'watchOS', name: 'watchos')
end
desc "Build documentation for a single platform"
lane :docc_platform do |values|
sh('cd .. && mkdir -p Docs')
docc_delete_derived_data
sh('cd .. && xcodebuild docbuild \
-scheme SwiftKit \
-destination \'generic/platform=' + values[:destination] + '\'')
sh('cd .. && \
find ~/Library/Developer/Xcode/DerivedData \
-name "SwiftKit.doccarchive" \
-exec cp -R {} Docs \;')
sh('cd .. && \
mv Docs/SwiftKit.doccarchive Docs/SwiftKit_' + values[:name] + '.doccarchive')
end
desc "Delete documentation derived data (may be historic duplicates)"
lane :docc_delete_derived_data do
sh('find ~/Library/Developer/Xcode/DerivedData \
-name "SwiftKit.doccarchive" \
-exec rm -Rf {} \; || true')
end
desc "Build static documentation websites for all platforms"
lane :docc_web do
docc
docc_web_platform(name: 'ios')
docc_web_platform(name: 'osx')
docc_web_platform(name: 'tvos')
docc_web_platform(name: 'watchos')
end
desc "Build static documentation website for a single platform"
lane :docc_web_platform do |values|
sh('cd .. && $(xcrun --find docc) process-archive \
transform-for-static-hosting Docs/SwiftKit_' + values[:name] + '.doccarchive \
--output-path Docs/web_' + values[:name] + ' \
--hosting-base-path SwiftKit')
end
end

View File

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

View File

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

View File

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

11
Readmes/Authentication.md Normal file
View File

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

9
Readmes/Coding.md Normal file
View File

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

9
Readmes/Device.md Normal file
View File

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

5
Readmes/Extensions.md Normal file
View File

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

9
Readmes/IoC.md Normal file
View File

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

11
Readmes/Keychain.md Normal file
View File

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

View File

@ -1,278 +1,6 @@
# 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.
I will bump revision by revision, until SwiftKit has all functionality that it should have from iExtra. I will then bump it to `1.0.0`.
## 0.0.4
@ -280,19 +8,16 @@ This version adds a bunch of [extensions][Extensions] and common types.
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`.

BIN
Resources/Icon.sketch Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

View File

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

View File

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

View File

@ -9,23 +9,10 @@
import Foundation
/**
This enum represents various authentication errors that can
occur while a user is being authenticated.
This enum represents possible authentication service errors.
*/
public enum AuthenticationServiceError: Error, Equatable {
/**
The authentication failed.
*/
case authenticationFailed
/**
The authentication failed with a certain error message.
*/
case authenticationFailedWithErrorMessage(String)
/**
The requested authentication type is not supported.
*/
case unsupportedAuthentication
}

View File

@ -6,20 +6,44 @@
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
#if os(iOS) || os(macOS)
import LocalAuthentication
/**
This authentication service uses `LocalAuthentication` such
as `FaceID` or `TouchID` to authenticate the user.
as `FaceID` or `TOuchID` to authenticate the user.
*/
public class BiometricAuthenticationService: LocalAuthenticationService {
public class BiometricAuthenticationService: AuthenticationService {
/**
Create a service instance.
*/
public init() {
super.init(policy: .deviceOwnerAuthenticationWithBiometrics)
// MARK: - Initialization
public init() {}
// MARK: - Properties
private let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
// MARK: - Public functions
public func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
guard canAuthenticateUser(for: auth) else { return completion(.failure(AuthError.unsupportedAuthentication)) }
performAuthentication(for: auth, reason: reason) { result in
DispatchQueue.main.async { completion(result) }
}
}
public func canAuthenticateUser(for auth: Authentication) -> Bool {
var error: NSError?
return LAContext().canEvaluatePolicy(policy, error: &error)
}
public func performAuthentication(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
LAContext().evaluatePolicy(policy, localizedReason: reason) { result, error in
if let error = error { return completion(.failure(error)) }
if result == false { return completion(.failure(AuthError.authenticationFailed)) }
completion(.success(()))
}
}
}
#endif

View File

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

View File

@ -9,8 +9,8 @@
import Foundation
/**
This class wraps another ``AuthenticationService`` instance
and keeps authentication results in a cache.
This service wraps another authentication service and keeps
its authentication result in a cache.
*/
public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
@ -31,11 +31,6 @@ public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
// MARK: - Functions
/**
Authenticate the user for a certain authentication type.
`reason` can be used to display information to the user.
*/
public func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
if isUserAuthenticated(for: auth) { return completion(.success(())) }
baseService.authenticateUser(for: auth, reason: reason) { result in
@ -44,35 +39,20 @@ public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
}
}
/**
Check if the service instance can authenticate the user.
*/
public func canAuthenticateUser(for auth: Authentication) -> Bool {
baseService.canAuthenticateUser(for: auth)
}
/**
Check if the service has already authenticated the user
for a certain authentication type.
*/
public func isUserAuthenticated(for auth: Authentication) -> Bool {
cache[auth.id] ?? false
}
/**
Reset the service's cached authentication state for the
provided authentication type.
*/
public func resetUserAuthentication(for auth: Authentication) {
setIsAuthenticated(false, for: auth)
public func resetUserAuthentication() {
cache.removeAll()
}
/**
Reset the service's cached authentication state for all
authentication types.
*/
public func resetUserAuthentications() {
cache.removeAll()
public func resetUserAuthentication(for auth: Authentication) {
setIsAuthenticated(false, for: auth)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
//
// StringDecoder.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-03-21.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can encode
and decode strings.
*/
public protocol StringCoder: StringEncoder, StringDecoder {}

View File

@ -0,0 +1,18 @@
//
// StringDecoder.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-03-21.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can decode
strings.
*/
public protocol StringDecoder: AnyObject {
func decode(_ string: String) -> String?
}

View File

@ -0,0 +1,18 @@
//
// StringEncoder.swift
// SwiftKit
//
// Created by Daniel Saidi on 2015-03-21.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can encode
strings.
*/
public protocol StringEncoder: AnyObject {
func encode(_ string: String) -> String?
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,13 +29,6 @@ public class KeychainBasedDeviceIdentifier: DeviceIdentifier {
private let backupIdentifier: DeviceIdentifier
private let keychainService: KeychainService
/**
Get a unique device identifier from the device keychain.
If no identifier exists in the keychain, the identifier
will use the provided `backupIdentifier` to generate an
identifier, then persist that id in the device keychain.
*/
public func getDeviceIdentifier() -> String {
if let id = keychainService.string(for: key, with: nil) { return id }
let id = backupIdentifier.getDeviceIdentifier()

View File

@ -28,13 +28,6 @@ public class UserDefaultsBasedDeviceIdentifier: DeviceIdentifier {
private let defaults: UserDefaults
/**
Get a unique device identifier from the user defaults.
If no persisted identifier exists, this identifier will
generate a new identifier, then persist and return that
identifier.
*/
public func getDeviceIdentifier() -> String {
if let id = defaults.string(forKey: key) { return id }
return generateDeviceIdentifier()
@ -46,6 +39,7 @@ private extension UserDefaultsBasedDeviceIdentifier {
func generateDeviceIdentifier() -> String {
let id = UUID().uuidString
defaults.set(id, forKey: key)
defaults.synchronize()
return id
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,15 +22,6 @@ public extension DispatchQueue {
deadline: .now() + interval,
execute: execute)
}
/**
Perform an operation after a time interval.
*/
func asyncAfter(
seconds: TimeInterval,
execute: @escaping () -> Void) {
let milli = Int(seconds * 1000)
asyncAfter(.milliseconds(milli), execute: execute)
}
/**
Perform an async operation then call a completion block

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,17 +12,11 @@ import Foundation
public extension String {
/**
Base64 decode the string.
*/
func base64Decoded() -> String? {
guard let data = Data(base64Encoded: self) else { return nil }
return String(data: data, encoding: .utf8)
}
/**
Base64 encode the string.
*/
func base64Encoded() -> String? {
data(using: .utf8)?.base64EncodedString()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@
// 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
//
@ -13,17 +12,10 @@ import Foundation
public extension String {
/**
This is a shortcut to `replacingOccurrences(of:with:)`.
*/
func replacing(_ string: String, with: String) -> String {
replacingOccurrences(of: string, with: with)
}
/**
This is a shortcut to `replacingOccurrences(of:with:)`,
with a `caseInsensitive` option enabled.
*/
func replacing(_ string: String, with: String, caseSensitive: Bool) -> String {
caseSensitive
? replacing(string, with: with)

View File

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

View File

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

View File

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

View File

@ -11,27 +11,9 @@
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)?
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)?
.replacingOccurrences(of: "&", with: "%26")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,13 +14,14 @@ import Foundation
You can either implement your own `IoCContainer` or comment
out the code of any `IocContainer` class in this folder and
add it to your app. You can then register it globally using
`IoC.register(...)` and use it with `IoC.resolve(...)`.
add it to your app. You can then either use it as is, or go
ahead and register it globally with `IoC.register(...)`. It
can then be used with the `IoC.resolve(...)` shortcuts.
If you don't want to use an IoC container, you can just use
the `IoC` class as a container for static properties.
In this folder, you'll find two disabled implementations of
`IocContainer`. `DipIoCContainer` and `SwinjectIoCContainer`
requires more external dependencies before they can be used.
*/
@available(*, deprecated, message: "This type has been deprecated and will be removed in the next major version.")
public final class IoC {
public private(set) static var container: IoCContainer!

View File

@ -14,7 +14,6 @@ import Foundation
inversion of control, by dynamically resolving types, given
any required arguments.
*/
@available(*, deprecated, message: "This type has been deprecated and will be removed in the next major version.")
public protocol IoCContainer {
func resolve<T>() -> T

View File

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

View File

@ -22,5 +22,6 @@ public protocol KeychainReader: AnyObject {
func float(for key: String, with accessibility: KeychainItemAccessibility?) -> Float?
func hasValue(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
func integer(for key: String, with accessibility: KeychainItemAccessibility?) -> Int?
func object(for key: String, with accessibility: KeychainItemAccessibility?) -> NSCoding?
func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String?
}

View File

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

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