├── .github
└── FUNDING.yml
├── .gitignore
├── .swiftlint.yml
├── DSSwiftKit.podspec
├── Fastlane
└── Fastfile
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── RELEASE_NOTES.md
├── Resources
├── Logo.png
├── Logo.sketch
└── Logo_solid.png
├── Sources
└── SwiftKit
│ ├── Bundle
│ └── Bundle+BundleInformation.swift
│ ├── Concurrency
│ └── Collection+Async.swift
│ ├── Cvs
│ ├── CsvParser.swift
│ ├── CsvParserError.swift
│ └── StandardCsvParser.swift
│ ├── Data
│ ├── Base64StringCoder.swift
│ ├── MimeType.swift
│ └── StringCoder.swift
│ ├── Date
│ ├── Calendar+Date.swift
│ ├── Date+AddRemove.swift
│ ├── Date+Compare.swift
│ ├── Date+Components.swift
│ ├── Date+Difference.swift
│ ├── Date+Init.swift
│ ├── DateDecoders.swift
│ ├── DateEncoders.swift
│ └── DateFormatter+Init.swift
│ ├── Device
│ ├── DeviceIdentifier.swift
│ ├── KeychainBasedDeviceIdentifier.swift
│ └── UserDefaultsBasedDeviceIdentifier.swift
│ ├── Extensions
│ ├── Collections
│ │ ├── Array+Range.swift
│ │ ├── Collection+Content.swift
│ │ ├── Collection+Distinct.swift
│ │ ├── Sequence+Batched.swift
│ │ └── Sequence+Grouped.swift
│ ├── Comparable+Closest.swift
│ ├── Comparable+Limit.swift
│ ├── ComparisonResult+Shortcuts.swift
│ ├── DispatchQueue+Async.swift
│ ├── DispatchQueue+Throttle.swift
│ ├── NSAttributedString
│ │ ├── NSAttributedString+Archive.swift
│ │ ├── NSAttributedString+Rtf.swift
│ │ └── NSAttributedString+Text.swift
│ ├── Optional+IsSet.swift
│ ├── String
│ │ ├── String+Base64.swift
│ │ ├── String+Bool.swift
│ │ ├── String+Capitalize.swift
│ │ ├── String+Characters.swift
│ │ ├── String+Contains.swift
│ │ ├── String+Content.swift
│ │ ├── String+Dictation.swift
│ │ ├── String+Paragraph.swift
│ │ ├── String+Replace.swift
│ │ ├── String+Split.swift
│ │ ├── String+Subscript.swift
│ │ ├── String+Trimmed.swift
│ │ └── String+UrlEncode.swift
│ ├── Url+Global.swift
│ └── UserDefaults+Codable.swift
│ ├── Files
│ ├── BundleFileFinder.swift
│ ├── DirectoryService.swift
│ ├── FileFinder.swift
│ ├── FileManager+UniqueFileName.swift
│ └── StandardDirectoryService.swift
│ ├── Geo
│ ├── CLLocationCoordinate2D+Equatable.swift
│ ├── CLLocationCoordinate2D+Map.swift
│ ├── CLLocationCoordinate2D+Valid.swift
│ └── WorldCoordinate.swift
│ ├── Keychain
│ ├── KeychainItemAccessibility.swift
│ ├── KeychainReader.swift
│ ├── KeychainService.swift
│ ├── KeychainWrapper.swift
│ ├── KeychainWriter.swift
│ └── StandardKeychainService.swift
│ ├── Localization
│ ├── BundleTranslator.swift
│ ├── LocalizationNotification.swift
│ ├── LocalizationService.swift
│ ├── StandardLocalizationService.swift
│ ├── StandardTranslator.swift
│ └── Translator.swift
│ ├── Numerics
│ ├── Decimal+Double.swift
│ ├── Double+Rounded.swift
│ ├── NumberFormatter+Init.swift
│ ├── NumberFormatter+Util.swift
│ ├── Numeric+Conversions.swift
│ └── Numeric+String.swift
│ ├── Services
│ ├── Decorator.swift
│ ├── MultiProxy.swift
│ └── Proxy.swift
│ ├── SwiftKit.docc
│ ├── Resources
│ │ └── Logo.png
│ └── SwiftKit.md
│ ├── Validation
│ ├── EmailValidator.swift
│ └── Validator.swift
│ ├── _Deprecated
│ ├── Authentication
│ │ ├── Authentication.swift
│ │ ├── AuthenticationService.swift
│ │ ├── AuthenticationServiceError.swift
│ │ ├── BiometricAuthenticationService.swift
│ │ ├── CachedAuthenticationService.swift
│ │ ├── CachedAuthenticationServiceProxy.swift
│ │ └── LocalAuthenticationService.swift
│ ├── Bundle
│ │ └── BundleInformation.swift
│ ├── Data
│ │ ├── Filter.swift
│ │ └── Persisted.swift
│ ├── Extensions
│ │ ├── Result+Utils.swift
│ │ ├── Url+GlobalDeprecated.swift
│ │ └── Url+QueryParameters.swift
│ ├── Files
│ │ ├── FileExporter.swift
│ │ └── StandardFileExporter.swift
│ ├── Geo
│ │ ├── AppleMapsService.swift
│ │ ├── ExternalMapService.swift
│ │ └── GoogleMapsService.swift
│ ├── IoC
│ │ ├── DipIoCContainer.swift
│ │ ├── IoC.swift
│ │ ├── IoCContainer.swift
│ │ └── SwinjectIoCContainer.swift
│ ├── Messaging
│ │ ├── MFMailComposeViewController+Attachments.swift
│ │ └── MSMessageComposeViewController+Attachments.swift
│ ├── Network
│ │ ├── ApiEnvironment.swift
│ │ ├── ApiModel.swift
│ │ ├── ApiRoute.swift
│ │ ├── ApiService.swift
│ │ ├── ApiTypes.swift
│ │ └── HttpMethod.swift
│ └── StoreKit
│ │ ├── StandardStoreService.swift
│ │ ├── StoreContext+Products.swift
│ │ ├── StoreContext.swift
│ │ ├── StoreService.swift
│ │ ├── StoreServiceError.swift
│ │ └── Transaction+Valid.swift
│ └── iCloud
│ ├── StandardiCloudDocumenSync.swift
│ ├── URL+iCloud.swift
│ └── iCloudDocumentSync.swift
└── Tests
└── SwiftKitTests
├── AsyncTrigger.swift
├── Coding
└── Base64StringCoderTests.swift
├── Csv
└── StandardCsvParserTests.swift
├── Data
├── FilterTests.swift
└── MimeTypeTests.swift
├── Date
├── Date+AddRemoveTests.swift
├── Date+CompareTests.swift
├── Date+DifferenceTests.swift
├── Date+InitTests.swift
├── DateDecodersTests.swift
├── DateEncodersTests.swift
└── DateFormatter+InitTests.swift
├── Device
├── KeychainBasedDeviceIdentifierTests.swift
├── MockDeviceIdentifier.swift
└── UserDefaultsBasedDeviceIdentifierTests.swift
├── Extensions
├── Bundle+BundleInformationTests.swift
├── Collections
│ ├── Array+RangeTests.swift
│ ├── Collection+ContentTests.swift
│ ├── Collection+DistinctTests.swift
│ ├── Sequence+BatchedTests.swift
│ └── Sequence+GroupedTests.swift
├── Comparable+ClosestTests.swift
├── Comparable+LimitTests.swift
├── ComparisonResult+ShortcutsTests.swift
├── DispatchQueue+AsyncTests.swift
├── Optional+IsSetTests.swift
├── Result+UtilsTests.swift
├── String
│ ├── String+Base64Tests.swift
│ ├── String+BoolTests.swift
│ ├── String+ContainsTests.swift
│ ├── String+ContentTests.swift
│ ├── String+ParagraphTests.swift
│ ├── String+ReplaceTests.swift
│ ├── String+SplitTests.swift
│ └── String+UrlEncodeTests.swift
├── Url+GlobalTests.swift
├── Url+QueryParametersTests.swift
└── UserDefaults+CodableTests.swift
├── Geo
├── CLLocationCoordinate2D+ValidTests.swift
└── WorldCoordinateTests.swift
├── Keychain
└── MockKeychainService.swift
├── Numerics
├── Decimal+DoubleTests.swift
├── Double+RoundedTests.swift
├── NumberFormatter+InitTests.swift
├── NumberFormatter+UtilTests.swift
├── Numeric+ConversionsTests.swift
└── Numeric+StringTests.swift
└── Validation
└── EmailValidatorTests.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: danielsaidi
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # SPM defaults
2 | .DS_Store
3 | /.build
4 | /Packages
5 | .swiftpm/
6 |
7 | # Documentation
8 | Docs
9 | documentation
10 | downloads
11 | videos
12 |
13 | # Fastlane
14 | Fastlane/report.xml
15 | Fastlane/Preview.html
16 | Fastlane/screenshots
17 | Fastlane/test_output
18 | Fastlane/README.md
19 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - function_body_length
3 | - identifier_name
4 | - line_length
5 | - todo
6 | - trailing_whitespace
7 | - type_name
8 | - vertical_whitespace
9 |
10 | included:
11 | - Sources
12 | - Tests
13 |
--------------------------------------------------------------------------------
/DSSwiftKit.podspec:
--------------------------------------------------------------------------------
1 | # Run `pod lib lint DSSwiftKit.podspec' to ensure this is a valid spec.
2 |
3 | Pod::Spec.new do |s|
4 | s.name = 'DSSwiftKit'
5 | s.version = '1.5.0'
6 | s.swift_versions = ['5.3']
7 | s.summary = 'SwiftKit contains extra functionality for Swift.'
8 |
9 | s.description = <<-DESC
10 | SwiftKit contains extra functionality for Swift, like extensions, utils etc.
11 | DESC
12 |
13 | s.homepage = 'https://github.com/danielsaidi/SwiftKit'
14 | s.license = { :type => 'MIT', :file => 'LICENSE' }
15 | s.author = { 'Daniel Saidi' => 'daniel.saidi@gmail.com' }
16 | s.source = { :git => 'https://github.com/danielsaidi/SwiftKit.git', :tag => s.version.to_s }
17 | s.social_media_url = 'https://twitter.com/danielsaidi'
18 |
19 | s.swift_version = '5.6'
20 | s.ios.deployment_target = '13.0'
21 | s.macos.deployment_target = '11.0'
22 | s.tvos.deployment_target = '13.0'
23 | s.watchos.deployment_target = '6.0'
24 |
25 | s.source_files = 'Sources/**/*.swift'
26 | end
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Daniel Saidi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "cwlcatchexception",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/mattgallagher/CwlCatchException.git",
7 | "state" : {
8 | "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea",
9 | "version" : "2.1.1"
10 | }
11 | },
12 | {
13 | "identity" : "cwlpreconditiontesting",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
16 | "state" : {
17 | "revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688",
18 | "version" : "2.1.0"
19 | }
20 | },
21 | {
22 | "identity" : "mockingkit",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/danielsaidi/MockingKit.git",
25 | "state" : {
26 | "revision" : "3e51adb1a3922cdccbe84a3088b7fa4d67ae236d",
27 | "version" : "1.1.0"
28 | }
29 | },
30 | {
31 | "identity" : "nimble",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/danielsaidi/Nimble.git",
34 | "state" : {
35 | "branch" : "main",
36 | "revision" : "f76b83c051fb3e6c120a33ebac200efba883065a"
37 | }
38 | },
39 | {
40 | "identity" : "quick",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/danielsaidi/Quick.git",
43 | "state" : {
44 | "branch" : "main",
45 | "revision" : "1efe9551db0ad6a6e979f33366969750123d14d9"
46 | }
47 | }
48 | ],
49 | "version" : 2
50 | }
51 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.6
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SwiftKit",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v11),
10 | .tvOS(.v13),
11 | .watchOS(.v6)
12 | ],
13 | products: [
14 | .library(
15 | name: "SwiftKit",
16 | targets: ["SwiftKit"]
17 | ),
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/danielsaidi/Quick.git", branch: "main"), // .upToNextMajor(from: "4.0.0")),
21 | .package(url: "https://github.com/danielsaidi/Nimble.git", branch: "main"), // .upToNextMajor(from: "9.0.0")),
22 | .package(url: "https://github.com/danielsaidi/MockingKit.git", .upToNextMajor(from: "1.1.0"))
23 | ],
24 | targets: [
25 | .target(
26 | name: "SwiftKit",
27 | dependencies: []),
28 | .testTarget(
29 | name: "SwiftKitTests",
30 | dependencies: ["SwiftKit", "Quick", "Nimble", "MockingKit"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## SwiftKit is being merged with SwiftUIKit
19 |
20 | This repository is being merged with [SwiftUIKit](https://github.com/danielsaidi/SwiftUIKit). You find most of the functionality here in SwiftUIKit 4.0.
21 |
22 |
23 |
24 | ## Installation
25 |
26 | SwiftKit can be installed with the Swift Package Manager:
27 |
28 | ```
29 | https://github.com/danielsaidi/SwiftKit.git
30 | ```
31 |
32 | If you prefer to not have external dependencies, you can also just copy the source code into your app.
33 |
34 |
35 |
36 | ## Documentation
37 |
38 | The [online documentation][Documentation] has more information, code examples, etc., and makes it easy to overview the various parts of the library.
39 |
40 |
41 |
42 | ## Support
43 |
44 | I manage my various open-source projects in my free time and am really thankful for any help I can get from the community.
45 |
46 | You can sponsor this project on [GitHub Sponsors][Sponsors] or get in touch for paid support.
47 |
48 |
49 |
50 | ## Contact
51 |
52 | Feel free to reach out if you have questions or if you want to contribute in any way:
53 |
54 | * Website: [danielsaidi.com][Website]
55 | * Mastodon: [@danielsaidi@mastodon.social][Mastodon]
56 | * Twitter: [@danielsaidi][Twitter]
57 | * E-mail: [daniel.saidi@gmail.com][Email]
58 |
59 |
60 |
61 | ## Supported Platforms
62 |
63 | SwiftKit supports `iOS 13`, `macOS 11`, `tvOS 13` and `watchOS 6`.
64 |
65 |
66 |
67 | ## License
68 |
69 | SwiftKit is available under the MIT license. See the [LICENSE][License] file for more info.
70 |
71 |
72 | [Email]: mailto:daniel.saidi@gmail.com
73 | [Website]: https://www.danielsaidi.com
74 | [Twitter]: https://www.twitter.com/danielsaidi
75 | [Mastodon]: https://mastodon.social/@danielsaidi
76 | [Sponsors]: https://github.com/sponsors/danielsaidi
77 |
78 | [Documentation]: https://danielsaidi.github.io/SwiftKit/documentation/swiftkit/
79 | [License]: https://github.com/danielsaidi/SwiftKit/blob/master/LICENSE
80 |
--------------------------------------------------------------------------------
/Resources/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SwiftKit/0acd067a8aaf1d7fc5aabda5de0e9d829de4c64d/Resources/Logo.png
--------------------------------------------------------------------------------
/Resources/Logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SwiftKit/0acd067a8aaf1d7fc5aabda5de0e9d829de4c64d/Resources/Logo.sketch
--------------------------------------------------------------------------------
/Resources/Logo_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SwiftKit/0acd067a8aaf1d7fc5aabda5de0e9d829de4c64d/Resources/Logo_solid.png
--------------------------------------------------------------------------------
/Sources/SwiftKit/Bundle/Bundle+BundleInformation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bundle+BundleInformation.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Bundle {
12 |
13 | /// Get the bundle build number, e.g. `42567`.
14 | var buildNumber: String {
15 | let key = String(kCFBundleVersionKey)
16 | let version = infoDictionary?[key] as? String
17 | return version ?? ""
18 | }
19 |
20 | /// Get the bundle display name, if any.
21 | var displayName: String {
22 | infoDictionary?["CFBundleDisplayName"] as? String ?? "-"
23 | }
24 |
25 | /// Get the bundle build number, e.g. `42567`.
26 | var versionNumber: String {
27 | let key = "CFBundleShortVersionString"
28 | let version = infoDictionary?[key] as? String
29 | return version ?? "0.0.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Concurrency/Collection+Async.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+Async.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-10.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Collection {
12 |
13 | /**
14 | Compact map a collection using an async transform.
15 | */
16 | func asyncCompactMap(_ transform: (Element) async -> ResultType?) async -> [ResultType] {
17 | await self
18 | .asyncMap(transform)
19 | .compactMap { $0 }
20 | }
21 |
22 | /**
23 | Compact map a collection using an async transform.
24 | */
25 | func asyncCompactMap(_ transform: (Element) async throws -> ResultType?) async throws -> [ResultType] {
26 | try await self
27 | .asyncMap(transform)
28 | .compactMap { $0 }
29 | }
30 |
31 | /**
32 | Map a collection using an async transform.
33 | */
34 | func asyncMap(_ transform: (Element) async -> ResultType) async -> [ResultType] {
35 | var result = [ResultType]()
36 | for item in self {
37 | await result.append(transform(item))
38 | }
39 | return result
40 | }
41 |
42 | /**
43 | Map a collection using an async transform.
44 | */
45 | func asyncMap(_ transform: (Element) async throws -> ResultType) async throws -> [ResultType] {
46 | var result = [ResultType]()
47 | for item in self {
48 | try await result.append(transform(item))
49 | }
50 | return result
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Cvs/CsvParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CsvParser.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-10-23.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by classes that can handle
13 | parsing of comma-separated value files and strings.
14 |
15 | When parsing a csv file or string, every line will be split
16 | up into components using the provided `componentSeparator`.
17 | */
18 | public protocol CsvParser {
19 |
20 | /**
21 | Parse a csv file in a certain bundle.
22 |
23 | - Parameters:
24 | - fileName: The name of the file to parse.
25 | - fileExtension: The extension of the file to parse.
26 | - bundle: The bundle in which the file is located.
27 | - componentSeparator: The separator that separates components on each line.
28 | */
29 | func parseCsvFile(
30 | named fileName: String,
31 | withExtension fileExtension: String,
32 | in bundle: Bundle,
33 | componentSeparator: Character
34 | ) throws -> [[String]]
35 |
36 | /**
37 | Parse a csv file at a certain url.
38 |
39 | - Parameters:
40 | - url: The url of the file to parse.
41 | - componentSeparator: The separator that separates components on each line.
42 | */
43 | func parseCsvFile(
44 | at url: URL,
45 | componentSeparator: Character
46 | ) throws -> [[String]]
47 |
48 | /**
49 | Parse the provided csv string.
50 |
51 | - Parameters:
52 | - string: The string to parse.
53 | - componentSeparator: The separator that separates components on each line.
54 | */
55 | func parseCsvString(
56 | _ string: String,
57 | componentSeparator: Character
58 | ) -> [[String]]
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Cvs/CsvParserError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CsvParserError.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-10-23.
6 | // Copyright © 2018 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This error can be thrown while parsing a csv string or file.
13 | */
14 | public enum CsvParserError: Error {
15 |
16 | /// The requested file doesn't exist.
17 | case noFileWithName(_ fileName: String, andExtension: String, inBundle: Bundle)
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Cvs/StandardCsvParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardCsvParser.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-10-23.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This class can be used to parse comma-separated value files
13 | and strings.
14 |
15 | When parsing a csv file or string, every line will be split
16 | up into components using the provided `componentSeparator`.
17 | */
18 | public class StandardCsvParser: CsvParser {
19 |
20 | /**
21 | Create a parser instance.
22 | */
23 | public init(fileManager: FileManager = .default) {
24 | self.fileManager = fileManager
25 | }
26 |
27 | private let fileManager: FileManager
28 |
29 |
30 | /**
31 | Parse a csv file in a certain bundle.
32 |
33 | - Parameters:
34 | - fileName: The name of the file to parse.
35 | - fileExtension: The extension of the file to parse.
36 | - bundle: The bundle in which the file is located.
37 | - componentSeparator: The separator that separates components on each line.
38 | */
39 | public func parseCsvFile(
40 | named fileName: String,
41 | withExtension ext: String,
42 | in bundle: Bundle,
43 | componentSeparator: Character
44 | ) throws -> [[String]] {
45 | guard let path = bundle.path(forResource: fileName, ofType: ext) else {
46 | throw CsvParserError.noFileWithName(fileName, andExtension: ext, inBundle: bundle)
47 | }
48 | let string = try String(contentsOfFile: path, encoding: .utf8)
49 | return parseCsvString(string, componentSeparator: componentSeparator)
50 | }
51 |
52 | /**
53 | Parse a csv file at a certain url.
54 |
55 | - Parameters:
56 | - url: The url of the file to parse.
57 | - componentSeparator: The separator that separates components on each line.
58 | */
59 | public func parseCsvFile(
60 | at url: URL,
61 | componentSeparator: Character
62 | ) throws -> [[String]] {
63 | let string = try String(contentsOf: url, encoding: .utf8)
64 | return parseCsvString(string, componentSeparator: componentSeparator)
65 | }
66 |
67 | /**
68 | Parse the provided csv string.
69 |
70 | - Parameters:
71 | - string: The string to parse.
72 | - componentSeparator: The separator that separates components on each line.
73 | */
74 | public func parseCsvString(
75 | _ string: String,
76 | componentSeparator: Character
77 | ) -> [[String]] {
78 | string
79 | .components(separatedBy: .newlines)
80 | .map { $0.trimmingCharacters(in: .whitespaces) }
81 | .filter { !$0.isEmpty }
82 | .map { $0.split(separator: componentSeparator)
83 | .map { String($0).trimmingCharacters(in: .whitespaces) }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Data/Base64StringCoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Base64StringEncoder.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-03-21.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This coder can encode and decode strings to and from base64.
13 | */
14 | public class Base64StringCoder: StringCoder {
15 |
16 | public init() {}
17 |
18 | /// Decode a base64 encoded string.
19 | public func decode(_ string: String) -> String? {
20 | guard let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters) else { return nil }
21 | return String(data: data, encoding: .utf8)
22 | }
23 |
24 | /// Encode a string to base64.
25 | public func encode(_ string: String) -> String? {
26 | let data = string.data(using: .utf8)
27 | let encoded = data?.base64EncodedData(options: .endLineWithLineFeed)
28 | guard let encodedData = encoded else { return nil }
29 | return String(data: encodedData, encoding: .utf8)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Data/StringCoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringDecoder.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-03-21.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by classes that can encode
13 | and decode strings.
14 | */
15 | public protocol StringCoder: StringEncoder, StringDecoder {}
16 |
17 | /**
18 | This protocol can be implemented by classes that can decode
19 | strings.
20 | */
21 | public protocol StringDecoder: AnyObject {
22 |
23 | /// Decode a string.
24 | func decode(_ string: String) -> String?
25 | }
26 |
27 | /**
28 | This protocol can be implemented by classes that can encode
29 | strings.
30 | */
31 | public protocol StringEncoder: AnyObject {
32 |
33 | /// Encode a string.
34 | func encode(_ string: String) -> String?
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/Calendar+Date.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Calendar+Date.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-04-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Calendar {
12 |
13 | /**
14 | Whether or not this calendar thinks that a certain date
15 | is the same day as another date.
16 | */
17 | func isDate(_ date1: Date, sameDayAs date2: Date) -> Bool {
18 | isDate(date1, equalTo: date2, toGranularity: .day)
19 | }
20 |
21 | /**
22 | Whether or not this calendar thinks that a certain date
23 | is the same month as another date.
24 | */
25 | func isDate(_ date1: Date, sameMonthAs date2: Date) -> Bool {
26 | isDate(date1, equalTo: date2, toGranularity: .month)
27 | }
28 |
29 | /**
30 | Whether or not this calendar thinks that a certain date
31 | is the same week as another date.
32 | */
33 | func isDate(_ date1: Date, sameWeekAs date2: Date) -> Bool {
34 | isDate(date1, equalTo: date2, toGranularity: .weekOfYear)
35 | }
36 |
37 | /**
38 | Whether or not this calendar thinks that a certain date
39 | is the same year as another date.
40 | */
41 | func isDate(_ date1: Date, sameYearAs date2: Date) -> Bool {
42 | isDate(date1, equalTo: date2, toGranularity: .year)
43 | }
44 |
45 | /**
46 | Whether or not this calendar thinks that a certain date
47 | is this month.
48 | */
49 | func isDateThisMonth(_ date: Date) -> Bool {
50 | isDate(date, sameMonthAs: Date())
51 | }
52 |
53 | /**
54 | Whether or not this calendar thinks that a certain date
55 | is this week.
56 | */
57 | func isDateThisWeek(_ date: Date) -> Bool {
58 | isDate(date, sameWeekAs: Date())
59 | }
60 |
61 | /**
62 | Whether or not this calendar thinks that a certain date
63 | is this year.
64 | */
65 | func isDateThisYear(_ date: Date) -> Bool {
66 | isDate(date, sameYearAs: Date())
67 | }
68 |
69 | /**
70 | Whether or not this calendar thinks that a certain date
71 | is today.
72 | */
73 | func isDateToday(_ date: Date) -> Bool {
74 | isDate(date, sameDayAs: Date())
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/Date+AddRemove.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Adding.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-05-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Date {
12 |
13 | /// Add a certain number days days to the date.
14 | func adding(days: Double) -> Date {
15 | let seconds = Double(days) * 60 * 60 * 24
16 | return addingTimeInterval(seconds)
17 | }
18 |
19 | /// Add a certain number hours days to the date.
20 | func adding(hours: Double) -> Date {
21 | let seconds = Double(hours) * 60 * 60
22 | return addingTimeInterval(seconds)
23 | }
24 |
25 | /// Add a certain number minutes days to the date.
26 | func adding(minutes: Double) -> Date {
27 | let seconds = Double(minutes) * 60
28 | return addingTimeInterval(seconds)
29 | }
30 |
31 | /// Add a certain number seconds days to the date.
32 | func adding(seconds: Double) -> Date {
33 | addingTimeInterval(Double(seconds))
34 | }
35 |
36 | /// Remove a certain number of days to the date.
37 | func removing(days: Double) -> Date {
38 | adding(days: -days)
39 | }
40 |
41 | /// Remove a certain number of hours to the date.
42 | func removing(hours: Double) -> Date {
43 | adding(hours: -hours)
44 | }
45 |
46 | /// Remove a certain number of minutes to the date.
47 | func removing(minutes: Double) -> Date {
48 | adding(minutes: -minutes)
49 | }
50 |
51 | /// Remove a certain number of seconds to the date.
52 | func removing(seconds: Double) -> Date {
53 | adding(seconds: -seconds)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/Date+Compare.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Compare.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-05-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | These extensions provide a semantic, more readable layer on
13 | top of the raw comparisons.
14 | */
15 | public extension Date {
16 |
17 | /// Whether or not the date occurs after another date.
18 | func isAfter(_ date: Date) -> Bool {
19 | self > date
20 | }
21 |
22 | /// Whether or not the date occurs before another date.
23 | func isBefore(_ date: Date) -> Bool {
24 | self < date
25 | }
26 |
27 | /// Whether or not the date is the same as another date.
28 | func isSame(as date: Date) -> Bool {
29 | self == date
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/Date+Components.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Components.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-03.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Date {
12 |
13 | /// Get the current day for the current calendar.
14 | var day: Int? { day() }
15 |
16 | /// Get the current hour for the current calendar.
17 | var hour: Int? { hour() }
18 |
19 | /// Get the current minute for the current calendar.
20 | var minute: Int? { minute() }
21 |
22 | /// Get the current month for the current calendar.
23 | var month: Int? { month() }
24 |
25 | /// Get the current second for the current calendar.
26 | var second: Int? { second() }
27 |
28 | /// Get the current year for the current calendar.
29 | var year: Int? { year() }
30 |
31 |
32 | /// Get the current day for the provided calendar.
33 | func day(for calendar: Calendar = .current) -> Int? {
34 | calendar.dateComponents([.day], from: self).day
35 | }
36 |
37 | /// Get the current hour for the provided calendar.
38 | func hour(for calendar: Calendar = .current) -> Int? {
39 | calendar.dateComponents([.hour], from: self).hour
40 | }
41 |
42 | /// Get the current minute for the provided calendar.
43 | func minute(for calendar: Calendar = .current) -> Int? {
44 | calendar.dateComponents([.minute], from: self).minute
45 | }
46 |
47 | /// Get the current month for the provided calendar.
48 | func month(for calendar: Calendar = .current) -> Int? {
49 | calendar.dateComponents([.month], from: self).month
50 | }
51 |
52 | /// Get the current second for the provided calendar.
53 | func second(for calendar: Calendar = .current) -> Int? {
54 | calendar.dateComponents([.second], from: self).second
55 | }
56 |
57 | /// Get the current year for the provided calendar.
58 | func year(for calendar: Calendar = .current) -> Int? {
59 | calendar.dateComponents([.year], from: self).year
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/Date+Difference.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Difference.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Date {
12 |
13 | /// The number of years between this and another date.
14 | func years(from date: Date, calendar: Calendar = .current) -> Int {
15 | calendar.dateComponents([.year], from: date, to: self).year ?? 0
16 | }
17 |
18 | /// The number of months between this and another date.
19 | func months(from date: Date, calendar: Calendar = .current) -> Int {
20 | calendar.dateComponents([.month], from: date, to: self).month ?? 0
21 | }
22 |
23 | /// The number of weeks between this and another date.
24 | func weeks(from date: Date, calendar: Calendar = .current) -> Int {
25 | calendar.dateComponents([.weekOfYear], from: date, to: self).weekOfYear ?? 0
26 | }
27 |
28 | /// The number of days between this and another date.
29 | func days(from date: Date, calendar: Calendar = .current) -> Int {
30 | calendar.dateComponents([.day], from: date, to: self).day ?? 0
31 | }
32 |
33 | /// The number of hours between this and another date.
34 | func hours(from date: Date, calendar: Calendar = .current) -> Int {
35 | calendar.dateComponents([.hour], from: date, to: self).hour ?? 0
36 | }
37 |
38 | /// The number of minutes between this and another date.
39 | func minutes(from date: Date, calendar: Calendar = .current) -> Int {
40 | calendar.dateComponents([.minute], from: date, to: self).minute ?? 0
41 | }
42 |
43 | /// The number of seconds between this and another date.
44 | func seconds(from date: Date, calendar: Calendar = .current) -> Int {
45 | calendar.dateComponents([.second], from: date, to: self).second ?? 0
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/Date+Init.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Init.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Date {
12 |
13 | /// Create a date value using the provided components.
14 | init?(
15 | year: Int,
16 | month: Int,
17 | day: Int,
18 | hour: Int = 0,
19 | minute: Int = 0,
20 | second: Int = 0,
21 | calendar: Calendar = .current) {
22 | let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second)
23 | guard let date = calendar.date(from: components) else {
24 | assertionFailure("Invalid date")
25 | return nil
26 | }
27 | self = date
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/DateDecoders.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateDecoders.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-09-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension JSONDecoder {
12 |
13 | /// Create a `JSONDecoder` that can decode ISO8601.
14 | static var iso8601: JSONDecoder {
15 | let decoder = JSONDecoder()
16 | decoder.dateDecodingStrategy = .robustISO8601
17 | return decoder
18 | }
19 | }
20 |
21 | private extension JSONDecoder.DateDecodingStrategy {
22 |
23 | /**
24 | This strategy can be used to parse ISO8601 dates. It is
25 | more robust than the standard strategy, and will try to
26 | parse both milliseconds and seconds.
27 | */
28 | static let robustISO8601 = custom { decoder throws -> Date in
29 | let container = try decoder.singleValueContainer()
30 | let string = try container.decode(String.self)
31 | let msFormatter = DateFormatter.iso8601Milliseconds
32 | let secFormatter = DateFormatter.iso8601Seconds
33 | if let date = msFormatter.date(from: string) ?? secFormatter.date(from: string) { return date }
34 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/DateEncoders.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateEncoders.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-09-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension JSONEncoder {
12 |
13 | /// Create a `JSONEncoder` that can encode ISO8601.
14 | static var iso8601: JSONEncoder {
15 | let decoder = JSONEncoder()
16 | decoder.dateEncodingStrategy = .customISO8601
17 | return decoder
18 | }
19 | }
20 |
21 | private extension JSONEncoder.DateEncodingStrategy {
22 |
23 | static let customISO8601 = custom { (date, encoder) throws -> Void in
24 | let formatter = DateFormatter.iso8601Milliseconds
25 | let string = formatter.string(from: date)
26 | var container = encoder.singleValueContainer()
27 | try container.encode(string)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Date/DateFormatter+Init.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateFormatter+Init.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-09-05.
6 | // Copyright © 2018 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension DateFormatter {
12 |
13 | /**
14 | Create a custom date formatter, that uses a custom date
15 | format, calendar, locale and time zone.
16 |
17 | - Parameters:
18 | - dateStyle: The date style to use.
19 | - timeStyle: The time style to use, by default `.none`.
20 | - locale: The locale to use, by default `en_US_POSIX`.
21 | - calendar: The calendar to use, by default `iso8601`.
22 | */
23 | convenience init(
24 | dateStyle: DateFormatter.Style,
25 | timeStyle: DateFormatter.Style = .none,
26 | locale: Locale = Locale(identifier: "en_US_POSIX"),
27 | calendar: Calendar = Calendar(identifier: .iso8601)
28 | ) {
29 | self.init()
30 | self.dateStyle = dateStyle
31 | self.timeStyle = timeStyle
32 | self.locale = locale
33 | self.calendar = calendar
34 | }
35 |
36 | /**
37 | Create a custom date formatter, that uses a custom date
38 | format, calendar, locale and time zone.
39 |
40 | - Parameters:
41 | - dateFormat: The date string format to use.
42 | - calendar: The calendar to use, by default `iso8601`.
43 | - locale: The locale to use, by default `en_US_POSIX`.
44 | - timeZone: The time zone to use, by default `GMT`.
45 | */
46 | convenience init(
47 | dateFormat: String,
48 | calendar: Calendar = Calendar(identifier: .iso8601),
49 | locale: Locale = Locale(identifier: "en_US_POSIX"),
50 | timeZone: TimeZone? = TimeZone(secondsFromGMT: 0)) {
51 | self.init()
52 | self.calendar = calendar
53 | self.locale = locale
54 | self.dateFormat = dateFormat
55 | self.timeZone = timeZone
56 | }
57 |
58 | /// Create an ISO8601 second date formatter.
59 | static var iso8601Seconds: DateFormatter {
60 | DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ssZ")
61 | }
62 |
63 | /// Create an ISO8601 ms date formatter.
64 | static var iso8601Milliseconds: DateFormatter {
65 | DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Device/DeviceIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeviceIdentifier.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by anything that can get a
13 | unique device identifier for the current device.
14 | */
15 | public protocol DeviceIdentifier: AnyObject {
16 |
17 | /// Get a unique device identifier.
18 | func getDeviceIdentifier() -> String
19 | }
20 |
21 | extension DeviceIdentifier {
22 |
23 | var key: String { "com.swiftkit.deviceidentifier" }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Device/KeychainBasedDeviceIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainBasedDeviceIdentifier.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This device identifier generates a unique device identifier
13 | and stores it in keychain, to make it possible to reuse the
14 | identifier, even if the app is uninstalled.
15 |
16 | The user default fallback maximizes the chance that the app
17 | can retrieve the identifier even if the keychain can not be
18 | read at the time of retrieval.
19 | */
20 | public class KeychainBasedDeviceIdentifier: DeviceIdentifier {
21 |
22 | public init(
23 | keychainService: KeychainService,
24 | backupIdentifier: DeviceIdentifier = UserDefaultsBasedDeviceIdentifier()) {
25 | self.keychainService = keychainService
26 | self.backupIdentifier = backupIdentifier
27 | }
28 |
29 | private let backupIdentifier: DeviceIdentifier
30 | private let keychainService: KeychainService
31 |
32 | /**
33 | Get a unique device identifier from the device keychain.
34 |
35 | If no identifier exists in the keychain, the identifier
36 | will use the provided `backupIdentifier` to generate an
37 | identifier, then persist that id in the device keychain.
38 | */
39 | public func getDeviceIdentifier() -> String {
40 | if let id = keychainService.string(for: key, with: nil) { return id }
41 | let id = backupIdentifier.getDeviceIdentifier()
42 | keychainService.set(id, for: key, with: nil)
43 | return id
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Device/UserDefaultsBasedDeviceIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsBasedDeviceIdentifier.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This device identifier generates a unique device identifier
13 | and stores it in user defaults, so that the same identifier
14 | is used every time for each app installation.
15 |
16 | If you want to use the same identifier between app installs,
17 | use a ``KeychainBasedDeviceIdentifier``.
18 | */
19 | public class UserDefaultsBasedDeviceIdentifier: DeviceIdentifier {
20 |
21 | public init(defaults: UserDefaults = .standard) {
22 | self.defaults = defaults
23 | }
24 |
25 | private let defaults: UserDefaults
26 |
27 | /**
28 | Get a unique device identifier from the user defaults.
29 |
30 | If no persisted identifier exists, this identifier will
31 | generate a new identifier, then persist and return that
32 | identifier.
33 | */
34 | public func getDeviceIdentifier() -> String {
35 | if let id = defaults.string(forKey: key) { return id }
36 | return generateDeviceIdentifier()
37 | }
38 | }
39 |
40 | private extension UserDefaultsBasedDeviceIdentifier {
41 |
42 | func generateDeviceIdentifier() -> String {
43 | let id = UUID().uuidString
44 | defaults.set(id, forKey: key)
45 | return id
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Collections/Array+Range.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+RemoveObject.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-06-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Array where Element: Comparable & Strideable {
12 |
13 | /**
14 | Create an array using a set of values from the provided
15 | `range`, stepping `stepSize` between each value.
16 | */
17 | init(_ range: ClosedRange, stepSize: Element.Stride) {
18 | self = Array(stride(from: range.lowerBound, through: range.upperBound, by: stepSize))
19 | }
20 | }
21 |
22 | public extension Array where Element == Double {
23 |
24 | /**
25 | Create an array using a set of values from the provided
26 | `range`, stepping `stepSize` between each value.
27 | */
28 | init(_ range: ClosedRange, stepSize: Element.Stride) {
29 | self = Array(stride(from: range.lowerBound, through: range.upperBound, by: stepSize))
30 | .map { $0.roundedWithPrecision(from: stepSize) }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Collections/Collection+Content.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+Content.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Collection {
12 |
13 | /// Check whether or not the collection has any elements.
14 | var hasContent: Bool { !isEmpty }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Collections/Collection+Distinct.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+HasContent.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Collection where Element: Hashable {
12 |
13 | /**
14 | Get distinct values from the collection, preserving the
15 | original order.
16 | */
17 | func distinct() -> [Element] {
18 | reduce([]) { $0.contains($1) ? $0 : $0 + [$1] }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Collections/Sequence+Batched.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sequence+Batch.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2017-05-10.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Sequence {
12 |
13 | /// Batch the sequence into groups of a certain size.
14 | func batched(withBatchSize size: Int) -> [[Element]] {
15 | var result: [[Element]] = []
16 | var batch: [Element] = []
17 |
18 | forEach {
19 | batch.append($0)
20 | if batch.count == size {
21 | result.append(batch)
22 | batch = []
23 | }
24 | }
25 |
26 | if !batch.isEmpty {
27 | result.append(batch)
28 | }
29 |
30 | return result
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Collections/Sequence+Grouped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sequence+Group.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-04.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Sequence {
12 |
13 | /**
14 | Group the sequence into a dictionary using any property
15 | from the sequence item type.
16 | */
17 | func grouped(by grouper: (Element) -> T) -> [T: [Element]] {
18 | Dictionary(grouping: self, by: grouper)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Comparable+Closest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+Closest.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum PreferredClosestValue {
12 |
13 | case greater, smaller
14 | }
15 |
16 | public extension Comparable {
17 |
18 | /// Get the closest value in the provided `collection`.
19 | func closest(in collection: [Self], preferred: PreferredClosestValue) -> Self? {
20 | if collection.contains(self) { return self }
21 | let sorted = collection.sorted()
22 | let greater = sorted.first { $0 > self }
23 | let smaller = sorted.last { $0 < self }
24 | switch preferred {
25 | case .greater: return greater ?? smaller
26 | case .smaller: return smaller ?? greater
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Comparable+Limit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+Limit.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2018-10-04.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Comparable {
12 |
13 | /**
14 | Limit the value to a closed range.
15 | */
16 | mutating func limit(to range: ClosedRange) {
17 | self = limited(to: range)
18 | }
19 |
20 | /**
21 | Return the value limited to a closed range.
22 |
23 | This could be implemented in a oneliner, but that would
24 | make the code less readable.
25 | */
26 | func limited(to range: ClosedRange) -> Self {
27 | if self < range.lowerBound { return range.lowerBound }
28 | if self > range.upperBound { return range.upperBound }
29 | return self
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/ComparisonResult+Shortcuts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComparisonResult+Shortcuts.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension ComparisonResult {
12 |
13 | /// This is an `.orderedAscending` shorthand.
14 | static var ascending: ComparisonResult {
15 | .orderedAscending
16 | }
17 |
18 | /// This is an `.orderedDescending` shorthand.
19 | static var descending: ComparisonResult {
20 | .orderedDescending
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/DispatchQueue+Async.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DispatchQueue+Async.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-02.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // https://danielsaidi.com/blog/2020/06/03/dispatch-queue
9 | //
10 |
11 | import Foundation
12 |
13 | public extension DispatchQueue {
14 |
15 | /// Perform an operation after a certain time interval.
16 | func asyncAfter(
17 | _ interval: DispatchTimeInterval,
18 | execute: @escaping () -> Void
19 | ) {
20 | asyncAfter(
21 | deadline: .now() + interval,
22 | execute: execute)
23 | }
24 |
25 | /// Perform an operation after a certain time interval.
26 | func asyncAfter(
27 | seconds: TimeInterval,
28 | execute: @escaping () -> Void
29 | ) {
30 | let milli = Int(seconds * 1000)
31 | asyncAfter(.milliseconds(milli), execute: execute)
32 | }
33 |
34 | /**
35 | Perform an async operation then call a completion block
36 | on another queue (default `.main`) with the result from
37 | the async operation being passed on.
38 | */
39 | func async(
40 | execute: @escaping () -> T,
41 | then completion: @escaping (T) -> Void,
42 | on completionQueue: DispatchQueue = .main) {
43 | async {
44 | let result = execute()
45 | completionQueue.async {
46 | completion(result)
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/DispatchQueue+Throttle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DispatchQueue+Throttle.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-09-17.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | private var lastDebounceCallTimes = [AnyHashable: DispatchTime]()
12 | private let nilContext: AnyHashable = Int.random(in: 0...100_000)
13 | private var throttleWorkItems = [AnyHashable: DispatchWorkItem]()
14 |
15 | public extension DispatchQueue {
16 |
17 | /**
18 | Try to perform a debounced operation.
19 |
20 | Executes a closure and ensures that no other executions
21 | will be made during the provided `interval`.
22 |
23 | - parameters:
24 | - interval: The time to delay a closure execution, in seconds
25 | - context: The context in which the debounce should be executed
26 | - action: The closure to be executed
27 | */
28 | func debounce(interval: Double, context: AnyHashable? = nil, action: @escaping () -> Void) {
29 | let worker = DispatchWorkItem {
30 | defer { throttleWorkItems.removeValue(forKey: context ?? nilContext) }
31 | action()
32 | }
33 |
34 | asyncAfter(deadline: .now() + interval, execute: worker)
35 | throttleWorkItems[context ?? nilContext]?.cancel()
36 | throttleWorkItems[context ?? nilContext] = worker
37 | }
38 |
39 | /**
40 | Try to perform a throttled operation.
41 |
42 | Performs the first performed operation, then delays any
43 | further operations until the provided `interval` passes.
44 |
45 | - parameters:
46 | - interval: The time to delay a closure execution, in seconds
47 | - context: The context in which the throttle should be executed
48 | - action: The closure to be executed
49 | */
50 | func throttle(interval: Double, context: AnyHashable? = nil, action: @escaping () -> Void) {
51 | if let last = lastDebounceCallTimes[context ?? nilContext], last + interval > .now() {
52 | return
53 | }
54 |
55 | lastDebounceCallTimes[context ?? nilContext] = .now()
56 | async(execute: action)
57 | debounce(interval: interval) {
58 | lastDebounceCallTimes.removeValue(forKey: context ?? nilContext)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/NSAttributedString/NSAttributedString+Archive.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Archive.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-22.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension NSAttributedString {
12 |
13 | /**
14 | Try to create an attributed string with `data` that was
15 | created with an `NSKeyedArchiver`.
16 | */
17 | convenience init?(keyedArchiveData data: Data) throws {
18 | let res = try NSKeyedUnarchiver.unarchivedObject(
19 | ofClass: NSAttributedString.self,
20 | from: data)
21 | guard let string = res else { return nil }
22 | self.init(attributedString: string)
23 | }
24 |
25 | /**
26 | Try to generate `NSKeyedArchiver` data from the string.
27 | */
28 | func getKeyedArchiveData() throws -> Data {
29 | try NSKeyedArchiver.archivedData(
30 | withRootObject: self,
31 | requiringSecureCoding: false)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/NSAttributedString/NSAttributedString+Rtf.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Rtf.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-22.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension NSAttributedString {
12 |
13 | /**
14 | Try to create an attributed string with `data` that has
15 | RTF formatted string content.
16 |
17 | This extension aims to simplify the chore of creating a
18 | proper attributed string from RTF data, since the Swift
19 | api:s are old and requires a lot or bridging.
20 | */
21 | convenience init(rtfData data: Data) throws {
22 | let docTypeKey = NSAttributedString.DocumentReadingOptionKey.documentType
23 | let rtfDocument = NSAttributedString.DocumentType.rtf
24 | var attributes = [docTypeKey: rtfDocument] as NSDictionary?
25 | try self.init(
26 | data: data,
27 | options: [.characterEncoding: String.Encoding.utf8.rawValue],
28 | documentAttributes: &attributes)
29 | }
30 |
31 | /**
32 | Try to generate RTF data from the attributed string.
33 | */
34 | func getRtfData() throws -> Data {
35 | let docTypeKey = NSAttributedString.DocumentAttributeKey.documentType
36 | let rtfDocument = NSAttributedString.DocumentType.rtf
37 | let attributes = [docTypeKey: rtfDocument]
38 | let data = try data(
39 | from: NSRange(location: 0, length: length),
40 | documentAttributes: attributes)
41 | return data
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/NSAttributedString/NSAttributedString+Text.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Text.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-22.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension NSAttributedString {
12 |
13 | /**
14 | This error can be thrown by `getPlainTextData()`.
15 | */
16 | enum PlainTextError: Error {
17 |
18 | case invalidPlainTextData(inString: String)
19 | }
20 |
21 | /**
22 | Try to create an attributed string with `data` that has
23 | plain, .utf8 encoded string content.
24 | */
25 | convenience init?(plainTextData data: Data) throws {
26 | let decoded = String(data: data, encoding: .utf8)
27 | guard let string = decoded else { return nil }
28 | let attributed = NSAttributedString(string: string)
29 | self.init(attributedString: attributed)
30 | }
31 |
32 | /**
33 | Try to generate plain text data from the string.
34 | */
35 | func getPlainTextData() throws -> Data {
36 | guard let data = string.data(using: .utf8) else {
37 | throw PlainTextError
38 | .invalidPlainTextData(inString: string)
39 | }
40 | return data
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Optional+IsSet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Optional+IsSet.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Optional {
12 |
13 | /// Whether or not the value is `nil`.
14 | var isNil: Bool { self == nil }
15 |
16 | /// Whether or not the value is set and not `nil`.
17 | var isSet: Bool { self != nil }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Base64.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Base64.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-12-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // https://danielsaidi.com/blog/2020/06/04/string-base64
9 | //
10 |
11 | import Foundation
12 |
13 | public extension String {
14 |
15 | /// Base64 decode the string.
16 | func base64Decoded() -> String? {
17 | guard let data = Data(base64Encoded: self) else { return nil }
18 | return String(data: data, encoding: .utf8)
19 | }
20 |
21 | /// Base64 encode the string.
22 | func base64Encoded() -> String? {
23 | data(using: .utf8)?.base64EncodedString()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Bool.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Bool.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-03.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /**
14 | Parse the potential bool value in the string.
15 |
16 | This function handles 1/0, yes/no, YES/NO etc., so it's
17 | a good alternative to use e.g. when parsing plist files.
18 | */
19 | var boolValue: Bool { (self as NSString).boolValue }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Capitalize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Capitalize.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-01-11.
6 | // Copyright © 2022 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /// Return a copy where the first letter is capitalized.
14 | func capitalizingFirstLetter() -> String {
15 | prefix(1).capitalized + dropFirst()
16 | }
17 |
18 | /// Capitalize the first letter in the string.
19 | mutating func capitalizeFirstLetter() {
20 | self = self.capitalizingFirstLetter()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Characters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Characters.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String.Element {
12 |
13 | static var carriageReturn: String.Element { "\r" }
14 | static var newLine: String.Element { "\n" }
15 | static var tab: String.Element { "\t" }
16 | }
17 |
18 |
19 | public extension String {
20 |
21 | static let newLine = String(.newLine)
22 | static let tab = String(.tab)
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Contains.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Contains.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-02-17.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // https://danielsaidi.com/blog/2020/06/04/string-contains
9 | //
10 |
11 | import Foundation
12 |
13 | public extension String {
14 |
15 | /// Check if this string contains another string.
16 | func contains(_ string: String, caseSensitive: Bool) -> Bool {
17 | caseSensitive
18 | ? contains(string)
19 | : range(of: string, options: .caseInsensitive) != nil
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Content.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Content.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /// Check if this string has any content.
14 | var hasContent: Bool {
15 | !isEmpty
16 | }
17 |
18 | /// Check if this string has any content after trimming.
19 | var hasTrimmedContent: Bool {
20 | !trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Dictation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Dictation.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-11-14.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /**
14 | This function cleans up space and other characters that
15 | can be added to the string during dictation.
16 |
17 | This happens on the Apple TV, when a user uses a remote
18 | to dictate text into a text field.
19 | */
20 | func cleanedUpAfterDictation() -> String {
21 | self
22 | .replacingOccurrences(of: "\u{fffc}", with: "")
23 | .trimmingCharacters(in: .whitespaces)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Paragraph.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Paragraph.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /**
14 | Backs to find the index of the first new line paragraph
15 | before the provided location, if any.
16 |
17 | A new paragraph is considered to start at the character
18 | after the newline char, not the newline itself.
19 | */
20 | func findIndexOfCurrentParagraph(from location: UInt) -> UInt {
21 | if isEmpty { return 0 }
22 | let count = UInt(count)
23 | var index = min(location, count-1)
24 | repeat {
25 | guard index > 0, index < count else { break }
26 | guard let char = character(at: index - 1) else { break }
27 | if char == .newLine || char == .carriageReturn { break }
28 | index -= 1
29 | } while true
30 | return max(index, 0)
31 | }
32 |
33 | /**
34 | Looks forward to find the next new line paragraph after
35 | the provided location, if any.
36 |
37 | A new paragraph is considered to start at the character
38 | after the newline char, not the newline itself.
39 | */
40 | func findIndexOfNextParagraph(from location: UInt) -> UInt {
41 | var index = location
42 | repeat {
43 | guard let char = character(at: index) else { break }
44 | index += 1
45 | guard index < count else { break }
46 | if char == .newLine || char == .carriageReturn { break }
47 | } while true
48 | let found = index < count
49 | return found ? index : findIndexOfCurrentParagraph(from: location)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Replace.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Replace.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-01-08.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // Read more here:
9 | // https://danielsaidi.com/blog/2020/06/04/string-replace
10 | //
11 |
12 | import Foundation
13 |
14 | public extension String {
15 |
16 | /// This is a `replacingOccurrences(of:with:)` shorthand.
17 | func replacing(_ string: String, with: String) -> String {
18 | replacingOccurrences(of: string, with: with)
19 | }
20 |
21 | /// This is a `replacingOccurrences(of:with:)` shorthand.
22 | func replacing(_ string: String, with: String, caseSensitive: Bool) -> String {
23 | caseSensitive
24 | ? replacing(string, with: with)
25 | : replacingOccurrences(of: string, with: with, options: .caseInsensitive)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Split.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Split.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-08-23.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /// Split the string using a list of separators.
14 | func split(by separators: [String]) -> [String] {
15 | let separators = CharacterSet(charactersIn: separators.joined())
16 | return components(separatedBy: separators)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Subscript.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Subscript.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This extension makes it possible to fetch characters from a
13 | string, as discussed here:
14 |
15 | https://stackoverflow.com/questions/24092884/get-nth-character-of-a-string-in-swift-programming-language
16 | */
17 | public extension StringProtocol {
18 |
19 | func character(at index: Int) -> String.Element? {
20 | guard count > index else { return nil }
21 | return self[index]
22 | }
23 |
24 | func character(at index: UInt) -> String.Element? {
25 | character(at: Int(index))
26 | }
27 |
28 | subscript(_ offset: Int) -> Element {
29 | self[index(startIndex, offsetBy: offset)]
30 | }
31 |
32 | subscript(_ range: Range) -> SubSequence {
33 | prefix(range.lowerBound+range.count).suffix(range.count)
34 | }
35 |
36 | subscript(_ range: ClosedRange) -> SubSequence {
37 | prefix(range.lowerBound+range.count).suffix(range.count)
38 | }
39 |
40 | subscript(_ range: PartialRangeThrough) -> SubSequence {
41 | prefix(range.upperBound.advanced(by: 1))
42 | }
43 |
44 | subscript(_ range: PartialRangeUpTo) -> SubSequence {
45 | prefix(range.upperBound)
46 | }
47 |
48 | subscript(_ range: PartialRangeFrom) -> SubSequence {
49 | suffix(Swift.max(0, count-range.lowerBound))
50 | }
51 | }
52 |
53 | private extension LosslessStringConvertible {
54 |
55 | var string: String { .init(self) }
56 | }
57 |
58 | private extension BidirectionalCollection {
59 |
60 | subscript(safe offset: Int) -> Element? {
61 | if isEmpty { return nil }
62 | guard let index = index(
63 | startIndex,
64 | offsetBy: offset,
65 | limitedBy: index(before: endIndex))
66 | else { return nil }
67 | return self[index]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+Trimmed.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Trimmed.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-11-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension String {
12 |
13 | /// This is a `trimmingCharacters(in:)` shorthand.
14 | func trimmed(
15 | for set: CharacterSet = .whitespacesAndNewlines
16 | ) -> String {
17 | self.trimmingCharacters(in: set)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/String/String+UrlEncode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+UrlEncode.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-12-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // https://danielsaidi.com/blog/2020/06/04/string-urlencode
9 | //
10 |
11 | import Foundation
12 |
13 | public extension String {
14 |
15 | /**
16 | Encode the string to work with `x-www-form-urlencoded`.
17 |
18 | This will first call `urlEncoded()`, then replace every
19 | `+` with `%2B`.
20 | */
21 | func formEncoded() -> String? {
22 | self.urlEncoded()?
23 | .replacingOccurrences(of: "+", with: "%2B")
24 | }
25 |
26 | /**
27 | Encode the string to work with quary parameters.
28 |
29 | This will first call `addingPercentEncoding`, using the
30 | `.urlPathAllowed` character set, then replace every `&`
31 | with `%26`.
32 | */
33 | func urlEncoded() -> String? {
34 | self.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)?
35 | .replacingOccurrences(of: "&", with: "%26")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/Url+Global.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Url+Global.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-31.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension URL {
12 |
13 | /**
14 | This url leads to the App Store page for a certain app.
15 | */
16 | static func appStoreUrl(forAppId appId: Int) -> URL? {
17 | URL(string: "https://itunes.apple.com/app/id\(appId)")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Extensions/UserDefaults+Codable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+Codable.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-09-23.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension UserDefaults {
12 |
13 | /// Try to decode a certain key to a decodable type.
14 | func codable(forKey key: String) -> T? {
15 | guard let data = object(forKey: key) as? Data else { return nil }
16 | let value = try? JSONDecoder().decode(T.self, from: data)
17 | return value
18 | }
19 |
20 | /// Persist a codable item.
21 | func setCodable(_ codable: T, forKey key: String) {
22 | let data = try? JSONEncoder().encode(codable)
23 | set(data, forKey: key)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Files/BundleFileFinder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | This class can be used to find files witin a certain bundle.
5 | */
6 | public class BundleFileFinder: FileFinder {
7 |
8 | public init(
9 | bundle: Bundle = .main
10 | ) {
11 | self.bundle = bundle
12 | }
13 |
14 | private let bundle: Bundle
15 |
16 | /// Find files names that start with a certain prefix.
17 | public func findFilesWithFileNamePrefix(_ prefix: String) -> [String] {
18 | let format = "self BEGINSWITH %@"
19 | let predicate = NSPredicate(format: format, argumentArray: [prefix])
20 | return findFilesWithPredicate(predicate)
21 | }
22 |
23 | /// Find files names that end with a certain suffix.
24 | public func findFilesWithFileNameSuffix(_ suffix: String) -> [String] {
25 | let format = "self ENDSWITH %@"
26 | let predicate = NSPredicate(format: format, argumentArray: [suffix])
27 | return findFilesWithPredicate(predicate)
28 | }
29 | }
30 |
31 | private extension BundleFileFinder {
32 |
33 | func findFilesWithPredicate(_ predicate: NSPredicate) -> [String] {
34 | do {
35 | let path = bundle.bundlePath
36 | let fileManager = FileManager.default
37 | let files = try fileManager.contentsOfDirectory(atPath: path)
38 | let array = files as NSArray
39 | let filteredFiles = array.filtered(using: predicate)
40 | return filteredFiles as? [String] ?? []
41 | } catch {
42 | return [String]()
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Files/DirectoryService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileDirectoryService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-12-19.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 |
8 | import Foundation
9 |
10 | /**
11 | This service can be implemented by classes that can be used
12 | to handle files within a certain local file directory.
13 | */
14 | public protocol DirectoryService: AnyObject {
15 |
16 | var directoryUrl: URL { get }
17 |
18 | func createFile(named name: String, contents: Data?) -> Bool
19 | func fileExists(withName name: String) -> Bool
20 | func getAttributesForFile(named name: String) -> [FileAttributeKey: Any]?
21 | func getFileNames() -> [String]
22 | func getFileNames(matching fileNamePatterns: [String]) -> [String]
23 | func getSizeOfAllFiles() -> UInt64
24 | func getSizeOfFile(named name: String) -> UInt64?
25 | func getUrlForFile(named name: String) -> URL?
26 | func getUrlsForAllFiles() -> [URL]
27 | func removeFile(named name: String) throws
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Files/FileFinder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | This protocol can be implemented by types that can look for
5 | files in various ways.
6 | */
7 | public protocol FileFinder {
8 |
9 | /**
10 | Find files with names that start with a certain prefix.
11 | */
12 | func findFilesWithFileNamePrefix(_ prefix: String) -> [String]
13 |
14 | /**
15 | Find files with names that end with a certain suffix.
16 | */
17 | func findFilesWithFileNameSuffix(_ suffix: String) -> [String]
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Files/FileManager+UniqueFileName.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileManager+UniqueFileName.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-01-18.
6 | // Copyright © 2022 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension FileManager {
12 |
13 | /**
14 | Get a unique destination for a certain destination file
15 | URL, to ensure that no existing files are replaced.
16 |
17 | For instance, if you have a destination url, and a file
18 | already exists at that url, this function will add `-1`
19 | to the file name and check if such a file exists. If it
20 | doesn't the function will return the new url, otherwise
21 | try with `-2`, `-3` etc. until no file exists.
22 | */
23 | func getUniqueDestinationUrl(
24 | for destinationUrl: URL,
25 | separator: String = "-") -> URL {
26 | if !fileExists(atPath: destinationUrl.path) { return destinationUrl }
27 | let fileExtension = destinationUrl.pathExtension
28 | let noExtension = destinationUrl.deletingPathExtension()
29 | let fileName = noExtension.lastPathComponent
30 | var counter = 1
31 | repeat {
32 | let newUrl = noExtension
33 | .deletingLastPathComponent()
34 | .appendingPathComponent(fileName.appending("\(separator)\(counter)"))
35 | .appendingPathExtension(fileExtension)
36 | if !fileExists(atPath: newUrl.path) { return newUrl }
37 | counter += 1
38 | } while true
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Files/StandardDirectoryService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardFileDirectoryService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-12-19.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This is a standard implementation of the `DirectoryService`.
13 | You can inherit and override any parts of it.
14 | */
15 | open class StandardDirectoryService: DirectoryService {
16 |
17 |
18 | // MARK: - Initialization
19 |
20 | public init?(
21 | directory: FileManager.SearchPathDirectory,
22 | fileManager: FileManager = .default
23 | ) {
24 | guard let dir = fileManager.urls(for: directory, in: .userDomainMask).last else { return nil }
25 | self.directoryUrl = dir
26 | self.fileManager = fileManager
27 | }
28 |
29 | public init(
30 | fileManager: FileManager = .default,
31 | directoryUrl: URL
32 | ) {
33 | self.directoryUrl = directoryUrl
34 | self.fileManager = fileManager
35 | }
36 |
37 |
38 | // MARK: - Properties
39 |
40 | public let directoryUrl: URL
41 | private let fileManager: FileManager
42 |
43 |
44 | // MARK: - Public Functions
45 |
46 | open func createFile(named name: String, contents: Data?) -> Bool {
47 | let url = directoryUrl.appendingPathComponent(name)
48 | return fileManager.createFile(atPath: url.path, contents: contents, attributes: nil)
49 | }
50 |
51 | open func fileExists(withName name: String) -> Bool {
52 | getUrlForFile(named: name) != nil
53 | }
54 |
55 | open func getAttributesForFile(named name: String) -> [FileAttributeKey: Any]? {
56 | guard let url = getUrlForFile(named: name) else { return nil }
57 | return try? fileManager.attributesOfItem(atPath: url.path)
58 | }
59 |
60 | open func getFileNames() -> [String] {
61 | guard let urls = try? fileManager.contentsOfDirectory(at: directoryUrl, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { return [] }
62 | return urls.map { $0.lastPathComponent }
63 | }
64 |
65 | open func getFileNames(matching fileNamePatterns: [String]) -> [String] {
66 | let patterns = fileNamePatterns.map { $0.lowercased() }
67 | return getFileNames().filter {
68 | let fileName = $0.lowercased()
69 | return patterns.filter { fileName.contains($0) }.first != nil
70 | }
71 | }
72 |
73 | open func getSizeOfAllFiles() -> UInt64 {
74 | getFileNames().reduce(0) { $0 + (getSizeOfFile(named: $1) ?? 0) }
75 | }
76 |
77 | open func getSizeOfFile(named name: String) -> UInt64? {
78 | guard let attributes = getAttributesForFile(named: name) else { return nil }
79 | let number = attributes[FileAttributeKey.size] as? NSNumber
80 | return number?.uint64Value
81 | }
82 |
83 | open func getUrlForFile(named name: String) -> URL? {
84 | let urls = try? fileManager.contentsOfDirectory(at: directoryUrl, includingPropertiesForKeys: nil)
85 | return urls?.first { $0.lastPathComponent == name }
86 | }
87 |
88 | open func getUrlsForAllFiles() -> [URL] {
89 | getFileNames().compactMap {
90 | getUrlForFile(named: $0)
91 | }
92 | }
93 |
94 | open func removeFile(named name: String) throws {
95 | guard let url = getUrlForFile(named: name) else { return }
96 | try fileManager.removeItem(at: url)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Geo/CLLocationCoordinate2D+Equatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLLocationCoordinate2D+Equatable.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-09-08.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | extension CLLocationCoordinate2D: Equatable {
12 |
13 | public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
14 | lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Geo/CLLocationCoordinate2D+Map.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLLocationCoordinate2D+Map.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2023-08-17.
6 | // Copyright © 2023 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import MapKit
11 |
12 | public extension CLLocationCoordinate2D {
13 |
14 | /// Create a map coordinate region.
15 | func mapCoordinateRegion(
16 | withSpanInMeters meters: Double
17 | ) -> MKCoordinateRegion {
18 | .init(
19 | center: self,
20 | latitudinalMeters: meters,
21 | longitudinalMeters: meters
22 | )
23 | }
24 |
25 | /// Create a map item with a certain name.
26 | func mapItem(
27 | withName name: String
28 | ) -> MKMapItem {
29 | let mapItem = MKMapItem(placemark: mapPlacemark())
30 | mapItem.name = name
31 | return mapItem
32 | }
33 |
34 | /// Create a map placemark.
35 | func mapPlacemark() -> MKPlacemark {
36 | .init(
37 | coordinate: self,
38 | addressDictionary: nil
39 | )
40 | }
41 |
42 | #if os(iOS) || os(macOS) || os(watchOS)
43 | /// Create launch options for the external Maps app.
44 | func mapsLaunchOptions(
45 | withRegionSpanInMeters meters: Double
46 | ) -> [String: Any] {
47 | let region = mapCoordinateRegion(withSpanInMeters: meters)
48 | return [
49 | MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: region.center),
50 | MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: region.span)
51 | ]
52 | }
53 |
54 | /// Open the coordinate in Maps
55 | func openInMaps(
56 | withName name: String,
57 | regionSpanInMeters meters: Double = 1_000
58 | ) {
59 | let item = mapItem(withName: name)
60 | let options = mapsLaunchOptions(withRegionSpanInMeters: meters)
61 | item.openInMaps(launchOptions: options)
62 | }
63 | #endif
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Geo/CLLocationCoordinate2D+Valid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLLocationCoordinate2D+Valid.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-09-18.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | public extension CLLocationCoordinate2D {
12 |
13 | /**
14 | Check if the coordinate is valid. This is a best effort
15 | check that both lat and long are not or any extremes.
16 | */
17 | var isValid: Bool {
18 | isValid(latitude) && isValid(longitude)
19 | }
20 | }
21 |
22 | private extension CLLocationCoordinate2D {
23 |
24 | func isValid(_ degrees: CLLocationDegrees) -> Bool {
25 | degrees != 0 && degrees != 180 && degrees != -180
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Geo/WorldCoordinate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorldCoordinate.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-10-04.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | /**
12 | This struct can be used to represent world coordinates, but
13 | without bloating `CLLocationCoordinate2D` with static props.
14 |
15 | The reason to why this is a struct and not an enum, is that
16 | it simplifies extending it with new coordinates in any apps
17 | that use custom coordinates.
18 | */
19 | public struct WorldCoordinate: Hashable, Equatable, Identifiable {
20 |
21 | public var id: String { name }
22 |
23 | /**
24 | The name of the coordinate.
25 | */
26 | public let name: String
27 |
28 | /**
29 | The coordinate value.
30 | */
31 | public let coordinate: CLLocationCoordinate2D
32 |
33 | public func hash(into hasher: inout Hasher) {
34 | hasher.combine(id)
35 | }
36 | }
37 |
38 | public extension WorldCoordinate {
39 |
40 | static var manhattan: WorldCoordinate = .init(name: "Manhattan", coordinate: CLLocationCoordinate2D(latitude: 40.7590615, longitude: -73.969231))
41 | static var newYork: WorldCoordinate = .init(name: "New York", coordinate: CLLocationCoordinate2D(latitude: 40.7033127, longitude: -73.979681))
42 | static var sanFrancisco: WorldCoordinate = .init(name: "San Francisco", coordinate: CLLocationCoordinate2D(latitude: 37.7796828, longitude: -122.4000062))
43 | static var tokyo: WorldCoordinate = .init(name: "Tokyo", coordinate: CLLocationCoordinate2D(latitude: 35.673, longitude: 139.710))
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Keychain/KeychainItemAccessibility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainItemAccessibility.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // Based on https://github.com/jrendel/SwiftKeychainWrapper
9 | // Created by James Blair on 4/24/16.
10 | // Copyright © 2016 Jason Rendel. All rights reserved.
11 |
12 | import Foundation
13 |
14 |
15 | public protocol KeychainAttrRepresentable {
16 |
17 | var keychainAttrValue: CFString { get }
18 | }
19 |
20 | /**
21 | This enum defines the various access scopes that a keychain
22 | item can use. The names follow certain conventions that are
23 | defined in the list below:
24 |
25 | * `afterFirstUnlock`
26 | The attribute cannot be accessed after a restart, until the
27 | device has been unlocked once by the user. After this first
28 | unlock, the items remains accessible until the next restart.
29 | This is recommended for items that must be available to any
30 | background applications or processes.
31 |
32 | * `ThisDeviceOnly`
33 | The attribute will not be included in encrypted backup, and
34 | are thus not available after restoring apps from backups on
35 | a different device.
36 |
37 | * `whenPasscodeSet`
38 | The attribute can only be accessed when the device has been
39 | unlocked by the user and a device passcode is set. No items
40 | can be stored on device if a passcode is not set. Disabling
41 | the passcode will delete all items.
42 |
43 | * `whenUnlocked`
44 | The attribute can only be accessed when the device has been
45 | unlocked by the user. This is recommended for items that we
46 | only mean to use when the application is active.
47 | */
48 | public enum KeychainItemAccessibility {
49 |
50 | case afterFirstUnlock
51 | case afterFirstUnlockThisDeviceOnly
52 | case whenPasscodeSetThisDeviceOnly
53 | case whenUnlocked
54 | case whenUnlockedThisDeviceOnly
55 |
56 | static func accessibilityForAttributeValue(_ keychainAttrValue: CFString) -> KeychainItemAccessibility? {
57 | keychainItemAccessibilityLookup.first { $0.value == keychainAttrValue }?.key
58 | }
59 | }
60 |
61 |
62 | private let keychainItemAccessibilityLookup: [KeychainItemAccessibility: CFString] = [
63 | .afterFirstUnlock: kSecAttrAccessibleAfterFirstUnlock,
64 | .afterFirstUnlockThisDeviceOnly: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
65 | .whenPasscodeSetThisDeviceOnly: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
66 | .whenUnlocked: kSecAttrAccessibleWhenUnlocked,
67 | .whenUnlockedThisDeviceOnly: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
68 | ]
69 |
70 | extension KeychainItemAccessibility: KeychainAttrRepresentable {
71 |
72 | public var keychainAttrValue: CFString {
73 | keychainItemAccessibilityLookup[self]!
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Keychain/KeychainReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainReader.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by keychain-based services
13 | that can read from the device keychain.
14 | */
15 | public protocol KeychainReader: AnyObject {
16 |
17 | func accessibility(for key: String) -> KeychainItemAccessibility?
18 | func bool(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool?
19 | func data(for key: String, with accessibility: KeychainItemAccessibility?) -> Data?
20 | func dataRef(for key: String, with accessibility: KeychainItemAccessibility?) -> Data?
21 | func double(for key: String, with accessibility: KeychainItemAccessibility?) -> Double?
22 | func float(for key: String, with accessibility: KeychainItemAccessibility?) -> Float?
23 | func hasValue(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
24 | func integer(for key: String, with accessibility: KeychainItemAccessibility?) -> Int?
25 | func string(for key: String, with accessibility: KeychainItemAccessibility?) -> String?
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Keychain/KeychainService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by keychain-based services
13 | that can read from and write to the device keychain.
14 | */
15 | public protocol KeychainService: KeychainReader, KeychainWriter {}
16 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Keychain/KeychainWriter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainWriter.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by keychain-based services
13 | that can write to the user's keychain.
14 | */
15 | public protocol KeychainWriter: AnyObject {
16 |
17 | @discardableResult
18 | func removeObject(for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
19 |
20 | @discardableResult
21 | func removeAllKeys() -> Bool
22 |
23 | @discardableResult
24 | func set(_ value: Bool, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
25 |
26 | @discardableResult
27 | func set(_ value: Data, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
28 |
29 | @discardableResult
30 | func set(_ value: Double, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
31 |
32 | @discardableResult
33 | func set(_ value: Float, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
34 |
35 | @discardableResult
36 | func set(_ value: Int, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
37 |
38 | @discardableResult
39 | func set(_ value: NSCoding, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
40 |
41 | @discardableResult
42 | func set(_ value: String, for key: String, with accessibility: KeychainItemAccessibility?) -> Bool
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Localization/BundleTranslator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BundleTranslator.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-04-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This `Translator` translates keys using a certain `Bundle`.
13 | */
14 | public class BundleTranslator: Translator {
15 |
16 | public init(bundle: Bundle) {
17 | self.bundle = bundle
18 | }
19 |
20 | private let bundle: Bundle
21 |
22 | /// Translate the provided key.
23 | public func translate(_ key: String) -> String {
24 | bundle.localizedString(forKey: key, value: "", table: nil)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Localization/LocalizationNotification.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalizationNotification.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-03-19.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | public extension NSNotification.Name {
13 |
14 | /// Get a localization-specific notification.
15 | static func localization(
16 | _ notification: LocalizationNotification
17 | ) -> NSNotification.Name {
18 | notification.name
19 | }
20 | }
21 |
22 | /**
23 | This enum has localization-specific notifications.
24 | */
25 | public enum LocalizationNotification: String {
26 |
27 | case
28 | localeWillChange,
29 | localeDidChange
30 |
31 | public var name: NSNotification.Name {
32 | NSNotification.Name(rawValue: "com.danielsaidi.swiftkit.\(rawValue)")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Localization/LocalizationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalizationService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-03-06.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented any ``Translator`` that is
13 | also capable of changing the app's current locale.
14 |
15 | Implementations of this protocol should make sure to post a
16 | ``LocalizationNotification`` when the app locale changes.
17 | */
18 | public protocol LocalizationService: Translator {
19 |
20 | /// Change the service's locale.
21 | func setLocale(_ locale: Locale) throws
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Localization/StandardLocalizationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardLanguageService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-03-06.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This service lets you change the locale of your app without
13 | having to restart the app for the change to be applied.
14 |
15 | This service wraps a translator, which is uses to translate
16 | language keys. A `StandardTranslator` will be used at first,
17 | but as soon as you call `setLocale` the standard translator
18 | will replaced with a `BundleTranslator`, that will used the
19 | bundle of the new locale.
20 | */
21 | open class StandardLocalizationService: LocalizationService {
22 |
23 | public init(
24 | translator: Translator = StandardTranslator(),
25 | bundle: Bundle = .main,
26 | notificationCenter: NotificationCenter = .default,
27 | userDefaults: UserDefaults = .standard) {
28 | self.translator = translator
29 | self.bundle = bundle
30 | self.notificationCenter = notificationCenter
31 | self.userDefaults = userDefaults
32 | }
33 |
34 | private let bundle: Bundle
35 | private let notificationCenter: NotificationCenter
36 | private var translator: Translator
37 | private let userDefaults: UserDefaults
38 |
39 | public enum LocaleError: Error {
40 | case languageCodeIsMissing(for: Locale)
41 | case lprojFileDoesNotExist(for: Locale)
42 | }
43 |
44 | /// Change the service's locale.
45 | open func setLocale(_ locale: Locale) throws {
46 | guard let languageCode = locale.languageCode else { throw LocaleError.languageCodeIsMissing(for: locale) }
47 | guard loadBundle(for: languageCode) else { throw LocaleError.lprojFileDoesNotExist(for: locale) }
48 | notificationCenter.post(name: .localization(.localeWillChange), object: nil)
49 | userDefaults.set([languageCode], forKey: "AppleLanguages")
50 | notificationCenter.post(name: .localization(.localeDidChange), object: nil)
51 | }
52 |
53 | /// Translate the provided key.
54 | open func translate(_ key: String) -> String {
55 | translator.translate(key)
56 | }
57 | }
58 |
59 |
60 | // MARK: - Private Functions
61 |
62 | private extension StandardLocalizationService {
63 |
64 | func loadBundle(for locale: String) -> Bool {
65 | guard let path = bundle.path(forResource: locale, ofType: "lproj") else { return false }
66 | guard let bundle = Bundle(path: path) else { return false }
67 | translator = BundleTranslator(bundle: bundle)
68 | return true
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Localization/StandardTranslator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardTranslator.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-04-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This standard ``Translator`` implementation translates keys
13 | using `NSLocalizedString`.
14 | */
15 | public class StandardTranslator: Translator {
16 |
17 | public init() {}
18 |
19 | /// Translate the provided key.
20 | public func translate(_ key: String) -> String {
21 | NSLocalizedString(key, comment: "")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Localization/Translator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Translator.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-04-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by any classes that can be
13 | used to translate a localized string synchronously.
14 | */
15 | public protocol Translator: AnyObject {
16 |
17 | /// Translate the provided key.
18 | func translate(_ key: String) -> String
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Numerics/Decimal+Double.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Decimal+Double.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Decimal {
12 |
13 | /// The value's `Double` value representation.
14 | var doubleValue: Double {
15 | Double(truncating: self as NSNumber)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Numerics/Double+Rounded.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double+Rounded.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Double {
12 |
13 | /// The value rounded to a certain number of decimals.
14 | func roundedWithDecimals(_ decimals: Int) -> Double {
15 | let divisor = pow(10.0, Double(decimals))
16 | return (self * divisor).rounded() / divisor
17 | }
18 |
19 | /**
20 | The value rounded to the decimal precision of a certain
21 | reference value.
22 |
23 | `TODO` This currently fails for a higher precision. See
24 | the unit tests for an example of a failing precision.
25 | */
26 | func roundedWithPrecision(from value: Double) -> Double {
27 | let stepSize = Decimal(value)
28 | let exponent = abs(min(0, stepSize.exponent))
29 | let multiplier = Decimal(sign: .plus, exponent: exponent, significand: 1)
30 | let multipliedValue = (self * multiplier.doubleValue).rounded()
31 | let multipliedStepSize = (stepSize.doubleValue * multiplier.doubleValue).rounded()
32 | let multipliedRemined = multipliedValue.truncatingRemainder(dividingBy: multipliedStepSize)
33 | let remainder = Decimal(multipliedRemined) / multiplier
34 | let offset = remainder.isZero ? 0 : stepSize - remainder
35 | let result = Decimal(multipliedValue) / multiplier + offset
36 | return result.doubleValue
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Numerics/NumberFormatter+Init.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberFormatter+Init.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-10-19.
6 | // Copyright © 2022 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension NumberFormatter {
12 |
13 | /**
14 | Create number formatter with a certain locale and style.
15 |
16 | The initializer will default to US English to make sure
17 | that we by default get the default Qapital locale.
18 |
19 | - Parameters:
20 | - numberStyle: The number style to use.
21 | - fixedDecimals: The number of fixed decimals to use, if any, by default `nil`.
22 | - locale: The locale to use, by default `en-US`.
23 | */
24 | convenience init(
25 | numberStyle: NumberFormatter.Style,
26 | fixedDecimals: Int? = nil,
27 | locale: Locale = Locale(identifier: "en-US")
28 | ) {
29 | self.init()
30 | self.numberStyle = numberStyle
31 | if let decimals = fixedDecimals {
32 | minimumFractionDigits = decimals
33 | maximumFractionDigits = decimals
34 | }
35 | self.locale = locale
36 | }
37 | }
38 |
39 | public extension NumberFormatter {
40 |
41 | /// A percent formatter with a fixed number of decimals.
42 | static func percent(decimals: Int) -> NumberFormatter {
43 | NumberFormatter(
44 | numberStyle: .percent,
45 | fixedDecimals: decimals
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Numerics/NumberFormatter+Util.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberFormatter+Util.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-10-19.
6 | // Copyright © 2022 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension NumberFormatter {
12 |
13 | /// Create a string for a double value.
14 | func string(for value: Double) -> String? {
15 | string(for: NSNumber(value: value))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Numerics/Numeric+Conversions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Numeric+Conversions.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreGraphics
11 |
12 | public extension CGFloat {
13 |
14 | func toDouble() -> Double { Double(self) }
15 | func toFloat() -> Float { Float(self) }
16 | func toInt() -> Int { Int(self) }
17 | }
18 |
19 | public extension Double {
20 |
21 | func toCGFloat() -> CGFloat { CGFloat(self) }
22 | func toFloat() -> Float { Float(self) }
23 | func toInt() -> Int { Int(self) }
24 | }
25 |
26 | public extension Float {
27 |
28 | func toCGFloat() -> CGFloat { CGFloat(self) }
29 | func toDouble() -> Double { Double(self) }
30 | func toInt() -> Int { Int(self) }
31 | }
32 |
33 | public extension Int {
34 |
35 | func toCGFloat() -> CGFloat { CGFloat(self) }
36 | func toDouble() -> Double { Double(self) }
37 | func toFloat() -> Float { Float(self) }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Numerics/Numeric+String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Numeric+Format.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-11-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // https://danielsaidi.com/blog/2020-06-03-numeric-string-representation
9 | //
10 |
11 | import Foundation
12 |
13 | public protocol NumericStringRepresentable: CVarArg {}
14 |
15 | extension Double: NumericStringRepresentable {}
16 | extension Float: NumericStringRepresentable {}
17 |
18 | public extension NumericStringRepresentable {
19 |
20 | func string(withDecimals decimals: Int) -> String {
21 | String(format: "%0.\(decimals)f", self)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Services/Decorator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Decorator.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This class can be inherited by any class that wraps another
13 | class that implements the same protocol.
14 |
15 | This can be used to implement the "decorator pattern" which
16 | lets you compose functionality without inheritance.
17 |
18 | For instance, say that you have an external movie api, from
19 | which you fetch moves. A `MovieService` protocol could then
20 | be used to generally describe how to fetch movies, while an
21 | `ApiMovieService` class could provide a pure implementation
22 | of the protocol, by communicating directly with the api. In
23 | order to add things like caching, offline support etc. your
24 | app could now implement other `MovieService` implementation
25 | classes, that instead of inheriting `ApiMovieService` apply
26 | aisolated features on top of any `MovieService`.
27 |
28 | This gives you a more modular way of composing features. It
29 | lets you avoid long dependency chains, makes it easier when
30 | unit testing etc.
31 | */
32 | open class Decorator {
33 |
34 | public init(base: T) {
35 | self.base = base
36 | }
37 |
38 | public let base: T
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Services/MultiProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiProxy.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This class can be implemented by any classes that should be
13 | used to proxy a certain operation to many targets.
14 |
15 | This can be a useful approach when an operation or any kind
16 | of action could be performed by several unrelated receivers.
17 | */
18 | open class MultiProxy {
19 |
20 | public init(targets: [T]) {
21 | self.targets = targets
22 | }
23 |
24 | public let targets: [T]
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Services/Proxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Proxy.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This class can be implemented by any classes that should be
13 | used to proxy a certain operation to another target.
14 |
15 | This can be a useful approach when an operation or any kind
16 | of action should be performed, but you want to add a second
17 | layer of logic on top of it.
18 | */
19 | open class Proxy {
20 |
21 | public init(target: T) {
22 | self.target = target
23 | }
24 |
25 | public let target: T
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/SwiftKit.docc/Resources/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SwiftKit/0acd067a8aaf1d7fc5aabda5de0e9d829de4c64d/Sources/SwiftKit/SwiftKit.docc/Resources/Logo.png
--------------------------------------------------------------------------------
/Sources/SwiftKit/SwiftKit.docc/SwiftKit.md:
--------------------------------------------------------------------------------
1 | # ``SwiftKit``
2 |
3 | SwiftKit adds extra functionality to the Swift programming language.
4 |
5 |
6 |
7 | ## Overview
8 |
9 | 
10 |
11 | The online documentation is currently iOS-specific. To generate documentation for other platforms, open the package in Xcode, select a simulator then run `Product/Build Documentation`.
12 |
13 | The library is divided into the namespaces found in the Topics section below. For more information, source code, an if you want to report issues, sponsor the project etc., visit the [project repository](https://github.com/danielsaidi/SwiftKit).
14 |
15 |
16 |
17 | ## Installation
18 |
19 | SwiftKit can be installed with the Swift Package Manager:
20 |
21 | ```
22 | https://github.com/danielsaidi/SwiftKit.git
23 | ```
24 |
25 | If you prefer to not have external dependencies, you can also just copy the source code into your app.
26 |
27 |
28 |
29 | ## License
30 |
31 | SwiftKit is available under the MIT license.
32 |
33 |
34 |
35 | ## Topics
36 |
37 | ### Data
38 |
39 | - ``Base64StringCoder``
40 | - ``CsvParser``
41 | - ``CsvParserError``
42 | - ``MimeType``
43 | - ``StandardCsvParser``
44 | - ``StringCoder``
45 | - ``StringDecoder``
46 | - ``StringEncoder``
47 |
48 | ### Device
49 |
50 | - ``DeviceIdentifier``
51 | - ``KeychainBasedDeviceIdentifier``
52 | - ``UserDefaultsBasedDeviceIdentifier``
53 |
54 | ### Extensions
55 |
56 | This namespace contains a lot of extensions and protocols that are applied to native types.
57 |
58 | - ``PreferredClosestValue``
59 | - ``NumericStringRepresentable``
60 |
61 | ### Files
62 |
63 | - ``BundleFileFinder``
64 | - ``DirectoryService``
65 | - ``FileFinder``
66 | - ``StandardDirectoryService``
67 |
68 | ### Geo
69 |
70 | - ``WorldCoordinate``
71 |
72 | ### iCloud
73 |
74 | - ``iCloudDocumentSync``
75 | - ``StandardiCloudDocumentSync``
76 |
77 | ### Keychain
78 |
79 | - ``KeychainReader``
80 | - ``KeychainService``
81 | - ``KeychainWrapper``
82 | - ``KeychainWriter``
83 | - ``StandardKeychainService``
84 |
85 | - ``KeychainAttrRepresentable``
86 | - ``KeychainItemAccessibility``
87 |
88 | ### Localization
89 |
90 | - ``BundleTranslator``
91 | - ``LocalizationNotification``
92 | - ``LocalizationService``
93 | - ``StandardLocalizationService``
94 | - ``StandardTranslator``
95 | - ``Translator``
96 |
97 | ### Services
98 |
99 | - ``Decorator``
100 | - ``MultiProxy``
101 | - ``Proxy``
102 |
103 | ### Validation
104 |
105 | - ``EmailValidator``
106 | - ``Validator``
107 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Validation/EmailValidator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmailValidator.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This `Validator` can be used to validate e-mail addresses.
13 | */
14 | public class EmailValidator: Validator {
15 |
16 | public init() {}
17 |
18 | public func validate(_ string: String) -> Bool {
19 | let regExp = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,} ?"
20 | let predicate = NSPredicate(format: "SELF MATCHES %@", regExp)
21 | return predicate.evaluate(with: string)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/Validation/Validator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Validator.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Validator {
12 |
13 | associatedtype Validation
14 |
15 | func validate(_ obj: Validation) -> Bool
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/Authentication.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
4 | public struct Authentication: Identifiable, Equatable {
5 |
6 | public init(id: String) {
7 | self.id = id
8 | }
9 |
10 | public var id: String
11 |
12 | static var standard: Authentication {
13 | Authentication(id: "com.swiftkit.auth.any")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/AuthenticationService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
4 | public protocol AuthenticationService: AnyObject {
5 |
6 | typealias AuthCompletion = (_ result: AuthResult) -> Void
7 | typealias AuthError = AuthenticationServiceError
8 | typealias AuthResult = Result
9 |
10 | func authenticateUser(
11 | for auth: Authentication,
12 | reason: String,
13 | completion: @escaping AuthCompletion)
14 |
15 | func canAuthenticateUser(
16 | for auth: Authentication
17 | ) -> Bool
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/AuthenticationServiceError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthenticationServiceError.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-04-28.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
12 | public enum AuthenticationServiceError: Error, Equatable {
13 |
14 | case authenticationFailed
15 | case authenticationFailedWithErrorMessage(String)
16 | case unsupportedAuthentication
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/BiometricAuthenticationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BiometricAuthenticationService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-01-18.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS) || os(macOS)
10 | import LocalAuthentication
11 |
12 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
13 | public class BiometricAuthenticationService: LocalAuthenticationService {
14 |
15 | public init() {
16 | super.init(policy: .deviceOwnerAuthenticationWithBiometrics)
17 | }
18 | }
19 | #endif
20 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/CachedAuthenticationService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
4 | public protocol CachedAuthenticationService: AuthenticationService {
5 |
6 | func isUserAuthenticated(for auth: Authentication) -> Bool
7 | func resetUserAuthentication(for auth: Authentication)
8 | func resetUserAuthentications()
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/CachedAuthenticationServiceProxy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
4 | public class CachedAuthenticationServiceProxy: CachedAuthenticationService {
5 |
6 | public init(baseService: AuthenticationService) {
7 | self.baseService = baseService
8 | }
9 |
10 | private let baseService: AuthenticationService
11 | private var cache = [String: Bool]()
12 |
13 | public func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
14 | if isUserAuthenticated(for: auth) { return completion(.success(())) }
15 | baseService.authenticateUser(for: auth, reason: reason) { result in
16 | self.handle(result, for: auth)
17 | completion(result)
18 | }
19 | }
20 |
21 | public func canAuthenticateUser(for auth: Authentication) -> Bool {
22 | baseService.canAuthenticateUser(for: auth)
23 | }
24 |
25 | public func isUserAuthenticated(for auth: Authentication) -> Bool {
26 | cache[auth.id] ?? false
27 | }
28 |
29 | public func resetUserAuthentication(for auth: Authentication) {
30 | setIsAuthenticated(false, for: auth)
31 | }
32 |
33 | public func resetUserAuthentications() {
34 | cache.removeAll()
35 | }
36 | }
37 |
38 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
39 | private extension CachedAuthenticationServiceProxy {
40 |
41 | func handle(_ result: AuthResult, for auth: Authentication) {
42 | switch result {
43 | case .failure: setIsAuthenticated(false, for: auth)
44 | case .success: setIsAuthenticated(true, for: auth)
45 | }
46 | }
47 |
48 | func setIsAuthenticated(_ isAuthenticated: Bool, for auth: Authentication) {
49 | cache[auth.id] = isAuthenticated
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Authentication/LocalAuthenticationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalAuthenticationService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-04-29.
6 | // Copyright © 2022 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS) || os(macOS)
10 | import LocalAuthentication
11 |
12 | @available(*, deprecated, message: "This is no longer used. Use LAContext directly instead.")
13 | open class LocalAuthenticationService: AuthenticationService {
14 |
15 | public init(policy: LAPolicy) {
16 | self.policy = policy
17 | }
18 |
19 |
20 | private let policy: LAPolicy
21 |
22 | open func authenticateUser(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
23 | performAuthentication(for: auth, reason: reason) { result in
24 | DispatchQueue.main.async { completion(result) }
25 | }
26 | }
27 |
28 | open func canAuthenticateUser(for auth: Authentication) -> Bool {
29 | var error: NSError?
30 | return LAContext().canEvaluatePolicy(policy, error: &error)
31 | }
32 |
33 | open func performAuthentication(for auth: Authentication, reason: String, completion: @escaping AuthCompletion) {
34 | LAContext().evaluatePolicy(policy, localizedReason: reason) { result, error in
35 | if let error = error { return completion(.failure(error)) }
36 | if result == false { return completion(.failure(AuthError.authenticationFailed)) }
37 | completion(.success(()))
38 | }
39 | }
40 | }
41 | #endif
42 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Bundle/BundleInformation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BundleInformation.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @available(*, deprecated, message: "Just use the corresponding bundle extensions.")
12 | extension Bundle: BundleInformation {}
13 |
14 | @available(*, deprecated, message: "Just use the corresponding bundle extensions.")
15 | public protocol BundleInformation {
16 |
17 | /// Get the bundle build number, e.g. `42567`.
18 | var buildNumber: String { get }
19 |
20 | /// Get the bundle display name, if any.
21 | var displayName: String { get }
22 |
23 | /// Get the bundle build number, e.g. `42567`.
24 | var versionNumber: String { get }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Data/Filter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Filter.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @available(*, deprecated, message: "This will be removed in SwiftKit 2.0")
12 | public struct Filter: Equatable {
13 |
14 | public init(available: [T], selected: [T]) {
15 | self.available = available
16 | self.selected = selected
17 | }
18 |
19 | public let available: [T]
20 | public var selected: [T]
21 | }
22 |
23 | @available(*, deprecated, message: "This will be removed in SwiftKit 2.0")
24 | public extension Filter {
25 |
26 | mutating func deselect(_ option: T) {
27 | selected = selected.filter { $0 != option }
28 | }
29 |
30 | mutating func select(_ option: T) {
31 | selected = Array(Set(selected + [option]))
32 | }
33 |
34 | func isIdentical(to filter: Filter) -> Bool {
35 | let isAvailableIdentical = available.sorted() == filter.available.sorted()
36 | let isSelectedIdentical = selected.sorted() == filter.selected.sorted()
37 | return isAvailableIdentical && isSelectedIdentical
38 | }
39 | }
40 |
41 | @available(*, deprecated, message: "This will be removed in SwiftKit 2.0")
42 | public protocol FilterOption: Hashable {
43 |
44 | associatedtype SortValue: Comparable
45 |
46 | var sortValue: SortValue { get }
47 | }
48 |
49 | @available(*, deprecated, message: "This will be removed in SwiftKit 2.0")
50 | public extension Sequence where Iterator.Element: FilterOption {
51 |
52 | func sorted() -> [Element] {
53 | sorted { $0.sortValue < $1.sortValue }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Data/Persisted.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Persisted.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-04-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This property wrapper automatically persists any new values
13 | to user defaults and sets the initial property value to the
14 | last persisted value or a fallback value.
15 |
16 | This type is internal, since the `SwiftUI` tyope is used in
17 | more ways. This type only serves the library functionality.
18 | */
19 | @propertyWrapper
20 | struct Persisted {
21 |
22 | init(
23 | key: String,
24 | store: UserDefaults = .standard,
25 | defaultValue: T) {
26 | self.key = key
27 | self.store = store
28 | self.defaultValue = defaultValue
29 | }
30 |
31 | private let key: String
32 | private let store: UserDefaults
33 | private let defaultValue: T
34 |
35 | var wrappedValue: T {
36 | get {
37 | guard let data = store.object(forKey: key) as? Data else { return defaultValue }
38 | let value = try? JSONDecoder().decode(T.self, from: data)
39 | return value ?? defaultValue
40 | }
41 | set {
42 | let data = try? JSONEncoder().encode(newValue)
43 | store.set(data, forKey: key)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Extensions/Result+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Result+Utils.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-04-28.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // https://danielsaidi.com/blog/2020/06/03/result-utils
9 | //
10 |
11 | import Foundation
12 |
13 | @available(*, deprecated, message: "Use async code instead.")
14 | public extension Result {
15 |
16 | /// Get the failure error, if any.
17 | var failureError: Failure? {
18 | switch self {
19 | case .failure(let error): return error
20 | case .success: return nil
21 | }
22 | }
23 |
24 | /// Check whether or not the result is a failure result.
25 | var isFailure: Bool { !isSuccess }
26 |
27 | /// Check whether or not the result is a success result.
28 | var isSuccess: Bool {
29 | switch self {
30 | case .failure: return false
31 | case .success: return true
32 | }
33 | }
34 |
35 | /// Get the success result, if any.
36 | var successResult: Success? {
37 | switch self {
38 | case .failure: return nil
39 | case .success(let value): return value
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Extensions/Url+GlobalDeprecated.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Url+Global.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-31.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension URL {
12 |
13 | /**
14 | This url leads to the Apple subscription screen for the
15 | currently logged in account.
16 | */
17 | @available(*, deprecated, message: "Use native functionality instead")
18 | static let userSubscriptions = URL(string: "https://apps.apple.com/account/subscriptions")
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Extensions/Url+QueryParameters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Url+QueryParameters.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-12-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @available(*, deprecated, message: "Use ApiKit instead")
12 | public extension URL {
13 |
14 | /// Get the url's query parameters.
15 | var queryParameters: [URLQueryItem] {
16 | URLComponents(string: absoluteString)?.queryItems ?? [URLQueryItem]()
17 | }
18 |
19 | /// Get the url's query parameters as a dictionary.
20 | var queryParametersDictionary: [String: String] {
21 | var result = [String: String]()
22 | queryParameters.forEach { result[$0.name] = $0.value ?? "" }
23 | return result
24 | }
25 |
26 | /// Get a certain query parameter by name.
27 | func queryParameter(named name: String) -> URLQueryItem? {
28 | queryParameters.first { $0.isNamed(name) }
29 | }
30 |
31 | /**
32 | Set the value of a certain query parameter.
33 |
34 | This will return a new url where the query parameter is
35 | either updated or added.
36 | */
37 | func setQueryParameter(name: String, value: String, urlEncode: Bool = true) -> URL? {
38 | guard let urlString = absoluteString.components(separatedBy: "?").first else { return self }
39 | let param = queryParameter(named: name)
40 | let name = param?.name ?? name
41 | var dictionary = queryParametersDictionary
42 | dictionary[name] = urlEncode ? value.urlEncoded() : value
43 | return URL(string: "\(urlString)?\(dictionary.queryString)")
44 | }
45 |
46 | /**
47 | Set the value of a certain set of query parameters.
48 |
49 | This will return a new url, where every query parameter
50 | in the dictionary is either updated or added.
51 | */
52 | func setQueryParameters(_ dict: [String: String], urlEncode: Bool = true) -> URL? {
53 | var result = self
54 | dict.forEach {
55 | result = result.setQueryParameter(name: $0, value: $1) ?? result
56 | }
57 | return result
58 | }
59 | }
60 |
61 |
62 | // MARK: - Dictionary Extensions
63 |
64 | private extension Dictionary where Key == String, Value == String {
65 |
66 | var queryString: String {
67 | let parameters = map { "\($0)=\($1)" }
68 | return parameters.joined(separator: "&")
69 | }
70 | }
71 |
72 |
73 | // MARK: - URLQueryItem Extensions
74 |
75 | private extension URLQueryItem {
76 |
77 | func isNamed(_ name: String) -> Bool {
78 | self.name.lowercased() == name.lowercased()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Files/FileExporter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileExporter.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-02-02.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @available(*, deprecated, message: "This will be removed in SwiftKit 2.0")
12 | public protocol FileExporter {
13 |
14 | typealias Completion = (Result) -> Void
15 |
16 | /**
17 | Delete a previously exported file.
18 |
19 | This function should be called when you are done with a
20 | file, to avoid that the file system fills up with files
21 | that are no longer used.
22 | */
23 | func deleteFile(named fileName: String)
24 |
25 | /**
26 | Export the provided data to a certain file.
27 |
28 | The resulting file url will depend on the file exporter
29 | implementation. For instance, the `StandardFileExporter`
30 | will store the file in the specified directory.
31 | */
32 | func export(data: Data, to fileName: String, completion: @escaping Completion)
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Files/StandardFileExporter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardFileExporter.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-02-02.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @available(*, deprecated, message: "This will be removed in SwiftKit 2.0")
12 | public class StandardFileExporter: FileExporter {
13 |
14 | public init(
15 | fileManager: FileManager = .default,
16 | directory: FileManager.SearchPathDirectory = .documentDirectory) {
17 | self.fileManager = fileManager
18 | self.directory = directory
19 | }
20 |
21 | private let fileManager: FileManager
22 | private let directory: FileManager.SearchPathDirectory
23 |
24 | public enum ExportError: Error {
25 | case invalidUrl
26 | }
27 |
28 | /**
29 | Delete a previously exported file.
30 |
31 | This function should be called when you are done with a
32 | file, to avoid that the file system fills up with files
33 | that are no longer used.
34 | */
35 | public func deleteFile(named fileName: String) {
36 | guard let url = getFileUrl(forFileName: fileName) else { return }
37 | try? fileManager.removeItem(at: url)
38 | }
39 |
40 | /**
41 | Export the provided data to a certain file.
42 |
43 | The resulting file url will depend on the file exporter
44 | implementation. For instance, the `StandardFileExporter`
45 | will store the file in the specified directory.
46 | */
47 | public func export(data: Data, to fileName: String, completion: @escaping Completion) {
48 | guard let url = getFileUrl(forFileName: fileName) else { return completion(.failure(ExportError.invalidUrl)) }
49 | tryWrite(data: data, to: url, completion: completion)
50 | }
51 | }
52 |
53 | private extension StandardFileExporter {
54 |
55 | func getFileUrl(forFileName fileName: String) -> URL? {
56 | fileManager.urls(for: directory, in: .userDomainMask).first?.appendingPathComponent(fileName)
57 | }
58 |
59 | func tryWrite(data: Data, to url: URL, completion: @escaping Completion) {
60 | do {
61 | try data.write(to: url, options: .atomicWrite)
62 | completion(.success(url))
63 | } catch {
64 | completion(.failure(error))
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Geo/AppleMapsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleMapsService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-02-18.
6 | // Copyright © 2015 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | @available(*, deprecated, message: "Use MKMapItem openInMaps instead")
12 | public class AppleMapsService: ExternalMapService {
13 |
14 | public init() {}
15 |
16 | public func getUrl(for coordinate: CLLocationCoordinate2D) -> URL? {
17 | let string = "http://maps.apple.com/maps?ll=\(coordinate.latitude),\(coordinate.longitude)"
18 | return URL(string: string)
19 | }
20 |
21 | public func getUrl(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> URL? {
22 | let string = "http://maps.apple.com/maps?saddr=\(from.latitude),\(from.longitude)&daddr=\(to.latitude),\(to.longitude)"
23 | return URL(string: string)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Geo/ExternalMapService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExternalMapService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-02-18.
6 | // Copyright © 2015 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | @available(*, deprecated, message: "Use MKMapItem openInMaps instead")
12 | public protocol ExternalMapService {
13 |
14 | func getUrl(for coordinate: CLLocationCoordinate2D) -> URL?
15 | func getUrl(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> URL?
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Geo/GoogleMapsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GoogleMapsService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2015-02-18.
6 | // Copyright © 2015 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | @available(*, deprecated, message: "Use MKMapItem openInMaps instead")
12 | public class GoogleMapsService: ExternalMapService {
13 |
14 | public init() {}
15 |
16 | public func getUrl(for coordinate: CLLocationCoordinate2D) -> URL? {
17 | let string = "comgooglemaps://?center=\(coordinate.latitude),\(coordinate.longitude)"
18 | return URL(string: string)
19 | }
20 |
21 | public func getUrl(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> URL? {
22 | let string = "comgooglemaps://?saddr=\(from.latitude),\(from.longitude)&daddr=\(to.latitude),\(to.longitude)"
23 | return URL(string: string)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/IoC/DipIoCContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DipContainer.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2017-09-06.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // Get Dip: https://github.com/AliSoftware/Dip
9 | //
10 |
11 | /*
12 | import Dip
13 |
14 | /**
15 | This class implements the `IoCContainer` protocol, using an
16 | external library called `Dip`.
17 |
18 | Since `SwiftKit` doesn't depend on `Dip`, this code must be
19 | commented out.
20 |
21 | You can add this class to apps that have a `Dip` dependency,
22 | then uncomment the code to get a complete `IoCContainer`.
23 | */
24 | class DipContainer: IoCContainer {
25 |
26 | init(container: DependencyContainer) {
27 | self.container = container
28 | }
29 |
30 | private var container: DependencyContainer
31 |
32 | func resolve() -> T {
33 | do {
34 | return try container.resolve()
35 | } catch {
36 | fatalError("\(#function) failed")
37 | }
38 | }
39 |
40 | func resolve(arguments arg1: A) -> T {
41 | do {
42 | return try container.resolve(arguments: arg1)
43 | } catch {
44 | fatalError("\(#function) failed")
45 | }
46 | }
47 |
48 | func resolve(arguments arg1: A, _ arg2: B) -> T {
49 | do {
50 | return try container.resolve(arguments: arg1, arg2)
51 | } catch {
52 | fatalError("\(#function) failed")
53 | }
54 | }
55 |
56 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C) -> T {
57 | do {
58 | return try container.resolve(arguments: arg1, arg2, arg3)
59 | } catch {
60 | fatalError("\(#function) failed")
61 | }
62 | }
63 |
64 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D) -> T {
65 | do {
66 | return try container.resolve(arguments: arg1, arg2, arg3, arg4)
67 | } catch {
68 | fatalError("\(#function) failed")
69 | }
70 | }
71 |
72 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D, _ arg5: E) -> T {
73 | do {
74 | return try container.resolve(arguments: arg1, arg2, arg3, arg4, arg5)
75 | } catch {
76 | fatalError("\(#function) failed")
77 | }
78 | }
79 | }
80 | */
81 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/IoC/IoC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IoC.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-03-10.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This class can be used to remove coupling to your preferred
13 | IoC library, e.g. Dip or Swinject.
14 |
15 | You can either implement your own `IoCContainer` or comment
16 | out the code of any `IocContainer` class in this folder and
17 | add it to your app. You can then register it globally using
18 | `IoC.register(...)` and use it with `IoC.resolve(...)`.
19 |
20 | If you don't want to use an IoC container, you can just use
21 | the `IoC` class as a container for static properties.
22 | */
23 | @available(*, deprecated, message: "This type has been deprecated and will be removed in the next major version.")
24 | public final class IoC {
25 |
26 | public private(set) static var container: IoCContainer!
27 |
28 | public static func register(_ container: IoCContainer) {
29 | IoC.container = container
30 | }
31 |
32 | public static func resolve() -> T {
33 | container.resolve()
34 | }
35 |
36 | public static func resolve(arguments arg1: A) -> T {
37 | container.resolve(arguments: arg1)
38 | }
39 |
40 | public static func resolve(arguments arg1: A, _ arg2: B) -> T {
41 | container.resolve(arguments: arg1, arg2)
42 | }
43 |
44 | public static func resolve(arguments arg1: A, _ arg2: B, _ arg3: C) -> T {
45 | container.resolve(arguments: arg1, arg2, arg3)
46 | }
47 |
48 | public static func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D) -> T {
49 | container.resolve(arguments: arg1, arg2, arg3, arg4)
50 | }
51 |
52 | public static func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D, _ arg5: E) -> T {
53 | container.resolve(arguments: arg1, arg2, arg3, arg4, arg5)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/IoC/IoCContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IoCContainer.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2016-03-10.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /**
13 | This protocol can be implemented by classes that can handle
14 | inversion of control, by dynamically resolving types, given
15 | any required arguments.
16 | */
17 | @available(*, deprecated, message: "This type has been deprecated and will be removed in the next major version.")
18 | public protocol IoCContainer {
19 |
20 | func resolve() -> T
21 | func resolve(arguments arg1: A) -> T
22 | func resolve(arguments arg1: A, _ arg2: B) -> T
23 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C) -> T
24 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D) -> T
25 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D, _ arg5: E) -> T
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/IoC/SwinjectIoCContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwinjectIoCContainer.swift
3 | // Pinny
4 | //
5 | // Created by Daniel Saidi on 2017-09-06.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 | // Get Swinject: https://github.com/Swinject/Swinject
9 | //
10 |
11 | /*
12 | import Swinject
13 |
14 | /**
15 | This class implements the `IoCContainer` protocol, using an
16 | external library called `Swinject`.
17 |
18 | Since `SwiftKit` doesn't depend on `Swinject` the code must
19 | be commented out.
20 |
21 | You can add this conainer to apps that use `Swinject`, then
22 | uncomment the code to get a complete `IoCContainer`.
23 | */
24 | class SwinjectIoCContainer: IoCContainer {
25 |
26 | init(container: Container) {
27 | self.container = container
28 | }
29 |
30 | private var container: Container
31 |
32 | func resolve() -> T {
33 | container.resolve(T.self)!
34 | }
35 |
36 | func resolve(arguments arg1: A) -> T {
37 | container.resolve(T.self, argument: arg1)!
38 | }
39 |
40 | func resolve(arguments arg1: A, _ arg2: B) -> T {
41 | container.resolve(T.self, arguments: arg1, arg2)!
42 | }
43 |
44 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C) -> T {
45 | container.resolve(T.self, arguments: arg1, arg2, arg3)!
46 | }
47 |
48 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D) -> T {
49 | container.resolve(T.self, arguments: arg1, arg2, arg3, arg4)!
50 | }
51 |
52 | func resolve(arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D, _ arg5: E) -> T {
53 | container.resolve(T.self, arguments: arg1, arg2, arg3, arg4, arg5)!
54 | }
55 | }
56 | */
57 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Messaging/MFMailComposeViewController+Attachments.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MFMailComposeViewController+Attachments.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-03-26.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import MessageUI
11 |
12 | @available(*, deprecated, message: "This extension has been deprecated and will be removed in the next major version.")
13 | public extension MFMailComposeViewController {
14 |
15 | /**
16 | Add a data attachment using a `mimeType` and `fileName`.
17 | */
18 | func addAttachmentData(data: Data, mimeType: MimeType, fileName: String) {
19 | addAttachmentData(data, mimeType: mimeType.identifier, fileName: fileName)
20 | }
21 | }
22 | #endif
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Messaging/MSMessageComposeViewController+Attachments.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MFMessageComposeViewController+Attachments.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-03-26.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import MessageUI
11 |
12 | @available(*, deprecated, message: "This extension has been deprecated and will be removed in the next major version.")
13 | public extension MFMessageComposeViewController {
14 |
15 | /**
16 | Add a data attachment using a custom `fileName`.
17 | */
18 | func addAttachmentData(data: Data, fileName: String) {
19 | addAttachmentData(data, typeIdentifier: "public.data", filename: fileName)
20 | }
21 | }
22 | #endif
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Network/ApiEnvironment.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// [DEPRECATED] Use ApiKit instead
4 | public protocol ApiEnvironment {
5 |
6 | var url: URL { get }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Network/ApiModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// [DEPRECATED] Use ApiKit instead
4 | public protocol ApiModel: Decodable {
5 |
6 | associatedtype LocalModel
7 |
8 | func convert() -> LocalModel
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Network/ApiTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApiTypes.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-09-30.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// [DEPRECATED] Use ApiKit instead
12 | public typealias ApiCompletion = (ApiResult) -> Void
13 |
14 | /// [DEPRECATED] Use ApiKit instead
15 | public enum ApiError: Error {
16 |
17 | case invalidData(Data, HTTPURLResponse, Error)
18 | case invalidResponse(Data?, HTTPURLResponse?, Error?)
19 | case invalidUrl(URL)
20 | }
21 |
22 | /// [DEPRECATED] Use ApiKit instead
23 | public typealias ApiResult = Result
24 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/Network/HttpMethod.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// [DEPRECATED] Use ApiKit instead
4 | public enum HttpMethod: String {
5 |
6 | case connect
7 | case delete
8 | case get
9 | case head
10 | case options
11 | case post
12 | case put
13 | case trace
14 |
15 | public var method: String { rawValue.uppercased() }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/StoreKit/StoreContext+Products.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoreContext+Products.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-09.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import StoreKit
10 |
11 | @available(*, deprecated, message: "StoreContext has been moved to StoreKitPlus - https://github.com/danielsaidi/StoreKitPlus")
12 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
13 | public extension StoreContext {
14 |
15 | func isProductPurchased(id: String) -> Bool {
16 | purchasedProductIds.contains(id)
17 | }
18 |
19 | func isProductPurchased(_ product: Product) -> Bool {
20 | isProductPurchased(id: product.id)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/StoreKit/StoreContext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoreContext.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-08.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import StoreKit
10 | import Combine
11 |
12 | @available(*, deprecated, message: "StoreContext has been moved to StoreKitPlus - https://github.com/danielsaidi/StoreKitPlus")
13 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
14 | public class StoreContext: ObservableObject {
15 |
16 | public init() {
17 | productIds = persistedProductIds
18 | purchasedProductIds = persistedPurchasedProductIds
19 | }
20 |
21 | public var products: [Product] = [] {
22 | didSet { productIds = products.map { $0.id} }
23 | }
24 |
25 | @Published
26 | public private(set) var productIds: [String] = [] {
27 | willSet { persistedProductIds = newValue }
28 | }
29 |
30 | @Published
31 | public private(set) var purchasedProductIds: [String] = [] {
32 | willSet { persistedPurchasedProductIds = newValue }
33 | }
34 |
35 | public var transactions: [StoreKit.Transaction] = [] {
36 | didSet { purchasedProductIds = transactions.map { $0.productID } }
37 | }
38 |
39 |
40 | @Persisted(key: key("productIds"), defaultValue: [])
41 | private var persistedProductIds: [String]
42 |
43 | @Persisted(key: key("purchasedProductIds"), defaultValue: [])
44 | private var persistedPurchasedProductIds: [String]
45 | }
46 |
47 | @available(*, deprecated, message: "StoreContext has been moved to StoreKitPlus - https://github.com/danielsaidi/StoreKitPlus")
48 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
49 | private extension StoreContext {
50 |
51 | static func key(_ name: String) -> String { "store.\(name)" }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/StoreKit/StoreService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoreService.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-08.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import StoreKit
10 |
11 | @available(*, deprecated, message: "StoreService has been moved to StoreKitPlus - https://github.com/danielsaidi/StoreKitPlus")
12 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
13 | public protocol StoreService {
14 |
15 | func getProducts() async throws -> [Product]
16 | func purchase(_ product: Product) async throws -> Product.PurchaseResult
17 | func restorePurchases() async throws
18 | func syncStoreData() async throws
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/StoreKit/StoreServiceError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoreServiceError.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-08.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import StoreKit
11 |
12 | @available(*, deprecated, message: "StoreServiceError has been moved to StoreKitPlus - https://github.com/danielsaidi/StoreKitPlus")
13 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
14 | public enum StoreServiceError: Error {
15 |
16 | case invalidTransaction(Transaction, VerificationResult.VerificationError)
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/_Deprecated/StoreKit/Transaction+Valid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Transaction+Valid.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-08.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import StoreKit
10 |
11 | @available(*, deprecated, message: "This extension has been moved to StoreKitPlus - https://github.com/danielsaidi/StoreKitPlus")
12 | @available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
13 | extension Transaction {
14 |
15 | var isValid: Bool {
16 | if revocationDate != nil { return false }
17 | guard let date = expirationDate else { return false }
18 | return date > Date()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/iCloud/StandardiCloudDocumenSync.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardiCloudDocumentSync.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-04-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This class can be used to sync iCloud document changes in a
13 | shared uqibuity container.
14 |
15 | Note that you must have setup iCloud entitlements and added
16 | an iCloud node to Info.plist. All apps that should sync any
17 | documents must belong to the same ubiquity container and be
18 | identically configured.
19 | */
20 | public class StandardiCloudDocumentSync: iCloudDocumentSync {
21 |
22 | public init(
23 | filePattern: String,
24 | fileManager: FileManager = .default,
25 | notificationCenter: NotificationCenter = .default) {
26 | self.filePattern = filePattern
27 | self.fileManager = fileManager
28 | self.notificationCenter = notificationCenter
29 | }
30 |
31 | private let filePattern: String
32 | private let fileManager: FileManager
33 | private let notificationCenter: NotificationCenter
34 |
35 | private lazy var metadataQuery: NSMetadataQuery = {
36 | let metadataQuery = NSMetadataQuery()
37 | metadataQuery.notificationBatchingInterval = 1
38 | metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDataScope, NSMetadataQueryUbiquitousDocumentsScope]
39 | metadataQuery.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, filePattern)
40 | metadataQuery.sortDescriptors = [NSSortDescriptor(key: NSMetadataItemFSNameKey, ascending: true)]
41 | let selector = #selector(handleQueryNotification)
42 | notificationCenter.addObserver(self, selector: selector, name: .NSMetadataQueryDidUpdate, object: metadataQuery)
43 | notificationCenter.addObserver(self, selector: selector, name: .NSMetadataQueryDidFinishGathering, object: metadataQuery)
44 | return metadataQuery
45 | }()
46 |
47 | /// Start syncing iCloud document changes.
48 | public func startSyncingChanges() {
49 | metadataQuery.start()
50 | }
51 |
52 | /// Stop syncing iCloud document changes.
53 | public func stopSyncingChanges() {
54 | notificationCenter.removeObserver(self)
55 | }
56 | }
57 |
58 | @objc private extension StandardiCloudDocumentSync {
59 |
60 | func handleQueryNotification(notification: Notification?) {
61 | guard let metadataQuery = notification?.object as? NSMetadataQuery else { return }
62 | metadataQuery.disableUpdates()
63 | metadataQuery.enumerateResults { item, _, _ in
64 | handleQueryItem(item)
65 | }
66 | metadataQuery.enableUpdates()
67 | }
68 |
69 | func handleQueryItem(_ item: Any) {
70 | guard
71 | let metadataItem = item as? NSMetadataItem,
72 | !isMetadataItemDownloaded(for: metadataItem),
73 | let url = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL
74 | else { return }
75 | try? fileManager.startDownloadingUbiquitousItem(at: url)
76 | }
77 |
78 | func isMetadataItemDownloaded(for item: NSMetadataItem) -> Bool {
79 | let statusKey = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey)
80 | return statusKey as? String == NSMetadataUbiquitousItemDownloadingStatusDownloaded
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/iCloud/URL+iCloud.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+iCloud.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-04-17.
6 | // Copyright 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension URL {
12 |
13 | /**
14 | The url to the iCloud ubiquity container. This is where
15 | documents and data will be saved and synced with iCloud.
16 | */
17 | static func ubiquityContainer(
18 | for manager: FileManager = .default,
19 | containerId: String? = nil) -> URL? {
20 | manager.url(forUbiquityContainerIdentifier: nil)
21 | }
22 |
23 | /**
24 | The url to the iCloud ubiquity container Documents root,
25 | where documents will be saved and synced with iCloud.
26 | */
27 | static func ubiquityContainerDocuments(
28 | for manager: FileManager = .default,
29 | containerId: String? = nil) -> URL? {
30 | ubiquityContainer(for: manager, containerId: containerId)?
31 | .appendingPathComponent("Documents")
32 | }
33 |
34 | /**
35 | The url to a local document fallback directory that can
36 | be used when the `ubiquityContainer` urls are nil.
37 | */
38 | static func ubiquityContainerDocumentsLocalFallbackDirectory(
39 | for manager: FileManager = .default) -> URL? {
40 | manager.urls(for: .documentDirectory, in: .userDomainMask).first
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/SwiftKit/iCloud/iCloudDocumentSync.swift:
--------------------------------------------------------------------------------
1 | //
2 | // iCloudDocumentSync.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-04-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | This protocol can be implemented by any classes that can be
13 | used to sync iCloud document changes.
14 | */
15 | public protocol iCloudDocumentSync {
16 |
17 | /// Start syncing iCloud document changes.
18 | func startSyncingChanges()
19 |
20 | /// Stop syncing iCloud document changes.
21 | func stopSyncingChanges()
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/AsyncTrigger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncTrigger.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-01-18.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class AsyncTrigger {
12 |
13 | public init() {}
14 |
15 | private var counter = 0
16 |
17 | public var hasTriggered: Bool { counter > 0 }
18 |
19 | public func hasTriggered(numberOfTimes: Int) -> Bool {
20 | counter == numberOfTimes
21 | }
22 |
23 | public func trigger() {
24 | counter += 1
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Coding/Base64StringCoderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Base64StringEncoderTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-09-11.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Base64StringEncoderTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | var coder: Base64StringCoder!
18 |
19 | beforeEach {
20 | coder = Base64StringCoder()
21 | }
22 |
23 | describe("encoding string") {
24 |
25 | it("results in base 64 encoded string") {
26 | let string = """
27 | foo
28 | bar
29 | """
30 | let encoded = coder.encode(string)
31 | expect(encoded).to(equal("Zm9vCmJhcg=="))
32 | }
33 | }
34 |
35 | describe("decoding encoded string") {
36 |
37 | it("fails for non-encoded string") {
38 | let string = "test"
39 | let decoded = coder.decode(string)
40 | expect(decoded).to(beNil())
41 | }
42 |
43 | it("results in base 64 encoded string") {
44 | let string = """
45 | foo
46 | bar
47 | """
48 | let encoded = coder.encode(string)!
49 | let decoded = coder.decode(encoded)
50 | expect(decoded).to(equal(string))
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Csv/StandardCsvParserTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardCsvParserTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-10-23.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftKit
10 | import XCTest
11 |
12 | class StandardCsvParserTests: XCTestCase {
13 |
14 | let parser = StandardCsvParser()
15 |
16 | func testCanParseSemicolonSeparatedString() {
17 | let result = parser.parseCsvString("foo;bar;baz\nenough", componentSeparator: ";")
18 | XCTAssertEqual(result.count, 2)
19 | XCTAssertEqual(result[0], ["foo", "bar", "baz"])
20 | XCTAssertEqual(result[1], ["enough"])
21 | }
22 |
23 | func testCanParseCommaSeparatedString() {
24 | let result = parser.parseCsvString("a,b,c", componentSeparator: ",")
25 | XCTAssertEqual(result.count, 1)
26 | XCTAssertEqual(result[0], ["a", "b", "c"])
27 | }
28 |
29 | func testTrimsComponents() {
30 | let result = parser.parseCsvString(" a , b , c ", componentSeparator: ",")
31 | XCTAssertEqual(result.count, 1)
32 | XCTAssertEqual(result[0], ["a", "b", "c"])
33 | }
34 |
35 | func testIncludesEmptyComponents() {
36 | let result = parser.parseCsvString(" a , , c ", componentSeparator: ",")
37 | XCTAssertEqual(result.count, 1)
38 | XCTAssertEqual(result[0], ["a", "", "c"])
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Data/FilterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilterTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class FilterTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | var array11: [TestOption] { [.swedish, .finnish] }
18 | var array12: [TestOption] { [.finnish, .swedish] }
19 | var array21: [TestOption] { [.swedish, .english] }
20 | var array22: [TestOption] { [.english, .swedish] }
21 | var array3: [TestOption] { [.german, .swedish] }
22 |
23 | describe("selecting") {
24 |
25 | it("adds a selected option once") {
26 | var filter = Filter(available: array11, selected: [])
27 | filter.select(.swedish)
28 | filter.select(.swedish)
29 | expect(filter.selected).to(equal([.swedish]))
30 | }
31 | }
32 |
33 | describe("deselecting") {
34 |
35 | it("removes a selected option") {
36 | var filter = Filter(available: array11, selected: [])
37 | filter.select(.swedish)
38 | expect(filter.selected).to(equal([.swedish]))
39 | filter.deselect(.finnish)
40 | expect(filter.selected).to(equal([.swedish]))
41 | filter.deselect(.swedish)
42 | expect(filter.selected).to(equal([]))
43 | }
44 | }
45 |
46 | describe("is identical") {
47 |
48 | it("is identical if both arrays contains same elements") {
49 | let filter1 = Filter(available: array11, selected: array21)
50 | let filter2 = Filter(available: array12, selected: array21)
51 | expect(filter1.isIdentical(to: filter2)).to(beTrue())
52 | }
53 |
54 | it("is not identical if available filters contains different elements") {
55 | let filter1 = Filter(available: array11, selected: array21)
56 | let filter2 = Filter(available: array3, selected: array22)
57 | expect(filter1.isIdentical(to: filter2)).to(beFalse())
58 | }
59 |
60 | it("is not identical if selected filters contains different elements") {
61 | let filter1 = Filter(available: array11, selected: array21)
62 | let filter2 = Filter(available: array12, selected: array3)
63 | expect(filter1.isIdentical(to: filter2)).to(beFalse())
64 | }
65 | }
66 | }
67 | }
68 |
69 | private enum TestOption: String, FilterOption {
70 |
71 | case swedish, finnish, english, german
72 |
73 | var sortValue: String { rawValue }
74 | }
75 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Date/Date+CompareTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+CompareTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-05-28.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import Quick
12 | import Nimble
13 | import SwiftKit
14 |
15 | class DateComparingTests: QuickSpec {
16 |
17 | override func spec() {
18 |
19 | describe("comparing dates") {
20 |
21 | it("correctly compares if a date is before another") {
22 | let date1 = Date(timeIntervalSince1970: 0)
23 | let date2 = Date(timeIntervalSince1970: 1)
24 | expect(date1.isBefore(date2)).to(beTrue())
25 | expect(date2.isBefore(date1)).to(beFalse())
26 | }
27 |
28 | it("correctly compares if a date is after another") {
29 | let date1 = Date(timeIntervalSince1970: 0)
30 | let date2 = Date(timeIntervalSince1970: 1)
31 | expect(date1.isAfter(date2)).to(beFalse())
32 | expect(date2.isAfter(date1)).to(beTrue())
33 | }
34 |
35 | it("correctly compares if a date is same as another") {
36 | let date1 = Date(timeIntervalSince1970: 0)
37 | let date2 = Date(timeIntervalSince1970: 1)
38 | let date3 = Date(timeIntervalSince1970: 0)
39 | expect(date1.isSame(as: date2)).to(beFalse())
40 | expect(date1.isSame(as: date3)).to(beTrue())
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Date/Date+InitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+InitTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Date_InitTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | let formatter = DateFormatter()
19 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
20 |
21 | describe("date") {
22 |
23 | it("can be initialized with date components") {
24 | let date = Date(year: 2011, month: 12, day: 10)!
25 | let string = formatter.string(from: date)
26 | expect(string).to(equal("2011-12-10 00:00:00"))
27 | }
28 |
29 | it("can be initialized with time components") {
30 | let date = Date(year: 2010, month: 03, day: 22, hour: 14, minute: 21, second: 32)!
31 | let string = formatter.string(from: date)
32 | expect(string).to(equal("2010-03-22 14:21:32"))
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Date/DateDecodersTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateDecodersTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-09-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class DateDecodersTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("iso8601 decoder") {
19 |
20 | it("can decode date with seconds") {
21 | let string = "{ \"date\": \"2017-02-24T01:02:03+04:05\" }"
22 | let data = string.data(using: .utf8)!
23 | let decoder = JSONDecoder.iso8601
24 | let obj = try? decoder.decode(TestClass.self, from: data)
25 | expect(obj).toNot(beNil())
26 | }
27 |
28 | it("can decode date with milliseconds") {
29 | let string = "{ \"date\": \"2018-09-05T21:55:16.1184588Z\" }"
30 | let data = string.data(using: .utf8)!
31 | let decoder = JSONDecoder.iso8601
32 | let obj = try? decoder.decode(TestClass.self, from: data)
33 | expect(obj).toNot(beNil())
34 | }
35 | }
36 | }
37 | }
38 |
39 |
40 | private class TestClass: Codable {
41 |
42 | init(date: Date) {
43 | self.date = date
44 | }
45 |
46 | var date: Date
47 |
48 | enum CodingKeys: String, CodingKey {
49 | case date
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Date/DateEncodersTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateEncodersTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-09-06.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class DateEncodersTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("iso8601 encoder") {
19 |
20 | it("can encode date to string") {
21 | let obj = TestClass(date: Date(timeIntervalSince1970: 12132141))
22 | let encoder = JSONEncoder.iso8601
23 | let data = try? encoder.encode(obj)
24 | let string = String(data: data!, encoding: .utf8)
25 | let expected = "{\"date\":\"1970-05-21T10:02:21.000+0000\"}"
26 | expect(string).to(equal(expected))
27 | }
28 | }
29 | }
30 | }
31 |
32 |
33 | private class TestClass: Codable {
34 |
35 | init(date: Date) {
36 | self.date = date
37 | }
38 |
39 | var date: Date
40 |
41 | enum CodingKeys: String, CodingKey {
42 | case date
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Date/DateFormatter+InitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateFormatter+InitTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-09-05.
6 | // Copyright © 2018 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftKit
11 | import XCTest
12 |
13 | class DateFormatter_InitTests: XCTestCase {
14 |
15 | func testConvenienceInitializerUsesUsEnglishWithNoTimeByDefault() {
16 | let formatter = DateFormatter(dateStyle: .medium)
17 | XCTAssertEqual(formatter.locale.identifier, "en_US_POSIX")
18 | XCTAssertEqual(formatter.dateStyle, .medium)
19 | XCTAssertEqual(formatter.timeStyle, .none)
20 | }
21 |
22 | func testConvenienceInstanceGeneratesValidDateStringForMediumDateStyle() {
23 | let date = Date(year: 2022, month: 10, day: 19) ?? Date()
24 | let formatter = DateFormatter(dateStyle: .medium)
25 | let result = formatter.string(from: date)
26 | XCTAssertEqual(result, "Oct 19, 2022")
27 | }
28 |
29 | func testConvenienceInstanceGeneratesValidDateStringForLongDateStyleAndShortTimeStyle() {
30 | let date = Date(year: 2022, month: 10, day: 19) ?? Date()
31 | let formatter = DateFormatter(
32 | dateStyle: .long,
33 | timeStyle: .short
34 | )
35 | let result = formatter.string(from: date)
36 | XCTAssertEqual(result, "October 19, 2022 at 12:00 AM")
37 | }
38 |
39 | func testConvenienceInstanceGeneratesValidDateStringForCustomLocale() {
40 | let date = Date(year: 2022, month: 10, day: 19) ?? Date()
41 | let formatter = DateFormatter(
42 | dateStyle: .long,
43 | timeStyle: .short,
44 | locale: Locale(identifier: "sv-SE")
45 | )
46 | let result = formatter.string(from: date)
47 | XCTAssertEqual(result, "19 oktober 2022 00:00")
48 | }
49 |
50 | func testIso8601SecondFormatterIsValid() {
51 | let formatter = DateFormatter.iso8601Seconds
52 | XCTAssertEqual(formatter.dateFormat, "yyyy-MM-dd'T'HH:mm:ssZ")
53 | XCTAssertEqual(formatter.calendar.identifier, .iso8601)
54 | XCTAssertEqual(formatter.locale.identifier, "en_US_POSIX")
55 | XCTAssertNotNil(formatter.timeZone)
56 | }
57 |
58 | func testIso8601MilliSecondFormatterIsValid() {
59 | let formatter = DateFormatter.iso8601Milliseconds
60 | XCTAssertEqual(formatter.dateFormat, "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
61 | XCTAssertEqual(formatter.calendar.identifier, .iso8601)
62 | XCTAssertEqual(formatter.locale.identifier, "en_US_POSIX")
63 | XCTAssertNotNil(formatter.timeZone)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Device/KeychainBasedDeviceIdentifierTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainBasedDeviceIdentifierTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | @testable import SwiftKit
13 |
14 | class KeychainBasedDeviceIdentifierTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("keychain-based device identifier") {
19 |
20 | var identifier: DeviceIdentifier!
21 | var keychainService: MockKeychainService!
22 | var backupIdentifier: MockDeviceIdentifier!
23 |
24 | beforeEach {
25 | keychainService = MockKeychainService()
26 | backupIdentifier = MockDeviceIdentifier()
27 | identifier = KeychainBasedDeviceIdentifier(
28 | keychainService: keychainService,
29 | backupIdentifier: backupIdentifier)
30 | }
31 |
32 | describe("getting device identifier") {
33 |
34 | context("when keychain value exists") {
35 |
36 | it("returns value") {
37 | let id = "foo"
38 | keychainService.registerResult(for: keychainService.stringRef) { _, _ in id }
39 | let result = identifier.getDeviceIdentifier()
40 | expect(result).to(equal(id))
41 | }
42 | }
43 |
44 | context("when keychain value does not exist") {
45 |
46 | beforeEach {
47 | backupIdentifier.registerResult(for: backupIdentifier.getDeviceIdentifierRef) { "foo" }
48 | }
49 |
50 | it("returns backup identifier value") {
51 | let result = identifier.getDeviceIdentifier()
52 | expect(result).to(equal("foo"))
53 | }
54 |
55 | it("writes to keychain") {
56 | _ = identifier.getDeviceIdentifier()
57 | let calls = keychainService.calls(to: keychainService.setStringRef)
58 | expect(calls.count).to(equal(1))
59 | expect(calls[0].arguments.0).to(equal("foo"))
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Device/MockDeviceIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockDeviceIdentifier.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-04.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import MockingKit
11 | import SwiftKit
12 |
13 | class MockDeviceIdentifier: Mock, DeviceIdentifier {
14 |
15 | lazy var getDeviceIdentifierRef = MockReference(getDeviceIdentifier)
16 |
17 | func getDeviceIdentifier() -> String {
18 | call(getDeviceIdentifierRef, args: ())
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Device/UserDefaultsBasedDeviceIdentifierTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsBasedDeviceIdentifierTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-11-24.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import MockingKit
13 | @testable import SwiftKit
14 |
15 | class UserDefaultsBasedDeviceIdentifierTests: QuickSpec {
16 |
17 | override func spec() {
18 |
19 | describe("user defaults-based device identifier") {
20 |
21 | var identifier: DeviceIdentifier!
22 | var defaults: MockUserDefaults!
23 |
24 | beforeEach {
25 | defaults = MockUserDefaults()
26 | identifier = UserDefaultsBasedDeviceIdentifier(defaults: defaults)
27 | }
28 |
29 | describe("getting device identifier") {
30 |
31 | context("when persisted value exists") {
32 |
33 | it("returns value") {
34 | defaults.registerResult(for: defaults.stringRef) { _ in "foo" }
35 | let result = identifier.getDeviceIdentifier()
36 | expect(result).to(equal("foo"))
37 | }
38 | }
39 |
40 | context("when persisted value does not exist") {
41 |
42 | it("generates new id") {
43 | let result = identifier.getDeviceIdentifier()
44 | expect(result.count).to(equal(36))
45 | }
46 |
47 | it("writes to user defaults") {
48 | _ = identifier.getDeviceIdentifier()
49 | let calls = defaults.calls(to: defaults.setValueRef)
50 | expect(calls.count).to(equal(1))
51 | let arg = calls[0].arguments.0 as? String
52 | expect(arg?.count).to(equal(36))
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Bundle+BundleInformationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bundle+BundleInformationTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 | import Foundation
13 |
14 | class Bundle_BundleInformationTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("bundle") {
19 |
20 | it("implements BundleInformation (empty due to SPM") {
21 | let bundle = Bundle.main
22 | expect(Int(bundle.buildNumber)).to(beGreaterThan(17501))
23 | // expect(bundle.versionNumber).to(equal("13.3"))
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Collections/Array+RangeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array_RangeTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Array_RangeTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("array with int range") {
19 |
20 | it("handles small step size") {
21 | let result = Array(0...10, stepSize: 1)
22 | expect(result).to(equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
23 | }
24 |
25 | it("handles larger step size") {
26 | let result = Array(0...10, stepSize: 3)
27 | expect(result).to(equal([0, 3, 6, 9]))
28 | }
29 | }
30 |
31 | describe("array with double range") {
32 |
33 | it("handles small step size") {
34 | let result = Array(0.0...10.0, stepSize: 1.0)
35 | expect(result).to(equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
36 | }
37 |
38 | it("handles decimal step size") {
39 | let result = Array(0.0...1.0, stepSize: 0.1)
40 | expect(result).to(equal([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]))
41 | }
42 |
43 | it("handles larger step size") {
44 | let result = Array(0.0...10.0, stepSize: 3.0)
45 | expect(result).to(equal([0, 3, 6, 9]))
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Collections/Collection+ContentTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+ContentTests.swift
3 | // SwiftKitTest
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Collection_ContentTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("Arrays") {
18 |
19 | it("does have content when not empty") {
20 | let value = ["whatever"]
21 | expect(value.hasContent).to(beTrue())
22 | }
23 |
24 | it("does not have content when empty") {
25 | let value = [String]()
26 | expect(value.hasContent).to(beFalse())
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Collections/Collection+DistinctTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+DistinctTests.swift
3 | // SwiftKitTest
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Collection_DistinctTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("distinct") {
18 |
19 | it("remove all duplicated and preserves sorting order") {
20 | let array = [1, 1, 1, 2, 2, 3, 1, 2, 3, 1, 1, 1, 3]
21 | let arrayUnique = array.distinct()
22 | expect(arrayUnique).to(equal([1, 2, 3]))
23 | }
24 |
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Collections/Sequence+BatchedTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sequence+BatchTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2017-05-10.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Sequence_BatchTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("batching array") {
18 |
19 | it("creates single batch if batch size exceeds array size") {
20 | let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
21 | let batch = array.batched(withBatchSize: 20)
22 |
23 | expect(batch.count).to(equal(1))
24 | expect(batch.first!).to(equal(array))
25 | }
26 |
27 | it("creates multiple batches if array size exceeds batch size") {
28 | let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
29 | let batch = array.batched(withBatchSize: 3)
30 |
31 | expect(batch.count).to(equal(4))
32 | expect(batch[0]).to(equal([1, 2, 3]))
33 | expect(batch[1]).to(equal([4, 5, 6]))
34 | expect(batch[2]).to(equal([7, 8, 9]))
35 | expect(batch[3]).to(equal([10]))
36 | }
37 |
38 | it("preserves identability") {
39 | let item1 = TestItem(name: "1")
40 | let item2 = TestItem(name: "2")
41 | let item3 = TestItem(name: "3")
42 | let item4 = TestItem(name: "4")
43 |
44 | let array = [item1, item2, item3, item4]
45 | let batch = array.batched(withBatchSize: 2)
46 |
47 | expect(batch.count).to(equal(2))
48 | expect(batch.last!).to(equal([item3, item4]))
49 | }
50 | }
51 | }
52 | }
53 |
54 | private struct TestItem: Equatable {
55 |
56 | let name: String
57 | }
58 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Collections/Sequence+GroupedTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+GroupTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2017-04-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Array_GroupTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | var array: [TestItem] {
18 | let obj1 = TestItem(name: "Foo", age: 10)
19 | let obj2 = TestItem(name: "Foo", age: 20)
20 | let obj3 = TestItem(name: "Bar", age: 20)
21 | return [obj1, obj2, obj3]
22 | }
23 |
24 | describe("grouping array") {
25 |
26 | it("can group by string") {
27 | let result = array.grouped { $0.name }
28 | expect(result["Foo"]?.count).to(equal(2))
29 | expect(result["Bar"]?.count).to(equal(1))
30 | }
31 |
32 | it("can group by int") {
33 | let result = array.grouped { $0.age }
34 | expect(result[10]?.count).to(equal(1))
35 | expect(result[20]?.count).to(equal(2))
36 | }
37 | }
38 | }
39 | }
40 |
41 | private struct TestItem: Equatable {
42 |
43 | var name: String
44 | var age: Int
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Comparable+ClosestTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+ClosestTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Comparable_ClosestTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("closest in array") {
18 |
19 | it("returns nil for empty array") {
20 | expect(5.closest(in: [], preferred: .greater)).to(beNil())
21 | }
22 |
23 | it("returns value itself if it exists in the collection") {
24 | expect(5.closest(in: [3, 4, 5], preferred: .greater)).to(equal(5))
25 | expect(5.closest(in: [3, 4, 5], preferred: .smaller)).to(equal(5))
26 | }
27 |
28 | context("when greater is preferred") {
29 |
30 | it("returns existing greater value") {
31 | expect(5.closest(in: [6, -10, -1], preferred: .greater)).to(equal(6))
32 | }
33 |
34 | it("returns existing smaller value if greater value doesn't exist") {
35 | expect(5.closest(in: [-10, -1], preferred: .greater)).to(equal(-1))
36 | }
37 | }
38 |
39 | context("when smaller is preferred") {
40 |
41 | it("returns existing smaller value") {
42 | expect(5.closest(in: [6, -10, -1], preferred: .smaller)).to(equal(-1))
43 | }
44 |
45 | it("returns existing greater value if smaller value doesn't exist") {
46 | expect(5.closest(in: [6, 10], preferred: .smaller)).to(equal(6))
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Comparable+LimitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+LimitTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-10-04.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Comparable_LimitTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("limiting comparable") {
18 |
19 | it("doesn't change value in range") {
20 | var value = 5
21 | value.limit(to: 0...10)
22 | expect(value).to(equal(5))
23 | }
24 |
25 | it("caps to min if original value is too low") {
26 | var value = 5
27 | value.limit(to: 6...10)
28 | expect(value).to(equal(6))
29 | }
30 |
31 | it("caps tp max if original value is too great") {
32 | var value = 5
33 | value.limit(to: 0...4)
34 | expect(value).to(equal(4))
35 | }
36 | }
37 |
38 | describe("limited result for comparable") {
39 |
40 | it("is original value in range") {
41 | expect(5.limited(to: 0...10)).to(equal(5))
42 | }
43 |
44 | it("is min value if original value is too low") {
45 | expect((-1).limited(to: 0...10)).to(equal(0))
46 | }
47 |
48 | it("is max value if original value is too great") {
49 | expect(11.limited(to: 0...10)).to(equal(10))
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/ComparisonResult+ShortcutsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComparisonResult+ShortcutsTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class ComparisonResult_ShortcutsTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("order shortcut") {
19 |
20 | it("has correct values") {
21 | expect(ComparisonResult.ascending).to(equal(.orderedAscending))
22 | expect(ComparisonResult.descending).to(equal(.orderedDescending))
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/DispatchQueue+AsyncTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DispatchQueue+AsyncTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-02.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class DispatchQueue_AsyncTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | let queue = DispatchQueue.main
19 |
20 | describe("async after time interval") {
21 |
22 | it("supports delaying for custom time interval") {
23 | var count = 0
24 | queue.asyncAfter(.microseconds(1)) { count += 1 }
25 | queue.asyncAfter(.milliseconds(1)) { count += 1 }
26 | expect(count).toEventually(equal(2))
27 | }
28 | }
29 |
30 | describe("async then") {
31 |
32 | it("supports chaining void block") {
33 | var count = 0
34 | queue.async(execute: { count += 1 }, then: { count += 1 })
35 | expect(count).toEventually(equal(2))
36 | }
37 |
38 | it("supports chaining generic block") {
39 | var count = 0
40 | queue.async(execute: { 1 }, then: { count += $0 })
41 | expect(count).toEventually(equal(1))
42 | }
43 |
44 | it("supports concatenating results") {
45 | var result = ""
46 | queue.async(
47 | execute: { "Hello"},
48 | then: { result = $0 + ", world!" },
49 | on: .main
50 | )
51 | expect(result).toEventually(equal("Hello, world!"))
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Optional+IsSetTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Optional+IsSetTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class Optional_HasValue: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("optional") {
18 |
19 | var value: String?
20 |
21 | beforeEach {
22 | value = nil
23 | }
24 |
25 | it("is set if not nil") {
26 | value = "value"
27 | expect(value.isSet).to(beTrue())
28 | expect(value.isNil).to(beFalse())
29 | }
30 |
31 | it("is not set if value is nil") {
32 | expect(value.isNil).to(beTrue())
33 | expect(value.isSet).to(beFalse())
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Result+UtilsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Result+UtilsTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-02.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Result_UtilsTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | let success = Result.success(true)
19 | let failure = Result.failure(.kaboom)
20 |
21 | describe("failureError") {
22 |
23 | it("is only set for failure results") {
24 | expect(success.failureError).to(beNil())
25 | expect(failure.failureError).to(equal(.kaboom))
26 | }
27 | }
28 |
29 | describe("is failure") {
30 |
31 | it("is only true for failure results") {
32 | expect(success.isFailure).to(beFalse())
33 | expect(failure.isFailure).to(beTrue())
34 | }
35 | }
36 |
37 | describe("is success") {
38 |
39 | it("is only true for success results") {
40 | expect(success.isSuccess).to(beTrue())
41 | expect(failure.isSuccess).to(beFalse())
42 | }
43 | }
44 |
45 | describe("successResult") {
46 |
47 | it("is only set for success results") {
48 | expect(success.successResult).to(beTrue())
49 | expect(failure.successResult).to(beNil())
50 | }
51 | }
52 | }
53 | }
54 |
55 | private enum TestError: Error {
56 |
57 | case kaboom
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+Base64Tests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Base64Tests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-12-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_Base64Tests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("string base64") {
18 |
19 | it("encodes empty string") {
20 | let string = ""
21 | let result = string.base64Encoded()!
22 | expect(result).to(equal(""))
23 | }
24 |
25 | it("encodes and decodes regular string") {
26 | let string = "foo bar"
27 | let result = string.base64Encoded()!.base64Decoded()
28 | expect(result).to(equal(string))
29 | }
30 |
31 | it("encodes and decodes swedish chars") {
32 | let string = "ÅÄÖ åäö"
33 | let result = string.base64Encoded()!.base64Decoded()
34 | expect(result).to(equal(string))
35 | }
36 |
37 | it("encodes and decodes strange device name") {
38 | let string = "saaaandii ♡"
39 | let result = string.base64Encoded()!.base64Decoded()
40 | expect(result).to(equal(string))
41 | }
42 |
43 | it("encodes and decodes emojis") {
44 | let string = "😁😂😃"
45 | let result = string.base64Encoded()!.base64Decoded()
46 | expect(result).to(equal(string))
47 | }
48 |
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+BoolTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+BoolTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2021-11-02.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_BoolTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("bool value") {
18 |
19 | func result(for string: String) -> Bool {
20 | string.boolValue
21 | }
22 |
23 | it("is valid for many different true expressions") {
24 | let expected = ["YES", "yes", "1"]
25 | expected.forEach {
26 | expect(result(for: $0)).to(beTrue())
27 | }
28 | }
29 |
30 | it("is valid for many different false expressions") {
31 | let expected = ["NO", "no", "0"]
32 | expected.forEach {
33 | expect(result(for: $0)).to(beFalse())
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+ContainsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+ContainsTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-12-13.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_ContainsTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("checking if string contains another string") {
18 |
19 | context("with case-sensitive check") {
20 |
21 | it("finds existing string") {
22 | let result = "foo".contains("foo", caseSensitive: true)
23 | expect(result).to(beTrue())
24 | }
25 |
26 | it("does not find non-existing string") {
27 | let result = "foo".contains("foO", caseSensitive: true)
28 | expect(result).to(beFalse())
29 | }
30 | }
31 |
32 | context("with case-insensitive check") {
33 |
34 | it("finds existing string case-sensitively") {
35 | let result = "foo".contains("foo", caseSensitive: false)
36 | expect(result).to(beTrue())
37 | }
38 |
39 | it("finds existing string case-insensitively") {
40 | let result = "foo".contains("foO", caseSensitive: false)
41 | expect(result).to(beTrue())
42 | }
43 |
44 | it("does not find non-existing string case-sensitively") {
45 | let result = "foo".contains("foot", caseSensitive: false)
46 | expect(result).to(beFalse())
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+ContentTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+ContentTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-04.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_ContentTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("has content") {
18 |
19 | it("is false if string is empty") {
20 | expect("".hasContent).to(beFalse())
21 | }
22 |
23 | it("is true if string is not empty") {
24 | expect(" ".hasContent).to(beTrue())
25 | }
26 | }
27 |
28 | describe("has trimmed content") {
29 |
30 | it("is false if string is empty") {
31 | expect("".hasTrimmedContent).to(beFalse())
32 | }
33 |
34 | it("is false if trimmed string is empty") {
35 | expect(" ".hasTrimmedContent).to(beFalse())
36 | }
37 |
38 | it("is true if trimmed string is not empty") {
39 | expect(" . ".hasTrimmedContent).to(beTrue())
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+ParagraphTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+ParagraphTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2021-11-29.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_ParagraphTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | let none = "foo bar baz"
18 | let single = "foo\nbar baz"
19 | let multi = "foo\nbar\rbaz"
20 |
21 | describe("index of current paragraph") {
22 |
23 | func result(for string: String, from location: UInt) -> UInt {
24 | string.findIndexOfCurrentParagraph(from: location)
25 | }
26 |
27 | it("is correct for empty string") {
28 | expect(result(for: "", from: 0)).to(equal(0))
29 | expect(result(for: "", from: 20)).to(equal(0))
30 | }
31 |
32 | it("is correct for string with no previous paragraph") {
33 | expect(result(for: none, from: 0)).to(equal(0))
34 | expect(result(for: none, from: 10)).to(equal(0))
35 | expect(result(for: none, from: 20)).to(equal(0))
36 | }
37 |
38 | it("is correct for string with single previous paragraph") {
39 | expect(result(for: single, from: 0)).to(equal(0))
40 | expect(result(for: single, from: 5)).to(equal(4))
41 | expect(result(for: single, from: 10)).to(equal(4))
42 | }
43 |
44 | it("is correct for string with multiple previous paragraphs") {
45 | expect(result(for: multi, from: 0)).to(equal(0))
46 | expect(result(for: multi, from: 5)).to(equal(4))
47 | expect(result(for: multi, from: 10)).to(equal(8))
48 | }
49 | }
50 |
51 | describe("index of next paragraph") {
52 |
53 | func result(for string: String, from location: UInt) -> UInt {
54 | string.findIndexOfNextParagraph(from: location)
55 | }
56 |
57 | it("is correct for empty string") {
58 | expect(result(for: "", from: 0)).to(equal(0))
59 | expect(result(for: "", from: 20)).to(equal(0))
60 | }
61 |
62 | it("is correct for string with no next paragraph") {
63 | expect(result(for: none, from: 0)).to(equal(0))
64 | expect(result(for: none, from: 10)).to(equal(0))
65 | expect(result(for: none, from: 20)).to(equal(0))
66 | }
67 |
68 | it("is correct for string with single next paragraph") {
69 | expect(result(for: single, from: 0)).to(equal(4))
70 | expect(result(for: single, from: 5)).to(equal(4))
71 | expect(result(for: single, from: 10)).to(equal(4))
72 | }
73 |
74 | it("is correct for string with multiple next paragraphs") {
75 | expect(result(for: multi, from: 0)).to(equal(4))
76 | expect(result(for: multi, from: 5)).to(equal(8))
77 | expect(result(for: multi, from: 10)).to(equal(8))
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+ReplaceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+ReplaceTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-12-13.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_ReplaceTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("replacing string with another string") {
18 |
19 | let string = "Hello, world!"
20 |
21 | it("does not replace anything if no match was found") {
22 | expect(string.replacing("World", with: "you")).to(equal(string))
23 | expect(string.replacing("World", with: "you", caseSensitive: true)).to(equal(string))
24 | expect(string.replacing("Earth", with: "you", caseSensitive: false)).to(equal(string))
25 | }
26 |
27 | it("can perform case-sensitive replace") {
28 | expect(string.replacing("world", with: "you")).to(equal("Hello, you!"))
29 | expect(string.replacing("world", with: "you", caseSensitive: true)).to(equal("Hello, you!"))
30 | }
31 |
32 | it("can perform case-insensitive replace") {
33 | expect(string.replacing("World", with: "you", caseSensitive: false)).to(equal("Hello, you!"))
34 | expect(string.replacing("world", with: "you", caseSensitive: false)).to(equal("Hello, you!"))
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+SplitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+SplitTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2021-09-08.
6 | // Copyright © 2021 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_SplitTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("split by separators") {
18 |
19 | it("splits on all provided separators") {
20 | let string = "I.Love,Swift!Very much"
21 | let result = string.split(by: [".", ",", "!"])
22 | let expected = ["I", "Love", "Swift", "Very much"]
23 | expect(result).to(equal(expected))
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/String/String+UrlEncodeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+UrlEncodeTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2016-12-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class String_UrlEncodeTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("url encoded string") {
18 |
19 | it("handles space") {
20 | expect("foo bar".urlEncoded()).to(equal("foo%20bar"))
21 | }
22 |
23 | it("handles question mark") {
24 | expect("?foo=bar".urlEncoded()).to(equal("%3Ffoo=bar"))
25 | }
26 |
27 | it("handles ampersand") {
28 | expect("foo=bar&baz=123".urlEncoded()).to(equal("foo=bar%26baz=123"))
29 | }
30 |
31 | it("handles square brackets") {
32 | expect("foo=[bar]".urlEncoded()).to(equal("foo=%5Bbar%5D"))
33 | }
34 |
35 | it("handles swedish chars") {
36 | expect("åäöÅÄÖ".urlEncoded()).to(equal("%C3%A5%C3%A4%C3%B6%C3%85%C3%84%C3%96"))
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/Url+GlobalTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Url+GlobalTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2012-08-31.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Url_GlobalTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("global urls") {
19 |
20 | describe("app store url") {
21 |
22 | it("is valid") {
23 | let url = URL.appStoreUrl(forAppId: 123)
24 | expect(url?.absoluteString).to(equal("https://itunes.apple.com/app/id\(123)"))
25 | }
26 | }
27 |
28 | describe("user subscriptions url") {
29 |
30 | it("is valid") {
31 | let url = URL.userSubscriptions
32 | expect(url?.absoluteString).to(equal("https://apps.apple.com/account/subscriptions"))
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Extensions/UserDefaults+CodableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+CodableTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-09-23.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class UserDefaults_CodableTets: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | var defaults: UserDefaults!
19 | let key = "user"
20 |
21 | beforeEach {
22 | defaults = UserDefaults.standard
23 | }
24 |
25 | afterEach {
26 | defaults.removeObject(forKey: key)
27 | }
28 |
29 | describe("getting codable") {
30 |
31 | it("returns nil if no data is persisted") {
32 | let result: User? = defaults.codable(forKey: key)
33 | expect(result).to(beNil())
34 | }
35 |
36 | it("returns nil if invalid data is persisted") {
37 | defaults.set("HEJ", forKey: key)
38 | let result: User? = defaults.codable(forKey: key)
39 | expect(result).to(beNil())
40 | }
41 |
42 | it("returns correctly persisted data") {
43 | let user = User(name: "Daniel", age: 40)
44 | defaults.setCodable(user, forKey: key)
45 | let result: User = defaults.codable(forKey: key)!
46 | expect(result).to(equal(user))
47 | }
48 | }
49 | }
50 | }
51 |
52 | private struct User: Codable, Equatable {
53 |
54 | let name: String
55 | let age: Int
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Geo/CLLocationCoordinate2D+ValidTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLLocationCoordinate2D+ValidTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2018-10-19.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 | import CoreLocation
13 |
14 | class CLLocationCoordinate2D_ValidTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("Coordinate validation") {
19 |
20 | func result(for lat: CLLocationDegrees, _ long: CLLocationDegrees) -> Bool {
21 | CLLocationCoordinate2D(latitude: lat, longitude: long).isValid
22 | }
23 |
24 | it("is invalid if latitude is invalid") {
25 | expect(result(for: 0, 120)).to(beFalse())
26 | expect(result(for: 180, 120)).to(beFalse())
27 | expect(result(for: -180, 120)).to(beFalse())
28 | }
29 |
30 | it("is invalid if longitude is invalid") {
31 | expect(result(for: 120, 0)).to(beFalse())
32 | expect(result(for: 120, 180)).to(beFalse())
33 | expect(result(for: 120, -180)).to(beFalse())
34 | }
35 |
36 | it("is valid if both components are valid") {
37 | expect(result(for: 120, 120)).to(beTrue())
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Geo/WorldCoordinateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorldCoordinateTests.swift
3 | // iExtraTests
4 | //
5 | // Created by Daniel Saidi on 2018-10-19.
6 | // Copyright © 2018 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import CoreLocation
12 | import SwiftKit
13 |
14 | class WorldCoordinateTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("world coordinate") {
19 |
20 | func validate(_ coordinate: WorldCoordinate, _ lat: CLLocationDegrees, _ long: CLLocationDegrees) -> Bool {
21 | let coord = coordinate.coordinate
22 | return coord.latitude == lat && coord.longitude == long
23 | }
24 |
25 | it("has valid coordinate") {
26 | expect(validate(.manhattan, 40.7590615, -73.969231)).to(beTrue())
27 | expect(validate(.newYork, 40.7033127, -73.979681)).to(beTrue())
28 | expect(validate(.sanFrancisco, 37.7796828, -122.4000062)).to(beTrue())
29 | expect(validate(.tokyo, 35.673, 139.710)).to(beTrue())
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Numerics/Decimal+DoubleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Decimal_DoubleTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Decimal_DoubleTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("double value") {
19 |
20 | func result(for value: Decimal) -> Double {
21 | value.doubleValue
22 | }
23 |
24 | it("handles various decimals") {
25 | expect(result(for: 1)).to(equal(1))
26 | expect(result(for: 1.2)).to(equal(1.2))
27 | expect(result(for: 1.23)).to(equal(1.23))
28 | expect(result(for: 1.234)).to(equal(1.234))
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Numerics/Double+RoundedTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double_RoundedTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-12.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Double_RoundedTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("rounded with decimals") {
19 |
20 | func result(for value: Double, _ decimals: Int) -> Double {
21 | value.roundedWithDecimals(decimals)
22 | }
23 |
24 | it("handles various decimals") {
25 | let value = 1.23456789
26 | expect(result(for: value, 0)).to(equal(1))
27 | expect(result(for: value, 1)).to(equal(1.2))
28 | expect(result(for: value, 2)).to(equal(1.23))
29 | expect(result(for: value, 3)).to(equal(1.235))
30 | }
31 | }
32 |
33 | describe("rounded with precision") {
34 |
35 | func result(for value: Double, _ precision: Double) -> Double {
36 | value.roundedWithPrecision(from: precision)
37 | }
38 |
39 | it("handles various decimals") {
40 | let value = 1.23456789
41 | expect(result(for: value, 1)).to(equal(1))
42 | expect(result(for: value, 1.2)).to(equal(1.2))
43 | expect(result(for: value, 1.23)).to(equal(1.23))
44 | }
45 |
46 | it("fails for higher precision") {
47 | let value = 1.23456789
48 | expect(result(for: value, 1.234)).to(equal(2.468))
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Numerics/NumberFormatter+InitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberFormatter+InitTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-10-19.
6 | // Copyright © 2012 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftKit
10 | import XCTest
11 |
12 | class NumberFormatter_InitTests: XCTestCase {
13 |
14 | func testConvenienceInitializerUsesUsEnglishByDefault() {
15 | let formatter = NumberFormatter(numberStyle: .currency)
16 | XCTAssertEqual(formatter.locale.identifier, "en-US")
17 | XCTAssertEqual(formatter.numberStyle, .currency)
18 | }
19 |
20 | func testConvenienceInitializerCanEnforceFixedDecimals() {
21 | let formatter = NumberFormatter(numberStyle: .percent, fixedDecimals: 2)
22 | XCTAssertEqual(formatter.locale.identifier, "en-US")
23 | XCTAssertEqual(formatter.numberStyle, .percent)
24 | XCTAssertEqual(formatter.minimumFractionDigits, 2)
25 | XCTAssertEqual(formatter.maximumFractionDigits, 2)
26 | }
27 |
28 | func testConvenienceInstanceGeneratesValidDateStringForDollars() {
29 | let value = 123_456_789.123
30 | let formatter = NumberFormatter(numberStyle: .currency)
31 | let result = formatter.string(from: NSNumber(value: value))
32 | XCTAssertEqual(result, "$123,456,789.12")
33 | }
34 |
35 | func testConvenienceInstanceGeneratesValidDateStringForSwedishKrona() {
36 | let value = 123_456_789.123
37 | let locale = Locale(identifier: "sv-SE")
38 | let formatter = NumberFormatter(numberStyle: .currency, locale: locale)
39 | let result = formatter.string(from: NSNumber(value: value))
40 | XCTAssertEqual(result, "123 456 789,12 kr")
41 | }
42 |
43 | func testPercentFormatterGeneratesValidStringWithTwoDecimals() {
44 | let value = 0.09156
45 | let formatter = NumberFormatter.percent(decimals: 2)
46 | let result = formatter.string(from: NSNumber(value: value))
47 | XCTAssertEqual(result, "9.16%")
48 | }
49 |
50 | func testPercentFormatterGeneratesValidStringWithZeroDecimals() {
51 | let value = 0.09156
52 | let formatter = NumberFormatter.percent(decimals: 0)
53 | let result = formatter.string(from: NSNumber(value: value))
54 | XCTAssertEqual(result, "9%")
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Numerics/NumberFormatter+UtilTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberFormatter+UtilTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2022-10-19.
6 | // Copyright © 2012 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftKit
10 | import XCTest
11 |
12 | class NumberFormatter_UtilTests: XCTestCase {
13 |
14 | func testStringForDoubleReturnsValidResult() {
15 | let value = 0.09156
16 | let formatter = NumberFormatter.percent(decimals: 2)
17 | let result = formatter.string(for: value)
18 | XCTAssertEqual(result, "9.16%")
19 | }
20 |
21 | func testStringForIntReturnsValidResult() {
22 | let value: Int = 9
23 | let formatter = NumberFormatter.percent(decimals: 2)
24 | let result = formatter.string(for: value)
25 | XCTAssertEqual(result, "900.00%")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Numerics/Numeric+ConversionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Numeric+ConversionsTests.swift
3 | // SwiftKit
4 | //
5 | // Created by Daniel Saidi on 2020-08-05.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import CoreGraphics
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Numeric_ConversionsTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("cg float") {
19 |
20 | it("can be converted to other formats") {
21 | let value: CGFloat = 123.456
22 | expect(value.toDouble()).to(equal(123.456))
23 | expect(value.toFloat()).to(equal(123.456))
24 | expect(value.toInt()).to(equal(123))
25 | }
26 | }
27 |
28 | describe("double") {
29 |
30 | it("can be converted to other formats") {
31 | let value: Double = 123.456
32 | expect(value.toCGFloat()).to(equal(123.456))
33 | expect(value.toFloat()).to(equal(123.456))
34 | expect(value.toInt()).to(equal(123))
35 | }
36 | }
37 |
38 | describe("float") {
39 |
40 | it("can be converted to other formats") {
41 | let value: Float = 123.456
42 | expect(value.toCGFloat()).to(beCloseTo(123.456))
43 | expect(value.toDouble()).to(beCloseTo(123.456))
44 | expect(value.toInt()).to(equal(123))
45 | }
46 | }
47 |
48 | describe("int") {
49 |
50 | it("can be converted to other formats") {
51 | let value: Int = 123
52 | expect(value.toCGFloat()).to(equal(123.000))
53 | expect(value.toDouble()).to(equal(123.000))
54 | expect(value.toFloat()).to(equal(123.000))
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Numerics/Numeric+StringTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Numeric+FormatTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2015-11-15.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Quick
11 | import Nimble
12 | import SwiftKit
13 |
14 | class Numeric_FormatTests: QuickSpec {
15 |
16 | override func spec() {
17 |
18 | describe("string with decimals") {
19 |
20 | let value = 123.456789
21 | let values: [NumericStringRepresentable] = [
22 | value,
23 | Float(value)
24 | ]
25 |
26 | func result(for decimals: Int) -> [String] {
27 | values.map { $0.string(withDecimals: decimals) }
28 | }
29 |
30 |
31 | it("handles no decimals") {
32 | result(for: 0).forEach {
33 | expect($0).to(equal("123"))
34 | }
35 | }
36 |
37 | it("handles one decimal") {
38 | result(for: 1).forEach {
39 | expect($0).to(equal("123.5"))
40 | }
41 | }
42 |
43 | it("handles two decimals") {
44 | result(for: 2).forEach {
45 | expect($0).to(equal("123.46"))
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/SwiftKitTests/Validation/EmailValidatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerformAsyncTests.swift
3 | // SwiftKitTests
4 | //
5 | // Created by Daniel Saidi on 2020-06-09.
6 | // Copyright © 2020 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | import SwiftKit
12 |
13 | class EmailValidatorTests: QuickSpec {
14 |
15 | override func spec() {
16 |
17 | describe("email validator") {
18 |
19 | let validate = EmailValidator().validate
20 |
21 | context("when validating validating valid addresses") {
22 |
23 | it("validates valid email addresses") {
24 | expect(validate("foobar@baz.com")).to(beTrue())
25 | expect(validate("foo1.bar2@baz.com")).to(beTrue())
26 | expect(validate("foo.bar@gmail.com")).to(beTrue())
27 | }
28 |
29 | it("validates long top domains") {
30 | expect(validate("foobar@baz.co")).to(beTrue())
31 | expect(validate("foobar@baz.com")).to(beTrue())
32 | expect(validate("foo1.bar2@baz.comm")).to(beTrue())
33 | expect(validate("foo.bar@gmail.commmmmmmmmmmmmmmm")).to(beTrue())
34 | }
35 | }
36 |
37 | context("when validating invalid addresses") {
38 |
39 | it("does not validate invalid email addresses") {
40 | expect(validate("foobar")).to(beFalse())
41 | expect(validate("foo1.bar2@")).to(beFalse())
42 | expect(validate("foo.bar@gmail")).to(beFalse())
43 | }
44 |
45 | it("does not validate too short top domains") {
46 | expect(validate("foobar@baz.c")).to(beFalse())
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------