├── Package.resolved ├── LICENSE ├── Sources └── GenericID │ ├── StaticKey.swift │ ├── DefaultConstructible.swift │ ├── LazyReference.swift │ ├── UINibGettable.swift │ ├── DataCoder.swift │ ├── OptionalType.swift │ ├── NSUbiquitousKeyValueStore.swift │ ├── AssociatedObject.swift │ ├── UIStoryboard.swift │ ├── DefaultsKey.swift │ ├── DefaultsValueTransformer.swift │ ├── UserDefaults.swift │ ├── UICollectionView.swift │ ├── UITableView.swift │ ├── DefaultsObservation.swift │ └── DefaultsPublisher.swift ├── Package.swift ├── Tests └── GenericIDTests │ ├── TestKeys.swift │ ├── AssociatedObjectTests.swift │ └── UserDefaultesTests.swift └── README.md /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CXShim", 6 | "repositoryURL": "https://github.com/cx-org/CXShim", 7 | "state": { 8 | "branch": null, 9 | "revision": "82fecc246c7ca9f0ac1656b8199b7956e64d68f3", 10 | "version": "0.4.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Xander Deng 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Sources/GenericID/StaticKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticKey.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | public class StaticKeyBase { 21 | 22 | let key: String 23 | 24 | init(_ key: String) { 25 | self.key = key 26 | } 27 | } 28 | 29 | extension StaticKeyBase: Hashable { 30 | 31 | public func hash(into hasher: inout Hasher) { 32 | hasher.combine(key) 33 | } 34 | 35 | public static func ==(lhs: StaticKeyBase, rhs: StaticKeyBase) -> Bool { 36 | return lhs.key == rhs.key 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/GenericID/DefaultConstructible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultConstructible.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | public protocol DefaultConstructible { 21 | init() 22 | } 23 | 24 | extension Bool: DefaultConstructible {} 25 | extension Int: DefaultConstructible {} 26 | extension Float: DefaultConstructible {} 27 | extension Double: DefaultConstructible {} 28 | extension String: DefaultConstructible {} 29 | extension Data: DefaultConstructible {} 30 | extension Array: DefaultConstructible {} 31 | extension Dictionary: DefaultConstructible {} 32 | 33 | extension Optional: DefaultConstructible { 34 | public init() { 35 | self = .none 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Sources/GenericID/LazyReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyReference.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | private enum LazyValue { 21 | 22 | case pending(() -> T) 23 | case evaluated(T) 24 | 25 | mutating func value() -> T { 26 | switch self { 27 | case let .pending(block): 28 | let v = block() 29 | self = .evaluated(v) 30 | return v 31 | case let .evaluated(v): 32 | return v 33 | } 34 | } 35 | } 36 | 37 | class LazyReference { 38 | 39 | private var _value: LazyValue 40 | 41 | init(_ block: @escaping () -> T) { 42 | _value = .pending(block) 43 | } 44 | 45 | var value: T { 46 | return _value.value() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/GenericID/UINibGettable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINibGettable.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | #if os(iOS) || os(tvOS) 21 | 22 | import UIKit 23 | 24 | public protocol UINibGettable: class { 25 | static var nibName: String { get } 26 | } 27 | 28 | extension UINibGettable { 29 | public static var nib: UINib { 30 | return UINib(nibName: nibName, bundle: Bundle(for: Self.self)) 31 | } 32 | } 33 | 34 | protocol UINibFromTypeGettable: UINibGettable { 35 | associatedtype NibType 36 | } 37 | 38 | extension UINibFromTypeGettable { 39 | public static var nibName: String { 40 | return String(describing: NibType.self) 41 | } 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/GenericID/DataCoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCoder.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | protocol DataEncoder { 21 | func encode(_ value: T) throws -> Data where T : Encodable 22 | } 23 | 24 | protocol DataDecoder { 25 | func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable 26 | } 27 | 28 | extension Encodable { 29 | func encodedData(encoder: DataEncoder) throws -> Data { 30 | return try encoder.encode(self) 31 | } 32 | } 33 | 34 | extension Decodable { 35 | init(data: Data, decoder: DataDecoder) throws { 36 | self = try decoder.decode(Self.self, from: data) 37 | } 38 | } 39 | 40 | extension JSONEncoder: DataEncoder {} 41 | extension JSONDecoder: DataDecoder {} 42 | 43 | extension PropertyListEncoder: DataEncoder {} 44 | extension PropertyListDecoder: DataDecoder {} 45 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "GenericID", 7 | products: [ 8 | .library( 9 | name: "GenericID", 10 | targets: ["GenericID"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/cx-org/CXShim", .upToNextMinor(from: "0.4.0")) 14 | ], 15 | targets: [ 16 | .target(name: "GenericID", dependencies: ["CXShim"]), 17 | .testTarget(name: "GenericIDTests", dependencies: ["GenericID"]), 18 | ] 19 | ) 20 | 21 | enum CombineImplementation { 22 | 23 | case combine 24 | case combineX 25 | case openCombine 26 | 27 | static var `default`: CombineImplementation { 28 | #if canImport(Combine) 29 | return .combine 30 | #else 31 | return .combineX 32 | #endif 33 | } 34 | 35 | init?(_ description: String) { 36 | let desc = description.lowercased().filter { $0.isLetter } 37 | switch desc { 38 | case "combine": self = .combine 39 | case "combinex": self = .combineX 40 | case "opencombine": self = .openCombine 41 | default: return nil 42 | } 43 | } 44 | } 45 | 46 | extension ProcessInfo { 47 | 48 | var combineImplementation: CombineImplementation { 49 | return environment["CX_COMBINE_IMPLEMENTATION"].flatMap(CombineImplementation.init) ?? .default 50 | } 51 | } 52 | 53 | import Foundation 54 | 55 | let combineImpl = ProcessInfo.processInfo.combineImplementation 56 | 57 | if combineImpl == .combine { 58 | package.platforms = [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)] 59 | } 60 | -------------------------------------------------------------------------------- /Sources/GenericID/OptionalType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalType.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | protocol AnyOptionalType { 19 | 20 | var anyWrapped: Any? { get } 21 | 22 | static var anyWrappedType: Any.Type { get } 23 | } 24 | 25 | protocol OptionalType: AnyOptionalType { 26 | 27 | associatedtype WrappedType 28 | 29 | var wrapped: WrappedType? { get } 30 | 31 | init(_ wrapped: WrappedType?) 32 | } 33 | 34 | extension OptionalType { 35 | 36 | var anyWrapped: Any? { 37 | return wrapped 38 | } 39 | 40 | static var anyWrappedType: Any.Type { 41 | return WrappedType.self 42 | } 43 | } 44 | 45 | extension Optional: OptionalType { 46 | 47 | var wrapped: Wrapped? { 48 | return self 49 | } 50 | 51 | init(_ wrapped: WrappedType?) { 52 | self = wrapped 53 | } 54 | } 55 | 56 | func unwrapRecursively(_ v: Any) -> Any? { 57 | if let wrapped = (v as? AnyOptionalType)?.anyWrapped { 58 | return unwrapRecursively(wrapped) 59 | } else { 60 | return v 61 | } 62 | } 63 | 64 | func unwrapRecursively(_ t: Any.Type) -> Any.Type { 65 | if let wrapped = (t as? AnyOptionalType.Type)?.anyWrappedType { 66 | return unwrapRecursively(wrapped) 67 | } else { 68 | return t 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/GenericID/NSUbiquitousKeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSUbiquitousKeyValueStore.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | @available(watchOS, unavailable) 21 | extension NSUbiquitousKeyValueStore { 22 | 23 | public typealias StoreKeys = UserDefaults.DefaultsKeys 24 | public typealias StoreKey = StoreKeys.Key 25 | 26 | public func contains(_ key: StoreKey) -> Bool { 27 | return object(forKey: key.key) != nil 28 | } 29 | 30 | public func remove(_ key: StoreKey) { 31 | removeObject(forKey: key.key) 32 | } 33 | 34 | public func removeAll() { 35 | for (key, _) in dictionaryRepresentation { 36 | removeObject(forKey: key) 37 | } 38 | } 39 | 40 | public subscript(_ key: StoreKey) -> T? { 41 | get { 42 | return object(forKey: key.key).flatMap(key.deserialize) 43 | } 44 | set { 45 | set(newValue.flatMap(key.serialize), forKey: key.key) 46 | } 47 | } 48 | 49 | public subscript(_ key: StoreKey) -> T { 50 | get { 51 | return object(forKey: key.key).flatMap(key.deserialize) ?? T() 52 | } 53 | set { 54 | // T might be optional and holds a `nil`, which will be bridged to `NSNull` 55 | // and cannot be stored in NSUbiquitousKeyValueStore. We must unwrap it manually. 56 | set(unwrapRecursively(newValue).flatMap(key.serialize), forKey: key.key) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/GenericID/AssociatedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObject.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | extension NSObject { 21 | 22 | public typealias AssociateKey = AssociateKeys.Key 23 | 24 | public class AssociateKeys: StaticKeyBase {} 25 | } 26 | 27 | extension NSObject.AssociateKeys { 28 | 29 | public final class Key: NSObject.AssociateKeys, RawRepresentable, ExpressibleByStringLiteral { 30 | 31 | public var rawValue: String { 32 | return key 33 | } 34 | 35 | public init(rawValue: String) { 36 | super.init(rawValue) 37 | } 38 | 39 | public init(stringLiteral value: String) { 40 | super.init(value) 41 | } 42 | } 43 | } 44 | 45 | extension NSObject.AssociateKey { 46 | 47 | public var opaqueKey: UnsafeRawPointer! { 48 | return UnsafeRawPointer.init(bitPattern: hashValue) 49 | } 50 | } 51 | 52 | extension NSObject { 53 | 54 | public subscript(_ associated: AssociateKeys.Key) -> T? { 55 | get { 56 | return objc_getAssociatedObject(self, associated.opaqueKey) as? T 57 | } 58 | set { 59 | objc_setAssociatedObject(self, associated.opaqueKey, newValue, .OBJC_ASSOCIATION_RETAIN) 60 | } 61 | } 62 | 63 | public func removeAssociateValue(for key: AssociateKey) { 64 | objc_setAssociatedObject(self, key.opaqueKey, nil, .OBJC_ASSOCIATION_ASSIGN) 65 | } 66 | 67 | public func removeAssociateValues() { 68 | objc_removeAssociatedObjects(self) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/GenericID/UIStoryboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStoryboard.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | #if os(iOS) || os(tvOS) 19 | 20 | import UIKit 21 | 22 | extension UIStoryboard { 23 | 24 | public typealias Identifier = Identifiers.ID where T: UIViewController 25 | 26 | public class Identifiers: StaticKeyBase {} 27 | } 28 | 29 | extension UIStoryboard.Identifiers { 30 | 31 | public final class ID: UIStoryboard.Identifiers, RawRepresentable, ExpressibleByStringLiteral { 32 | 33 | public var rawValue: String { 34 | return key 35 | } 36 | 37 | public init(rawValue: String) { 38 | super.init(rawValue) 39 | } 40 | 41 | public init(stringLiteral value: String) { 42 | super.init(value) 43 | } 44 | } 45 | } 46 | 47 | extension UIStoryboard { 48 | 49 | public func instantiateViewController(withIdentifier identifier: Identifier) -> T { 50 | guard let vc = instantiateViewController(withIdentifier: identifier.rawValue) as? T else { 51 | fatalError("instantiate view controller '\(identifier.rawValue)' of '\(self)' is not of class '\(T.self)'") 52 | } 53 | return vc 54 | } 55 | } 56 | 57 | extension UIStoryboard { 58 | 59 | open class func main() -> UIStoryboard { 60 | guard let mainStoryboardName = Bundle.main.infoDictionary?["UIMainStoryboardFile"] as? String else { 61 | fatalError("No UIMainStoryboardFile found in main bundle") 62 | } 63 | return UIStoryboard(name: mainStoryboardName, bundle: .main) 64 | } 65 | } 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /Tests/GenericIDTests/TestKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestKeys.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | #if os(macOS) 19 | import Cocoa 20 | typealias Color = NSColor 21 | #else 22 | import UIKit 23 | typealias Color = UIColor 24 | #endif 25 | 26 | extension UserDefaults.DefaultsKeys { 27 | static let BoolKey = Key("BoolKey") 28 | static let IntKey = Key("IntKey") 29 | static let FloatKey = Key("FloatKey") 30 | static let DoubleKey = Key("DoubleKey") 31 | static let StringKey = Key("StringKey") 32 | static let DataKey = Key("DataKey") 33 | static let ArrayKey = Key<[Any]>("ArrayKey") 34 | static let StringArrayKey = Key<[String]>("StringArrayKey") 35 | static let DictionaryKey = Key<[String: Any]>("DictionaryKey") 36 | } 37 | 38 | extension UserDefaults.DefaultsKeys { 39 | static let BoolOptKey = Key("BoolOptKey") 40 | static let IntOptKey = Key("IntOptKey") 41 | static let FloatOptKey = Key("FloatOptKey") 42 | static let DoubleOptKey = Key("DoubleOptKey") 43 | static let StringOptKey = Key("StringOptKey") 44 | static let URLOptKey = Key("URLOptKey", transformer: .keyedArchive) 45 | static let DateOptKey = Key("DateOptKey") 46 | static let DataOptKey = Key("DataOptKey") 47 | static let ArrayOptKey = Key<[Any]?>("ArrayOptKey") 48 | static let StringArrayOptKey = Key<[String]?>("StringArrayOptKey") 49 | static let DictionaryOptKey = Key<[String: Any]?>("DictionaryOptKey") 50 | static let ColorOptKey = Key("ColorOptKey", transformer: .keyedArchive) 51 | static let RectOptKey = Key("RectOptKey", transformer: .json) 52 | static let AnyOptKey = Key("AnyOptKey") 53 | } 54 | 55 | extension NSObject.AssociateKeys { 56 | static let ValueTypeKey: Key = "ValueTypeKey" 57 | static let ReferenceTypeKey: Key = "ReferenceTypeKey" 58 | } 59 | -------------------------------------------------------------------------------- /Tests/GenericIDTests/AssociatedObjectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObjectTests.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import XCTest 19 | @testable import GenericID 20 | 21 | class AssociatedObjectTests: XCTestCase { 22 | 23 | func testValueType() { 24 | let obj = NSObject() 25 | XCTAssertNil(obj[.ValueTypeKey]) 26 | 27 | obj[.ValueTypeKey] = 10 28 | XCTAssertEqual(obj[.ValueTypeKey], 10) 29 | 30 | obj[.ValueTypeKey] = 20 31 | XCTAssertEqual(obj[.ValueTypeKey], 20) 32 | } 33 | 34 | func testReferenceType() { 35 | let obj = NSObject() 36 | XCTAssertNil(obj[.ReferenceTypeKey]) 37 | 38 | var date = NSDate() 39 | obj[.ReferenceTypeKey] = date 40 | XCTAssertEqual(obj[.ReferenceTypeKey], date) 41 | 42 | date = NSDate() 43 | obj[.ReferenceTypeKey] = date 44 | XCTAssertEqual(obj[.ReferenceTypeKey], date) 45 | } 46 | 47 | func testRemoving() { 48 | let obj = NSObject() 49 | XCTAssertNil(obj[.ValueTypeKey]) 50 | 51 | obj[.ValueTypeKey] = 233 52 | XCTAssertEqual(obj[.ValueTypeKey], 233) 53 | 54 | obj.removeAssociateValue(for: .ValueTypeKey) 55 | XCTAssertNil(obj[.ValueTypeKey]) 56 | } 57 | 58 | func testRemovingAll() { 59 | let obj = NSObject() 60 | XCTAssertNil(obj[.ValueTypeKey]) 61 | XCTAssertNil(obj[.ReferenceTypeKey]) 62 | 63 | 64 | let date = NSDate() 65 | obj[.ReferenceTypeKey] = date 66 | XCTAssertEqual(obj[.ReferenceTypeKey], date) 67 | 68 | obj[.ValueTypeKey] = 233 69 | XCTAssertEqual(obj[.ValueTypeKey], 233) 70 | 71 | obj.removeAssociateValues() 72 | XCTAssertNil(obj[.ValueTypeKey]) 73 | XCTAssertNil(obj[.ReferenceTypeKey]) 74 | } 75 | 76 | func testDynamicKey() { 77 | let key: NSObject.AssociateKey = "ValueTypeKey" 78 | let obj = NSObject() 79 | XCTAssertNil(obj[.ValueTypeKey]) 80 | XCTAssertNil(obj[key]) 81 | 82 | obj[.ValueTypeKey] = 10 83 | XCTAssertEqual(obj[key], 10) 84 | 85 | obj[key] = 20 86 | XCTAssertEqual(obj[.ValueTypeKey], 20) 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /Sources/GenericID/DefaultsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsKey.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | extension UserDefaults { 21 | 22 | public typealias DefaultsKey = DefaultsKeys.Key 23 | 24 | public class DefaultsKeys { 25 | 26 | public let key: String 27 | 28 | public let valueTransformer: ValueTransformer? 29 | 30 | private init(_ key: String) { 31 | self.key = key 32 | self.valueTransformer = nil 33 | } 34 | 35 | private init(_ key: String, transformer: ValueTransformer) { 36 | self.key = key 37 | self.valueTransformer = transformer 38 | } 39 | 40 | func serialize(_ v: Any) -> Any? { 41 | fatalError("Must override") 42 | } 43 | 44 | func deserialize(_ v: Any) -> Any? { 45 | fatalError("Must override") 46 | } 47 | } 48 | } 49 | 50 | extension UserDefaults.DefaultsKeys { 51 | 52 | public final class Key: UserDefaults.DefaultsKeys { 53 | 54 | public override init(_ key: String) { 55 | super.init(key) 56 | } 57 | 58 | public override init(_ key: String, transformer: UserDefaults.ValueTransformer) { 59 | super.init(key, transformer: transformer) 60 | } 61 | 62 | override func serialize(_ v: Any) -> Any? { 63 | guard let t = valueTransformer else { return v } 64 | return t.serialize(v) 65 | } 66 | 67 | override func deserialize(_ v: Any) -> Any? { 68 | guard let t = valueTransformer else { return v } 69 | return t.deserialize(T.self, from: v) 70 | } 71 | 72 | func deserialize(_ v: Any) -> T? { 73 | guard let t = valueTransformer else { return v as? T } 74 | return t.deserialize(T.self, from: v) 75 | } 76 | } 77 | } 78 | 79 | // MARK: - Conformances 80 | 81 | extension UserDefaults.DefaultsKeys: Equatable { 82 | 83 | public static func ==(lhs: UserDefaults.DefaultsKeys, rhs: UserDefaults.DefaultsKeys) -> Bool { 84 | return lhs.key == rhs.key && 85 | lhs.valueTransformer === rhs.valueTransformer 86 | } 87 | } 88 | 89 | extension UserDefaults.DefaultsKeys: Hashable { 90 | 91 | public func hash(into hasher: inout Hasher) { 92 | hasher.combine(key) 93 | } 94 | } 95 | 96 | extension UserDefaults.DefaultsKeys.Key: ExpressibleByStringLiteral { 97 | 98 | public convenience init(stringLiteral value: String) { 99 | self.init(value) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This project is currently in beta and APIs are subject to change. 2 | 3 | # GenericID 4 | 5 | ![platforms](https://img.shields.io/badge/platforms-macOS%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg) 6 | ![supports](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen.svg) 7 | ![swift](https://img.shields.io/badge/swift-4.0-orange.svg) 8 | [![codebeat badge](https://codebeat.co/badges/2bf7d7e0-2bfe-4280-bbb3-ed64566ddd10)](https://codebeat.co/projects/github-com-ddddxxx-genericid-master) 9 | 10 | A Swift extension to use string-based API in a **type-safe** way. 11 | 12 | All these fantastic API are compatible with traditional string-based API. 13 | 14 | ## Requirements 15 | 16 | - Swift 5.2 (Xcode 11.4) 17 | 18 | ## Type-safe `UserDefaults` 19 | 20 | > You can use `NSUbiquitousKeyValueStore` with almost the same API. 21 | 22 | ### 1. Define your keys 23 | 24 | ```swift 25 | extension UserDefaults.DefaultKeys { 26 | static let intKey = Key("intKey") 27 | static let colorKey = Key("colorKey", transformer: .keyedArchive) 28 | static let pointKey = Key("pointKey", transformer: .json) 29 | } 30 | ``` 31 | 32 | ### 2. Have fun! 33 | 34 | ```swift 35 | let ud = UserDefaults.standard 36 | 37 | // Get & Set 38 | let value = ud[.intKey] 39 | ud[.stringKey] = "foo" 40 | 41 | // Modify 42 | ud[.intKey] += 1 43 | ud[.stringKey] += "bar" 44 | 45 | // Typed array 46 | ud[.stringArrayKey].contains("foo") 47 | ud[.intArrayKey][0] += 1 48 | 49 | // Work with NSKeyedArchiver 50 | ud[.colorKey] = UIColor.orange 51 | ud[.colorKey]?.redComponent 52 | 53 | // Work with JSONEncoder 54 | ud[.pointKey] = CGPoint(x: 1, y: 1) 55 | ud[.pointKey]?.x += 1 56 | 57 | // Modern Key-Value Observing 58 | let observation = defaults.observe(.someKey, options: [.old, .new]) { (defaults, change) in 59 | print(change.newValue) 60 | } 61 | 62 | // KVO with deserializer 63 | let observation = defaults.observe(.rectKey, options: [.old, .new]) { (defaults, change) in 64 | // deserialized automatically 65 | if let rect = change.newValue { 66 | someView.frame = rect 67 | } 68 | } 69 | 70 | // Register with serializer 71 | ud.register(defaults: [ 72 | .intKey: 42, 73 | .stringKey: "foo", 74 | .colorKey: UIColor.blue, // serialized automatically 75 | .pointKey: CGPoint(x: 1, y: 1), 76 | ]) 77 | ``` 78 | 79 | ### Default value 80 | 81 | If associated type of a key conforms `DefaultConstructible`, a default value will be constructed for `nil` result. 82 | 83 | ```swift 84 | public protocol DefaultConstructible { 85 | init() 86 | } 87 | ``` 88 | 89 | Here's types that conforms `DefaultConstructible` and its default value: 90 | 91 | | Type | Default value | 92 | |---------------|---------------| 93 | | Bool | `false` | 94 | | Int | `0` | 95 | | Float/Double | `0.0` | 96 | | String | `""` | 97 | | Data | [empty data] | 98 | | Array | `[]` | 99 | | Dictionary | `[:]` | 100 | | Optional | `nil` | 101 | 102 | Note: `Optional` also conforms `DefaultConstructible`, therefore a key typed as `DefaultKey` aka `DefaultKey>` will still returns `nil`, which is the result of default construction of `Optional`. 103 | 104 | You can always associate an optional type if you want an optional value. 105 | 106 | 107 | 108 | ## Type-safe `UITableViewCell` / `UICollectionViewCell` 109 | 110 | ### 1. Define your reuse identifiers 111 | 112 | ```swift 113 | extension UITableView.CellReuseIdentifiers { 114 | static let customCell : ID = "CustomCellReuseIdentifier" 115 | } 116 | ``` 117 | 118 | ### 2. Register your cells 119 | 120 | ```swift 121 | tableView.register(id: .customCell) 122 | ``` 123 | 124 | ### 3. Dequeue your cells 125 | 126 | ```swift 127 | let cell = tableView.dequeueReusableCell(withIdentifier: .customCell, for: indexPath) 128 | // Typed as MyCustomCell 129 | ``` 130 | 131 | ### XIB-based cells 132 | 133 | ```swift 134 | // That's it! 135 | extension MyCustomCell: UINibFromTypeGettable 136 | 137 | // Or, incase your nib name is not the same as class name 138 | extension MyCustomCell: UINibGettable { 139 | static var nibName = "MyNibName" 140 | } 141 | 142 | // Then register 143 | tableView.registerNib(id: .customCell) 144 | ``` 145 | 146 | ## Type-safe Storyboard 147 | 148 | ### 1. Define your storyboards identifiers 149 | 150 | ```swift 151 | extension UIStoryboard.Identifiers { 152 | static let customVC: ID = "CustomVCStoryboardIdentifier" 153 | } 154 | ``` 155 | 156 | ### 2. Use It! 157 | 158 | ```swift 159 | // Also extend to get main storyboard 160 | let sb = UIStoryboard.main() 161 | 162 | let vc = sb.instantiateViewController(withIdentifier: .customVC) 163 | // Typed as MyCustomViewController 164 | ``` 165 | 166 | ## Type-safe Associated Object 167 | 168 | ```swift 169 | // Define your associate keys 170 | extension YourClass.AssociateKeys { 171 | static let someKey: Key = "someKey" 172 | } 173 | 174 | // Use it! 175 | yourObject[.someKey] = 42 176 | ``` 177 | 178 | ## License 179 | 180 | GenericID is available under the MIT license. See the [LICENSE file](LICENSE). 181 | -------------------------------------------------------------------------------- /Sources/GenericID/DefaultsValueTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsValueTransformer.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | extension UserDefaults { 21 | 22 | open class ValueTransformer { 23 | 24 | open func serialize(_ value: T) -> Any? { 25 | fatalError("Must override") 26 | } 27 | 28 | open func deserialize(_ type: T.Type, from: Any) -> T? { 29 | fatalError("Must override") 30 | } 31 | } 32 | 33 | final class DataCoderValueTransformer: ValueTransformer { 34 | 35 | let encoder: DataEncoder 36 | let decoder: DataDecoder 37 | 38 | init(encoder: DataEncoder, decoder: DataDecoder) { 39 | self.encoder = encoder 40 | self.decoder = decoder 41 | } 42 | 43 | override func serialize(_ value: T) -> Any? { 44 | guard let v = value as? Encodable else { return nil } 45 | return try? v.encodedData(encoder: encoder) 46 | } 47 | 48 | override func deserialize(_ type: T.Type, from: Any) -> T? { 49 | // Unwrap optional type. this can be removed with dynamically querying conditional conformance in Swift 4.2. 50 | let unwrappedType = unwrapRecursively(type) 51 | guard let t = unwrappedType as? Decodable.Type, 52 | let data = from as? Data else { 53 | return nil 54 | } 55 | return (try? t.init(data: data, decoder: decoder)) as? T 56 | } 57 | } 58 | 59 | @available(OSXApplicationExtension 10.11, *) 60 | final class KeyedArchiveValueTransformer: ValueTransformer { 61 | 62 | override func serialize(_ value: T) -> Any? { 63 | return NSKeyedArchiver.archivedData(withRootObject: value) 64 | } 65 | 66 | override func deserialize(_ type: T.Type, from: Any) -> T? { 67 | guard let data = from as? Data else { return nil } 68 | return (try? NSKeyedUnarchiver.my_unarchiveTopLevelObjectWithData(data)) as? T 69 | } 70 | } 71 | } 72 | 73 | extension UserDefaults.ValueTransformer { 74 | 75 | public static let json: UserDefaults.ValueTransformer = 76 | UserDefaults.DataCoderValueTransformer(encoder: JSONEncoder(), 77 | decoder: JSONDecoder()) 78 | 79 | public static let plist: UserDefaults.ValueTransformer = 80 | UserDefaults.DataCoderValueTransformer(encoder: PropertyListEncoder(), 81 | decoder: PropertyListDecoder()) 82 | 83 | @available(OSXApplicationExtension 10.11, *) 84 | public static let keyedArchive: UserDefaults.ValueTransformer = 85 | UserDefaults.KeyedArchiveValueTransformer() 86 | } 87 | 88 | // MARK: - 89 | 90 | private extension NSKeyedUnarchiver { 91 | 92 | private class DummyKeyedUnarchiverDelegate: NSObject, NSKeyedUnarchiverDelegate { 93 | 94 | @objc(UnknownClass_AyWMH3gRIKYqLBV4) 95 | private final class Unknown: NSObject, NSCoding { 96 | func encode(with aCoder: NSCoder) {} 97 | init?(coder aDecoder: NSCoder) { return nil } 98 | } 99 | 100 | var unknownClassName: String? 101 | 102 | func unarchiver(_ unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> Swift.AnyClass? { 103 | unknownClassName = name 104 | print(classNames) 105 | return Unknown.self 106 | } 107 | } 108 | 109 | @nonobjc class func my_unarchiveTopLevelObjectWithData(_ data: Data) throws -> Any? { 110 | if #available(macOS 10.11, iOS 9.0, *) { 111 | return try unarchiveTopLevelObjectWithData(data) 112 | } else { 113 | let unarchiver = NSKeyedUnarchiver(forReadingWith: data) 114 | let delegate = DummyKeyedUnarchiverDelegate() 115 | unarchiver.delegate = delegate 116 | let obj = unarchiver.decodeObject(forKey: "root") 117 | if let name = delegate.unknownClassName { 118 | let desc = "*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (\(name)); the class may be defined in source code or a library that is not linked" 119 | throw NSError(domain: NSCocoaErrorDomain, code: 4864, userInfo: [NSDebugDescriptionErrorKey: desc]) 120 | } 121 | return obj 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/GenericID/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultes.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | extension UserDefaults { 21 | 22 | public func contains(_ key: DefaultsKey) -> Bool { 23 | if T.self is DefaultConstructible.Type, 24 | !(T.self is AnyOptionalType.Type) { 25 | return true 26 | } 27 | return object(forKey: key.key) != nil 28 | } 29 | 30 | public func remove(_ key: DefaultsKey) { 31 | removeObject(forKey: key.key) 32 | } 33 | 34 | public func removeAll() { 35 | if let appDomain = Bundle.main.bundleIdentifier { 36 | removePersistentDomain(forName: appDomain) 37 | synchronize() 38 | } else { 39 | for key in dictionaryRepresentation().keys { 40 | removeObject(forKey: key) 41 | } 42 | } 43 | } 44 | 45 | public func register(defaults: [DefaultsKeys: Any]) { 46 | var dict = Dictionary(minimumCapacity: defaults.count) 47 | for (key, value) in defaults { 48 | if let transformer = key.valueTransformer { 49 | dict[key.key] = transformer.serialize(value) 50 | } else { 51 | dict[key.key] = value 52 | } 53 | } 54 | register(defaults: dict) 55 | } 56 | 57 | public func unregister(_ key: DefaultsKey) { 58 | var domain = volatileDomain(forName: UserDefaults.registrationDomain) 59 | domain.removeValue(forKey: key.key) 60 | setVolatileDomain(domain, forName: UserDefaults.registrationDomain) 61 | } 62 | 63 | public func unregisterAll() { 64 | setVolatileDomain([:], forName: UserDefaults.registrationDomain) 65 | } 66 | } 67 | 68 | // MARK: - Subscript 69 | 70 | extension UserDefaults { 71 | 72 | public subscript(_ key: DefaultsKey) -> T? { 73 | get { 74 | return object(forKey: key.key).flatMap(key.deserialize) 75 | } 76 | set { 77 | set(newValue.flatMap(key.serialize), forKey: key.key) 78 | } 79 | } 80 | 81 | public subscript(_ key: DefaultsKey) -> T { 82 | get { 83 | return object(forKey: key.key).flatMap(key.deserialize) ?? T() 84 | } 85 | set { 86 | // T might be optional and holds a `nil`, which will be bridged to `NSNull` 87 | // and cannot be stored in UserDefaults. We must unwrap it manually. 88 | set(unwrapRecursively(newValue).flatMap(key.serialize), forKey: key.key) 89 | } 90 | } 91 | } 92 | 93 | // MARK: - Binding 94 | 95 | class DefaultsDeserializeValueTransformer: ValueTransformer { 96 | 97 | var defaultsKey: UserDefaults.DefaultsKeys 98 | 99 | init(key: UserDefaults.DefaultsKeys) { 100 | defaultsKey = key 101 | super.init() 102 | } 103 | 104 | override class func transformedValueClass() -> Swift.AnyClass { 105 | return AnyObject.self 106 | } 107 | 108 | override class func allowsReverseTransformation() -> Bool { 109 | return true 110 | } 111 | 112 | override func transformedValue(_ value: Any?) -> Any? { 113 | guard let value = value else { return nil } 114 | return defaultsKey.deserialize(value).flatMap(unwrapRecursively) 115 | } 116 | 117 | override func reverseTransformedValue(_ value: Any?) -> Any? { 118 | guard let value = value else { return nil } 119 | return defaultsKey.serialize(value) 120 | } 121 | } 122 | 123 | #if canImport(AppKit) 124 | 125 | import AppKit 126 | 127 | extension NSObject { 128 | 129 | public func bind(_ binding: NSBindingName, 130 | to userDefaults: UserDefaults = UserDefaults.standard, 131 | defaultsKey: UserDefaults.DefaultsKey, 132 | options: [NSBindingOption: Any] = [:]) { 133 | var options = options 134 | if let transformer = defaultsKey.valueTransformer { 135 | if #available(OSXApplicationExtension 10.11, *), 136 | transformer is UserDefaults.KeyedArchiveValueTransformer { 137 | options[.valueTransformerName] = NSValueTransformerName.keyedUnarchiveFromDataTransformerName 138 | } else { 139 | options[.valueTransformer] = DefaultsDeserializeValueTransformer(key: defaultsKey) 140 | } 141 | } 142 | bind(binding, to: userDefaults, withKeyPath: defaultsKey.key, options: options) 143 | } 144 | } 145 | 146 | #endif 147 | -------------------------------------------------------------------------------- /Sources/GenericID/UICollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | #if os(iOS) || os(tvOS) 19 | 20 | import UIKit 21 | 22 | // MARK: UICollectionViewCell 23 | 24 | extension UICollectionView { 25 | 26 | public typealias CellReuseIdentifier = CellReuseIdentifiers.ID where T: UICollectionViewCell 27 | 28 | public class CellReuseIdentifiers: StaticKeyBase {} 29 | } 30 | 31 | extension UICollectionView.CellReuseIdentifiers { 32 | 33 | public final class ID: UICollectionView.CellReuseIdentifiers, RawRepresentable, ExpressibleByStringLiteral { 34 | 35 | public var rawValue: String { 36 | return key 37 | } 38 | 39 | public init(rawValue: String) { 40 | super.init(rawValue) 41 | } 42 | 43 | public init(stringLiteral value: String) { 44 | super.init(value) 45 | } 46 | } 47 | } 48 | 49 | extension UICollectionView.CellReuseIdentifier: UINibFromTypeGettable { 50 | typealias NibType = T 51 | } 52 | 53 | extension UICollectionView { 54 | 55 | public func register(id: CellReuseIdentifier) { 56 | register(T.self, forCellWithReuseIdentifier: id.rawValue) 57 | } 58 | 59 | public func registerNib(id: CellReuseIdentifier) { 60 | register(type(of: id).nib, forCellWithReuseIdentifier: id.rawValue) 61 | } 62 | 63 | public func registerNib(id: CellReuseIdentifier) { 64 | register(T.nib, forCellWithReuseIdentifier: id.rawValue) 65 | } 66 | 67 | public func dequeueReusableCell(withReuseIdentifier identifier: CellReuseIdentifier, for indexPath: IndexPath) -> T { 68 | guard let cell = dequeueReusableCell(withReuseIdentifier: identifier.rawValue, for: indexPath) as? T else { 69 | fatalError("Could not dequeue reusable cell with identifier '\(identifier.rawValue)' for type '\(T.self)'") 70 | } 71 | return cell 72 | } 73 | } 74 | 75 | // MARK: - UICollectionReusableView 76 | 77 | extension UICollectionView { 78 | 79 | public typealias SupplementaryViewReuseIdentifier = SupplementaryViewReuseIdentifiers.ID where T: UICollectionReusableView 80 | 81 | public class SupplementaryViewReuseIdentifiers: StaticKeyBase {} 82 | } 83 | 84 | extension UICollectionView.SupplementaryViewReuseIdentifiers { 85 | 86 | public final class ID: UICollectionView.SupplementaryViewReuseIdentifiers, RawRepresentable, ExpressibleByStringLiteral { 87 | 88 | public var rawValue: String { 89 | return key 90 | } 91 | 92 | public init(rawValue: String) { 93 | super.init(rawValue) 94 | } 95 | 96 | public init(stringLiteral value: String) { 97 | super.init(value) 98 | } 99 | } 100 | } 101 | 102 | extension UICollectionView.SupplementaryViewReuseIdentifier: UINibFromTypeGettable { 103 | typealias NibType = T 104 | } 105 | 106 | extension UICollectionView { 107 | 108 | public func register(id: SupplementaryViewReuseIdentifier, ofKind elementKind: String) { 109 | register(T.self, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: id.rawValue) 110 | } 111 | 112 | public func registerNib(id: SupplementaryViewReuseIdentifier, ofKind elementKind: String) { 113 | register(type(of: id).nib, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: id.rawValue) 114 | } 115 | 116 | public func registerNib(id: SupplementaryViewReuseIdentifier, ofKind elementKind: String) { 117 | register(T.nib, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: id.rawValue) 118 | } 119 | 120 | public func dequeueReusableSupplementaryView(ofKind elementKind: String, withIdentifier identifier: SupplementaryViewReuseIdentifier, for indexPath: IndexPath) -> T { 121 | guard let view = dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: identifier.rawValue, for: indexPath) as? T else { 122 | fatalError("Could not dequeue reusable supplementary view with identifier '\(identifier.rawValue)' for type '\(T.self)'") 123 | } 124 | return view 125 | } 126 | } 127 | 128 | #endif 129 | -------------------------------------------------------------------------------- /Sources/GenericID/UITableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | #if os(iOS) || os(tvOS) 19 | 20 | import UIKit 21 | 22 | // MARK: UITableViewCell 23 | 24 | extension UITableView { 25 | 26 | public typealias CellReuseIdentifier = CellReuseIdentifiers.ID where T: UITableViewCell 27 | 28 | public class CellReuseIdentifiers: StaticKeyBase {} 29 | } 30 | 31 | extension UITableView.CellReuseIdentifiers { 32 | 33 | public final class ID: UITableView.CellReuseIdentifiers, RawRepresentable, ExpressibleByStringLiteral { 34 | 35 | public var rawValue: String { 36 | return key 37 | } 38 | 39 | public init(rawValue: String) { 40 | super.init(rawValue) 41 | } 42 | 43 | public init(stringLiteral value: String) { 44 | super.init(value) 45 | } 46 | } 47 | } 48 | 49 | extension UITableView.CellReuseIdentifier: UINibFromTypeGettable { 50 | typealias NibType = T 51 | } 52 | 53 | extension UITableView { 54 | 55 | public func register(id: CellReuseIdentifier) { 56 | register(T.self, forCellReuseIdentifier: id.rawValue) 57 | } 58 | 59 | public func registerNib(id: CellReuseIdentifier) { 60 | register(type(of: id).nib, forCellReuseIdentifier: id.rawValue) 61 | } 62 | 63 | public func registerNib(id: CellReuseIdentifier) { 64 | register(T.nib, forCellReuseIdentifier: id.rawValue) 65 | } 66 | 67 | public func dequeueReusableCell(withIdentifier identifier: CellReuseIdentifier) -> T? { 68 | guard let dequeue = dequeueReusableCell(withIdentifier: identifier.rawValue) else { 69 | return nil 70 | } 71 | guard let cell = dequeue as? T else { 72 | fatalError("Could not dequeue reusable cell with identifier '\(identifier.rawValue)' for type '\(T.self)'") 73 | } 74 | return cell 75 | } 76 | 77 | public func dequeueReusableCell(withIdentifier identifier: CellReuseIdentifier, for indexPath: IndexPath) -> T { 78 | guard let cell = dequeueReusableCell(withIdentifier: identifier.rawValue, for: indexPath) as? T else { 79 | fatalError("Could not dequeue reusable cell with identifier '\(identifier.rawValue)' for type '\(T.self)'") 80 | } 81 | return cell 82 | } 83 | } 84 | 85 | // MARK: - UITableViewHeaderFooterView 86 | 87 | extension UITableView { 88 | 89 | public typealias HeaderFooterViewReuseIdentifier = HeaderFooterViewReuseIdentifiers.ID where T: UITableViewHeaderFooterView 90 | 91 | public class HeaderFooterViewReuseIdentifiers: StaticKeyBase {} 92 | } 93 | 94 | extension UITableView.HeaderFooterViewReuseIdentifiers { 95 | 96 | public final class ID: UITableView.HeaderFooterViewReuseIdentifiers, RawRepresentable, ExpressibleByStringLiteral { 97 | 98 | public var rawValue: String { 99 | return key 100 | } 101 | 102 | public init(rawValue: String) { 103 | super.init(rawValue) 104 | } 105 | 106 | public init(stringLiteral value: String) { 107 | super.init(value) 108 | } 109 | } 110 | } 111 | 112 | extension UITableView.HeaderFooterViewReuseIdentifier: UINibFromTypeGettable { 113 | typealias NibType = T 114 | } 115 | 116 | extension UITableView { 117 | 118 | public func register(id: HeaderFooterViewReuseIdentifier) { 119 | register(T.self, forHeaderFooterViewReuseIdentifier: id.rawValue) 120 | } 121 | 122 | public func registerNib(id: HeaderFooterViewReuseIdentifier) { 123 | register(type(of: id).nib, forHeaderFooterViewReuseIdentifier: id.rawValue) 124 | } 125 | 126 | public func registerNib(id: HeaderFooterViewReuseIdentifier) { 127 | register(T.nib, forHeaderFooterViewReuseIdentifier: id.rawValue) 128 | } 129 | 130 | public func dequeueReusableHeaderFooterView(withIdentifier identifier: HeaderFooterViewReuseIdentifier) -> T? { 131 | guard let dequeue = dequeueReusableHeaderFooterView(withIdentifier: identifier.rawValue) else { 132 | return nil 133 | } 134 | guard let view = dequeue as? T else { 135 | fatalError("Could not dequeue reusable header footer view with identifier '\(identifier.rawValue)' for type '\(T.self)'") 136 | } 137 | return view 138 | } 139 | } 140 | 141 | #endif 142 | -------------------------------------------------------------------------------- /Sources/GenericID/DefaultsObservation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsObservation.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import Foundation 19 | 20 | public protocol DefaultsObservation { 21 | func invalidate() 22 | } 23 | 24 | extension UserDefaults { 25 | 26 | class _DefaultsObservedChange { 27 | 28 | fileprivate let isPrior:Bool 29 | fileprivate let newValue: Any? 30 | fileprivate let oldValue: Any? 31 | 32 | init(observedChange change: [NSKeyValueChangeKey: Any]) { 33 | isPrior = change[.notificationIsPriorKey] as? Bool ?? false 34 | oldValue = change[.oldKey] 35 | newValue = change[.newKey] 36 | } 37 | } 38 | 39 | public struct DefaultsObservedChange { 40 | 41 | private let _change: _DefaultsObservedChange 42 | private let _oldValue: LazyReference 43 | private let _newValue: LazyReference 44 | 45 | public var isPrior: Bool { return _change.isPrior } 46 | public var newValue: T? { return _newValue.value } 47 | public var oldValue: T? { return _oldValue.value } 48 | 49 | fileprivate init(_ change: _DefaultsObservedChange, transformer: @escaping (Any) -> T?) { 50 | _change = change 51 | _oldValue = LazyReference { change.oldValue.flatMap(transformer) } 52 | _newValue = LazyReference { change.newValue.flatMap(transformer) } 53 | } 54 | } 55 | 56 | public struct ConstructedDefaultsObservedChange { 57 | 58 | let _change: _DefaultsObservedChange 59 | let _oldValue: LazyReference 60 | let _newValue: LazyReference 61 | 62 | public var isPrior: Bool { return _change.isPrior } 63 | public var newValue: T { return _newValue.value } 64 | public var oldValue: T { return _oldValue.value } 65 | 66 | fileprivate init(_ change: _DefaultsObservedChange, transformer: @escaping (Any?) -> T) { 67 | _change = change 68 | _oldValue = LazyReference { transformer(change.oldValue) } 69 | _newValue = LazyReference { transformer(change.newValue) } 70 | } 71 | } 72 | 73 | class SingleKeyObservation: NSObject, DefaultsObservation { 74 | 75 | typealias Callback = (UserDefaults, _DefaultsObservedChange) -> Void 76 | 77 | private weak var object: UserDefaults? 78 | private let callback: Callback 79 | private let key: String 80 | 81 | fileprivate init(object: UserDefaults, key: String, callback: @escaping Callback) { 82 | self.key = key 83 | self.object = object 84 | self.callback = callback 85 | } 86 | 87 | deinit { 88 | invalidate() 89 | } 90 | 91 | fileprivate func start(_ options: NSKeyValueObservingOptions) { 92 | object?.addObserver(self, forKeyPath: key, options: options, context: nil) 93 | } 94 | 95 | func invalidate() { 96 | object?.removeObserver(self, forKeyPath: key, context: nil) 97 | object = nil 98 | } 99 | 100 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 101 | guard let ourObject = self.object, object as? NSObject == ourObject, let change = change else { return } 102 | let notification = _DefaultsObservedChange(observedChange: change) 103 | callback(ourObject, notification) 104 | } 105 | } 106 | } 107 | 108 | extension UserDefaults { 109 | 110 | public func observe(_ key: DefaultsKey, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (UserDefaults, DefaultsObservedChange) -> Void) -> DefaultsObservation { 111 | let result = SingleKeyObservation(object: self, key: key.key) { (defaults, change) in 112 | let notification = DefaultsObservedChange(change, transformer: key.deserialize) 113 | changeHandler(defaults, notification) 114 | } 115 | result.start(options) 116 | return result 117 | } 118 | 119 | public func observe(_ key: DefaultsKey, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (UserDefaults, ConstructedDefaultsObservedChange) -> Void) -> DefaultsObservation { 120 | let result = SingleKeyObservation(object: self, key: key.key) { (defaults, change) in 121 | let notification = ConstructedDefaultsObservedChange(change) { 122 | $0.flatMap(key.deserialize) ?? T() 123 | } 124 | changeHandler(defaults, notification) 125 | } 126 | result.start(options) 127 | return result 128 | } 129 | } 130 | 131 | // MARK: - Observe Multiple Keys 132 | 133 | extension UserDefaults { 134 | 135 | class MultiKeyObservation: NSObject, DefaultsObservation { 136 | 137 | typealias Callback = () -> Void 138 | 139 | private weak var object: UserDefaults? 140 | private let callback: Callback 141 | private let keys: [String] 142 | 143 | fileprivate init(object: UserDefaults, keys: [String], callback: @escaping Callback) { 144 | self.keys = keys 145 | self.object = object 146 | self.callback = callback 147 | } 148 | 149 | deinit { 150 | invalidate() 151 | } 152 | 153 | fileprivate func start(_ options: NSKeyValueObservingOptions) { 154 | for key in keys { 155 | object?.addObserver(self, forKeyPath: key, options: options, context: nil) 156 | } 157 | } 158 | 159 | func invalidate() { 160 | for key in keys { 161 | object?.removeObserver(self, forKeyPath: key, context: nil) 162 | } 163 | object = nil 164 | } 165 | 166 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 167 | guard let ourObject = self.object, object as? NSObject == ourObject else { return } 168 | callback() 169 | } 170 | } 171 | 172 | public func observe(keys: [DefaultsKeys], options: NSKeyValueObservingOptions = [], changeHandler: @escaping () -> Void) -> DefaultsObservation { 173 | let keys = keys.map { $0.key } 174 | let result = MultiKeyObservation(object: self, keys: keys, callback: changeHandler) 175 | result.start(options) 176 | return result 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/GenericID/DefaultsPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsPublisher.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | #if canImport(CXShim) 19 | 20 | import Foundation 21 | import CXShim 22 | 23 | extension UserDefaults { 24 | 25 | public func publisher(for key: DefaultsKey) -> Publisher { 26 | return Publisher(object: self, key: key) 27 | } 28 | 29 | public func publisher(for key: DefaultsKey) -> Publishers.Map, Value> { 30 | return Publisher(object: self, key: key).map { $0 ?? Value() } 31 | } 32 | 33 | public func publisher(for keys: [DefaultsKeys]) -> MultiValuePublisher { 34 | return MultiValuePublisher(object: self, keys: keys) 35 | } 36 | } 37 | 38 | extension UserDefaults { 39 | 40 | public struct Publisher: CXShim.Publisher { 41 | 42 | public typealias Output = Value? 43 | public typealias Failure = Never 44 | 45 | public let object: UserDefaults 46 | public let key: DefaultsKey 47 | 48 | init(object: UserDefaults, key: DefaultsKey) { 49 | self.object = object 50 | self.key = key 51 | } 52 | 53 | public func receive(subscriber: S) where S.Failure == Failure, S.Input == Output { 54 | let subscription = UserDefaults.Subscription(object: object, key: key, downstream: subscriber) 55 | subscriber.receive(subscription: subscription) 56 | } 57 | } 58 | 59 | public struct MultiValuePublisher: CXShim.Publisher { 60 | 61 | public typealias Output = Void 62 | public typealias Failure = Never 63 | 64 | public var object: UserDefaults 65 | public var keys: [DefaultsKeys] 66 | 67 | init(object: UserDefaults, keys: [DefaultsKeys]) { 68 | self.object = object 69 | self.keys = keys 70 | } 71 | 72 | public func receive(subscriber: S) where S.Failure == Failure, S.Input == Output { 73 | let subscription = UserDefaults.MultiValueSubscription(object: object, keys: keys, downstream: subscriber) 74 | subscriber.receive(subscription: subscription) 75 | } 76 | } 77 | } 78 | 79 | private extension UserDefaults { 80 | 81 | final class Subscription: NSObject, CXShim.Subscription where Downstream.Input == Output?, Downstream.Failure == Never { 82 | 83 | private let lock = NSLock() 84 | 85 | private let downstreamLock = NSRecursiveLock() 86 | 87 | private var downstream: Downstream? 88 | 89 | private var demand = Subscribers.Demand.none 90 | 91 | private var object: UserDefaults? 92 | 93 | private let key: DefaultsKey 94 | 95 | init(object: UserDefaults, key: DefaultsKey, downstream: Downstream) { 96 | self.object = object 97 | self.key = key 98 | self.downstream = downstream 99 | super.init() 100 | object.addObserver(self, forKeyPath: key.key, options: [.new], context: nil) 101 | } 102 | 103 | deinit { 104 | cancel() 105 | } 106 | 107 | func request(_ demand: Subscribers.Demand) { 108 | lock.lock() 109 | self.demand += demand 110 | lock.unlock() 111 | } 112 | 113 | func cancel() { 114 | lock.lock() 115 | guard let object = self.object else { 116 | lock.unlock() 117 | return 118 | } 119 | self.object = nil 120 | lock.unlock() 121 | object.removeObserver(self, forKeyPath: key.key, context: nil) 122 | } 123 | 124 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 125 | lock.lock() 126 | guard demand > 0, let downstream = downstream else { 127 | lock.unlock() 128 | return 129 | } 130 | demand -= 1 131 | lock.unlock() 132 | 133 | downstreamLock.lock() 134 | let value = self.object?[self.key] 135 | let newDemand = downstream.receive(value) 136 | downstreamLock.unlock() 137 | 138 | lock.lock() 139 | demand += newDemand 140 | lock.unlock() 141 | } 142 | } 143 | 144 | final class MultiValueSubscription: NSObject, CXShim.Subscription where Downstream.Input == Void, Downstream.Failure == Never { 145 | 146 | private let lock = NSLock() 147 | 148 | private let downstreamLock = NSRecursiveLock() 149 | 150 | private var downstream: Downstream? 151 | 152 | private var demand = Subscribers.Demand.none 153 | 154 | private var object: UserDefaults? 155 | 156 | private var keys: [DefaultsKeys] 157 | 158 | init(object: UserDefaults, keys: [DefaultsKeys], downstream: Downstream) { 159 | self.object = object 160 | self.keys = keys 161 | self.downstream = downstream 162 | super.init() 163 | for key in keys { 164 | object.addObserver(self, forKeyPath: key.key, options: [.new], context: nil) 165 | } 166 | } 167 | 168 | deinit { 169 | cancel() 170 | } 171 | 172 | func request(_ demand: Subscribers.Demand) { 173 | lock.lock() 174 | self.demand += demand 175 | lock.unlock() 176 | } 177 | 178 | func cancel() { 179 | lock.lock() 180 | guard let object = self.object else { 181 | lock.unlock() 182 | return 183 | } 184 | self.object = nil 185 | lock.unlock() 186 | for key in keys { 187 | object.removeObserver(self, forKeyPath: key.key, context: nil) 188 | } 189 | } 190 | 191 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 192 | lock.lock() 193 | guard demand > 0, let downstream = downstream else { 194 | lock.unlock() 195 | return 196 | } 197 | demand -= 1 198 | lock.unlock() 199 | 200 | downstreamLock.lock() 201 | let newDemand = downstream.receive() 202 | downstreamLock.unlock() 203 | 204 | lock.lock() 205 | demand += newDemand 206 | lock.unlock() 207 | } 208 | } 209 | } 210 | 211 | #endif // canImport(CXShim) 212 | -------------------------------------------------------------------------------- /Tests/GenericIDTests/UserDefaultesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultesTests.swift 3 | // 4 | // This file is part of GenericID. 5 | // Copyright (c) 2017 Xander Deng 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | 18 | import XCTest 19 | @testable import GenericID 20 | 21 | class UserDefaultesTests: XCTestCase { 22 | 23 | let defaults = UserDefaults.standard 24 | 25 | override func setUp() { 26 | super.setUp() 27 | defaults.unregisterAll() 28 | defaults.removeAll() 29 | } 30 | 31 | // MARK: - Non-Optional Key 32 | 33 | func testBool() { 34 | XCTAssertFalse(defaults[.BoolKey]) 35 | 36 | defaults[.BoolKey] = true 37 | XCTAssertTrue(defaults[.BoolKey]) 38 | 39 | defaults.remove(.BoolKey) 40 | XCTAssertFalse(defaults[.BoolKey]) 41 | } 42 | 43 | func testInt() { 44 | XCTAssertEqual(defaults[.IntKey], 0) 45 | 46 | defaults[.IntKey] = 233 47 | XCTAssertEqual(defaults[.IntKey], 233) 48 | 49 | defaults[.IntKey] += 233 50 | XCTAssertEqual(defaults[.IntKey], 466) 51 | 52 | defaults.remove(.IntKey) 53 | XCTAssertEqual(defaults[.IntKey], 0) 54 | } 55 | 56 | func testFloat() { 57 | XCTAssertEqual(defaults[.FloatKey], 0) 58 | 59 | defaults[.FloatKey] = 3.14 60 | XCTAssertEqual(defaults[.FloatKey], 3.14) 61 | 62 | defaults[.FloatKey] += 0.01 63 | XCTAssertEqual(defaults[.FloatKey], 3.15) 64 | 65 | defaults.remove(.FloatKey) 66 | XCTAssertEqual(defaults[.FloatKey], 0) 67 | } 68 | 69 | func testDouble() { 70 | XCTAssertEqual(defaults[.DoubleKey], 0) 71 | 72 | defaults[.DoubleKey] = 3.14 73 | XCTAssertEqual(defaults[.DoubleKey], 3.14) 74 | 75 | defaults[.DoubleKey] += 0.01 76 | XCTAssertEqual(defaults[.DoubleKey], 3.15) 77 | 78 | defaults.remove(.DoubleKey) 79 | XCTAssertEqual(defaults[.DoubleKey], 0) 80 | } 81 | 82 | func testData() { 83 | XCTAssert(defaults[.DataKey].isEmpty) 84 | 85 | let data = "foo".data(using: .ascii)! 86 | defaults[.DataKey] = data 87 | XCTAssertEqual(defaults[.DataKey], data) 88 | 89 | defaults[.DataKey].removeFirst() 90 | XCTAssertEqual(defaults[.DataKey], data.dropFirst()) 91 | 92 | defaults.remove(.DataKey) 93 | XCTAssert(defaults[.DataKey].isEmpty) 94 | } 95 | 96 | func testString() { 97 | XCTAssert(defaults[.StringKey].isEmpty) 98 | 99 | defaults[.StringKey] = "foo" 100 | XCTAssertEqual(defaults[.StringKey], "foo") 101 | 102 | defaults[.StringKey] += "bar" 103 | XCTAssertEqual(defaults[.StringKey], "foobar") 104 | 105 | defaults.remove(.StringKey) 106 | XCTAssert(defaults[.StringKey].isEmpty) 107 | } 108 | 109 | func testArray() { 110 | XCTAssert(defaults[.ArrayKey].isEmpty) 111 | 112 | defaults[.ArrayKey] = [true, 233, 3.14] 113 | XCTAssertEqual(defaults[.ArrayKey].count, 3) 114 | XCTAssertEqual(defaults[.ArrayKey][0] as? Bool, true) 115 | XCTAssertEqual(defaults[.ArrayKey][1] as? Int, 233) 116 | XCTAssertEqual(defaults[.ArrayKey][2] as? Double, 3.14) 117 | 118 | defaults[.ArrayKey].append("foo") 119 | XCTAssertEqual(defaults[.ArrayKey].count, 4) 120 | XCTAssertEqual(defaults[.ArrayKey][3] as? String, "foo") 121 | 122 | defaults.remove(.ArrayKey) 123 | XCTAssert(defaults[.ArrayKey].isEmpty) 124 | } 125 | 126 | func testStringArray() { 127 | XCTAssert(defaults[.StringArrayKey].isEmpty) 128 | 129 | defaults[.StringArrayKey] = ["foo", "bar", "baz"] 130 | XCTAssertEqual(defaults[.StringArrayKey][0], "foo") 131 | XCTAssertEqual(defaults[.StringArrayKey][1], "bar") 132 | XCTAssertEqual(defaults[.StringArrayKey][2], "baz") 133 | 134 | defaults[.StringArrayKey].append("qux") 135 | XCTAssertEqual(defaults[.StringArrayKey][3], "qux") 136 | 137 | defaults.remove(.StringArrayKey) 138 | XCTAssert(defaults[.StringArrayKey].isEmpty) 139 | } 140 | 141 | func testDictionary() { 142 | XCTAssert(defaults[.DictionaryKey].isEmpty) 143 | 144 | defaults[.DictionaryKey] = [ 145 | "foo": true, 146 | "bar": 233, 147 | "baz": 3.14, 148 | ] 149 | XCTAssertEqual(defaults[.DictionaryKey]["foo"] as? Bool, true) 150 | XCTAssertEqual(defaults[.DictionaryKey]["bar"] as? Int, 233) 151 | XCTAssertEqual(defaults[.DictionaryKey]["baz"] as? Double, 3.14) 152 | 153 | defaults[.DictionaryKey]["qux"] = [1, 2, 3] 154 | XCTAssertEqual(defaults[.DictionaryKey]["qux"] as! [Int], [1, 2, 3]) 155 | 156 | defaults.remove(.DictionaryKey) 157 | XCTAssert(defaults[.DictionaryKey].isEmpty) 158 | } 159 | 160 | // MARK: - Optional Key 161 | 162 | func testOptBool() { 163 | XCTAssertNil(defaults[.BoolOptKey]) 164 | 165 | defaults[.BoolOptKey] = true 166 | XCTAssertEqual(defaults[.BoolOptKey], true) 167 | 168 | defaults[.BoolOptKey] = nil 169 | XCTAssertNil(defaults[.BoolOptKey]) 170 | } 171 | 172 | func testOptInt() { 173 | XCTAssertNil(defaults[.IntOptKey]) 174 | 175 | defaults[.IntOptKey] = 233 176 | XCTAssertEqual(defaults[.IntOptKey], 233) 177 | 178 | defaults[.IntOptKey]? += 233 179 | XCTAssertEqual(defaults[.IntOptKey], 466) 180 | 181 | defaults[.IntOptKey] = nil 182 | XCTAssertNil(defaults[.IntOptKey]) 183 | } 184 | 185 | func testOptFloat() { 186 | XCTAssertNil(defaults[.FloatOptKey]) 187 | 188 | defaults[.FloatOptKey] = 3.14 189 | XCTAssertEqual(defaults[.FloatOptKey], 3.14) 190 | 191 | defaults[.FloatOptKey]? += 0.01 192 | XCTAssertEqual(defaults[.FloatOptKey], 3.15) 193 | 194 | defaults[.FloatOptKey] = nil 195 | XCTAssertNil(defaults[.FloatOptKey]) 196 | } 197 | 198 | func testOptDouble() { 199 | XCTAssertNil(defaults[.DoubleOptKey]) 200 | 201 | defaults[.DoubleOptKey] = 3.14 202 | XCTAssertEqual(defaults[.DoubleOptKey], 3.14) 203 | 204 | defaults[.DoubleOptKey]? += 0.01 205 | XCTAssertEqual(defaults[.DoubleOptKey], 3.15) 206 | 207 | defaults[.DoubleOptKey] = nil 208 | XCTAssertNil(defaults[.DoubleOptKey]) 209 | } 210 | 211 | func testOptString() { 212 | XCTAssertNil(defaults[.StringOptKey]) 213 | 214 | defaults[.StringOptKey] = "foo" 215 | XCTAssertEqual(defaults[.StringOptKey], "foo") 216 | 217 | defaults[.StringOptKey]? += "bar" 218 | XCTAssertEqual(defaults[.StringOptKey], "foobar") 219 | 220 | defaults[.StringOptKey] = nil 221 | XCTAssertNil(defaults[.StringOptKey]) 222 | } 223 | 224 | func testOptURL() { 225 | XCTAssertNil(defaults[.URLOptKey]) 226 | 227 | let url = URL(string: "https://google.com")! 228 | defaults[.URLOptKey] = url 229 | XCTAssertEqual(defaults[.URLOptKey], url) 230 | 231 | defaults[.URLOptKey]?.appendPathComponent("404") 232 | XCTAssertEqual(defaults[.URLOptKey], URL(string: "https://google.com/404")!) 233 | 234 | defaults[.URLOptKey] = nil 235 | XCTAssertNil(defaults[.URLOptKey]) 236 | } 237 | 238 | func testOptDate() { 239 | XCTAssertNil(defaults[.DateOptKey]) 240 | 241 | let date = Date() 242 | defaults[.DateOptKey] = date 243 | XCTAssertEqual(defaults[.DateOptKey], date) 244 | 245 | defaults[.DateOptKey]?.addTimeInterval(123) 246 | XCTAssertEqual(defaults[.DateOptKey], date.addingTimeInterval(123)) 247 | 248 | defaults[.DateOptKey] = nil 249 | XCTAssertNil(defaults[.DateOptKey]) 250 | } 251 | 252 | func testOptData() { 253 | XCTAssertNil(defaults[.DataOptKey]) 254 | 255 | let data = "foo".data(using: .ascii)! 256 | defaults[.DataOptKey] = data 257 | XCTAssertEqual(defaults[.DataOptKey], data) 258 | 259 | defaults[.DataOptKey]?.removeFirst() 260 | XCTAssertEqual(defaults[.DataOptKey], data.dropFirst()) 261 | 262 | defaults[.DataOptKey] = nil 263 | XCTAssertNil(defaults[.DataOptKey]) 264 | } 265 | 266 | func testOptArray() { 267 | XCTAssertNil(defaults[.ArrayOptKey]) 268 | 269 | defaults[.ArrayOptKey] = [true, 233, 3.14] 270 | XCTAssertEqual(defaults[.ArrayOptKey]?[0] as? Bool, true) 271 | XCTAssertEqual(defaults[.ArrayOptKey]?[1] as? Int, 233) 272 | XCTAssertEqual(defaults[.ArrayOptKey]?[2] as? Double, 3.14) 273 | 274 | defaults[.ArrayOptKey]?.append("foo") 275 | XCTAssertEqual(defaults[.ArrayOptKey]?[3] as? String, "foo") 276 | 277 | defaults[.ArrayOptKey] = nil 278 | XCTAssertNil(defaults[.ArrayOptKey]) 279 | } 280 | 281 | func testOptStringArray() { 282 | XCTAssertNil(defaults[.StringArrayOptKey]) 283 | 284 | defaults[.StringArrayOptKey] = ["foo", "bar", "baz"] 285 | XCTAssertEqual(defaults[.StringArrayOptKey]?[0], "foo") 286 | XCTAssertEqual(defaults[.StringArrayOptKey]?[1], "bar") 287 | XCTAssertEqual(defaults[.StringArrayOptKey]?[2], "baz") 288 | 289 | defaults[.StringArrayOptKey]?.append("qux") 290 | XCTAssertEqual(defaults[.StringArrayOptKey]?[3], "qux") 291 | 292 | defaults[.StringArrayOptKey] = nil 293 | XCTAssertNil(defaults[.StringArrayOptKey]) 294 | } 295 | 296 | func testOptDictionary() { 297 | XCTAssertNil(defaults[.DictionaryOptKey]) 298 | 299 | defaults[.DictionaryOptKey] = [ 300 | "foo": true, 301 | "bar": 233, 302 | "baz": 3.14, 303 | ] 304 | XCTAssertEqual(defaults[.DictionaryOptKey]?["foo"] as? Bool, true) 305 | XCTAssertEqual(defaults[.DictionaryOptKey]?["bar"] as? Int, 233) 306 | XCTAssertEqual(defaults[.DictionaryOptKey]?["baz"] as? Double, 3.14) 307 | 308 | defaults[.DictionaryOptKey]?["qux"] = [1, 2, 3] 309 | XCTAssertEqual(defaults[.DictionaryOptKey]?["qux"] as! [Int], [1, 2, 3]) 310 | 311 | defaults[.DictionaryOptKey] = nil 312 | XCTAssertNil(defaults[.DictionaryOptKey]) 313 | } 314 | 315 | func testOptAny() { 316 | XCTAssertNil(defaults[.AnyOptKey]) 317 | 318 | defaults[.AnyOptKey] = true 319 | XCTAssertEqual(defaults[.AnyOptKey] as? Bool, true) 320 | 321 | defaults[.AnyOptKey] = 233 322 | XCTAssertEqual(defaults[.AnyOptKey] as? Int, 233) 323 | 324 | defaults[.AnyOptKey] = 3.14 325 | XCTAssertEqual(defaults[.AnyOptKey] as? Double, 3.14) 326 | 327 | defaults[.AnyOptKey] = "foo" 328 | XCTAssertEqual(defaults[.AnyOptKey] as? String, "foo") 329 | 330 | defaults[.AnyOptKey] = [1, 2, 3] 331 | XCTAssertEqual(defaults[.AnyOptKey] as! [Int], [1, 2, 3]) 332 | 333 | defaults[.AnyOptKey] = nil 334 | XCTAssertNil(defaults[.AnyOptKey]) 335 | } 336 | 337 | // MARK: - Other Key 338 | 339 | func testArchiving() { 340 | XCTAssertNil(defaults[.ColorOptKey]) 341 | 342 | defaults[.ColorOptKey] = .red 343 | XCTAssertEqual(defaults[.ColorOptKey], .red) 344 | 345 | defaults[.ColorOptKey] = .green 346 | XCTAssertEqual(defaults[.ColorOptKey], .green) 347 | } 348 | 349 | func testCoding() { 350 | XCTAssertNil(defaults[.RectOptKey]) 351 | 352 | var rect = CGRect(x: 1, y: 2, width: 3, height: 4) 353 | defaults[.RectOptKey] = rect 354 | XCTAssertEqual(defaults[.RectOptKey], rect) 355 | 356 | rect = CGRect(x: 5, y: 6, width: 7, height: 8) 357 | defaults[.RectOptKey] = rect 358 | XCTAssertEqual(defaults[.RectOptKey], rect) 359 | } 360 | 361 | func testBrokenData() { 362 | XCTAssertNil(defaults[.ColorOptKey]) 363 | 364 | let data = "BrokenData".data(using: .utf8)! 365 | defaults.set(data, forKey: UserDefaults.DefaultsKeys.ColorOptKey.key) 366 | 367 | XCTAssertTrue(defaults.contains(.ColorOptKey)) 368 | XCTAssertNil(defaults[.ColorOptKey]) 369 | } 370 | 371 | func testContainment() { 372 | XCTAssertFalse(defaults.contains(.StringOptKey)) 373 | XCTAssertTrue(defaults.contains(.StringKey)) 374 | } 375 | 376 | func testRemoving() { 377 | XCTAssertFalse(defaults.contains(.StringOptKey)) 378 | XCTAssertNil(defaults[.StringOptKey]) 379 | 380 | defaults[.StringOptKey] = "foo" 381 | XCTAssertEqual(defaults[.StringOptKey], "foo") 382 | 383 | XCTAssert(defaults.contains(.StringOptKey)) 384 | XCTAssertNotNil(defaults[.StringOptKey]) 385 | 386 | defaults.remove(.StringOptKey) 387 | 388 | XCTAssertFalse(defaults.contains(.StringOptKey)) 389 | XCTAssertNil(defaults[.StringOptKey]) 390 | } 391 | 392 | func testRemovingAll() { 393 | XCTAssertFalse(defaults.contains(.StringOptKey)) 394 | XCTAssertNil(defaults[.StringOptKey]) 395 | 396 | defaults[.StringOptKey] = "foo" 397 | XCTAssertEqual(defaults[.StringOptKey], "foo") 398 | 399 | XCTAssert(defaults.contains(.StringOptKey)) 400 | XCTAssertNotNil(defaults[.StringOptKey]) 401 | 402 | defaults.removeAll() 403 | 404 | XCTAssertFalse(defaults.contains(.StringOptKey)) 405 | XCTAssertNil(defaults[.StringOptKey]) 406 | } 407 | 408 | // MARK: - 409 | 410 | func testRegistration() { 411 | let rect = CGRect(x: 1, y: 2, width: 3, height: 4) 412 | 413 | XCTAssertEqual(defaults[.IntKey], 0) 414 | XCTAssertNil(defaults[.StringOptKey]) 415 | XCTAssertNil(defaults[.ColorOptKey]) 416 | XCTAssertNil(defaults[.RectOptKey]) 417 | 418 | let dict: [UserDefaults.DefaultsKeys : Any] = [ 419 | .IntKey: 42, 420 | .StringOptKey: "foo", 421 | .ColorOptKey: Color.red, 422 | .RectOptKey: rect, 423 | ] 424 | defaults.register(defaults: dict) 425 | 426 | XCTAssertEqual(defaults[.IntKey], 42) 427 | XCTAssertEqual(defaults[.StringOptKey], "foo") 428 | XCTAssertEqual(defaults[.ColorOptKey], .red) 429 | XCTAssertEqual(defaults[.RectOptKey], rect) 430 | 431 | defaults.unregisterAll() 432 | } 433 | 434 | func testObserving() { 435 | let ex = expectation(description: "observing get called") 436 | #if !os(macOS) 437 | // FIXME: KVO get called twice on iOS/tvOS. Why? 438 | ex.expectedFulfillmentCount = 2 439 | #endif 440 | defaults[.IntKey] = 233 441 | let token = defaults.observe(.IntKey, options: [.old, .new]) { (defaults, change) in 442 | #if os(macOS) 443 | XCTAssertEqual(change.oldValue, 233) 444 | XCTAssertEqual(change.newValue, 234) 445 | #endif 446 | ex.fulfill() 447 | } 448 | defaults[.IntKey] = 234 449 | token.invalidate() 450 | defaults[.IntKey] = 123 451 | waitForExpectations(timeout: 0) 452 | } 453 | 454 | func testObservingWithOptional() { 455 | let ex = expectation(description: "observing get called") 456 | #if !os(macOS) 457 | ex.expectedFulfillmentCount = 2 458 | #endif 459 | defaults[.StringOptKey] = "foo" 460 | let token = defaults.observe(.StringOptKey, options: [.old, .new]) { defaults, change in 461 | #if os(macOS) 462 | XCTAssertEqual(change.oldValue, "foo") 463 | XCTAssertEqual(change.newValue, "bar") 464 | #endif 465 | ex.fulfill() 466 | } 467 | defaults[.StringOptKey] = "bar" 468 | token.invalidate() 469 | defaults[.StringOptKey] = "baz" 470 | waitForExpectations(timeout: 0) 471 | } 472 | 473 | func testObservingWithArchiving() { 474 | let ex = expectation(description: "observing get called") 475 | #if !os(macOS) 476 | ex.expectedFulfillmentCount = 2 477 | #endif 478 | defaults[.ColorOptKey] = .red 479 | let token = defaults.observe(.ColorOptKey, options: [.old, .new]) { (defaults, change) in 480 | #if os(macOS) 481 | XCTAssertEqual(change.oldValue, .red) 482 | XCTAssertEqual(change.newValue, .green) 483 | #endif 484 | ex.fulfill() 485 | } 486 | defaults[.ColorOptKey] = .green 487 | token.invalidate() 488 | defaults[.ColorOptKey] = .blue 489 | waitForExpectations(timeout: 0) 490 | } 491 | 492 | func testObservingWithCoding() { 493 | let ex = expectation(description: "observing get called") 494 | #if !os(macOS) 495 | ex.expectedFulfillmentCount = 2 496 | #endif 497 | let rect1 = CGRect(x: 1, y: 2, width: 3, height: 4) 498 | let rect2 = CGRect(x: 5, y: 6, width: 7, height: 8) 499 | defaults[.RectOptKey] = rect1 500 | let token = defaults.observe(.RectOptKey, options: [.old, .new]) { (defaults, change) in 501 | #if os(macOS) 502 | XCTAssertEqual(change.oldValue, rect1) 503 | XCTAssertEqual(change.newValue, rect2) 504 | #endif 505 | ex.fulfill() 506 | } 507 | defaults[.RectOptKey] = rect2 508 | token.invalidate() 509 | defaults[.RectOptKey] = rect1 510 | waitForExpectations(timeout: 0) 511 | } 512 | 513 | func testObservingMultipleKeys() { 514 | let ex = expectation(description: "observing get called") 515 | #if os(macOS) 516 | ex.expectedFulfillmentCount = 2 517 | #else 518 | // FIXME: KVO get called twice on iOS/tvOS. Why? 519 | ex.expectedFulfillmentCount = 4 520 | #endif 521 | let token = defaults.observe(keys: [.IntKey, .StringKey], options: [.old, .new]) { 522 | ex.fulfill() 523 | } 524 | defaults[.IntKey] = 42 525 | defaults[.StringKey] = "foo" 526 | token.invalidate() 527 | defaults[.IntKey] = 123 528 | waitForExpectations(timeout: 0) 529 | } 530 | 531 | #if os(macOS) 532 | func testBinding() { 533 | class A: NSObject { @objc dynamic var rect: CGRect = .zero } 534 | let bindingName = NSBindingName("rect") 535 | 536 | let a = A() 537 | let rect = CGRect(x: 1, y: 1, width: 1, height: 1) 538 | 539 | a.bind(bindingName, defaultsKey: .RectOptKey) 540 | defaults[.RectOptKey] = rect 541 | XCTAssertEqual(a.rect, rect) 542 | a.unbind(bindingName) 543 | } 544 | #endif 545 | } 546 | --------------------------------------------------------------------------------