├── .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 | SwiftKit Logo 3 |

4 | 5 |

6 | Version 7 | Swift 5.6 8 | MIT License 9 | 10 | Twitter: @danielsaidi 11 | 12 | 13 | Mastodon: @danielsaidi@mastodon.social 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 | ![SwiftKit logo](Logo.png) 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 | --------------------------------------------------------------------------------