├── .gitignore ├── Tests ├── XCTestManifests.swift ├── EnvironmentVariableTests │ └── EnvironmentVariableTests.swift ├── LinuxMain.swift ├── LateInitTests │ └── LateInitTests.swift ├── LazyTests │ ├── LazyConstantTests.swift │ └── LazyTests.swift ├── TrimmedTests │ └── TrimmedTests.swift ├── CopyingTests │ └── CopyingTests.swift ├── AtomicWriteTests │ └── AtomicWriteTests.swift ├── DefaultValueTests │ └── DefaultValueTests.swift ├── DynamicUIColorTests │ └── DynamicUIColorTests.swift ├── ClampingTests │ └── ClampingTests.swift ├── ExpirableTests │ └── ExpirableTests.swift ├── UserDefaultTests │ └── UserDefaultTests.swift └── UndoRedoTests │ └── UndoRedoTests.swift ├── Sources ├── Copying │ └── Copying.swift ├── EnvironmentVariable │ └── EnvironmentVariable.swift ├── Trimmed │ └── Trimmed.swift ├── Clamping │ └── Clamping.swift ├── LateInit │ └── LateInit.swift ├── Lazy │ └── Lazy.swift ├── LazyConstant │ └── LazyConstant.swift ├── DefaultValue │ └── DefaultValue.swift ├── AtomicWrite │ └── AtomicWrite.swift ├── UserDefault │ └── UserDefault.swift ├── DynamicUIColor │ └── DynamicUIColor.swift ├── Expirable │ └── Expirable.swift └── UndoRedo │ └── UndoRedo.swift ├── LICENSE ├── CONTRIBUTING.md ├── Burritos.podspec ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm 6 | Package.resolved 7 | 8 | # Ignore UserDefault .plists generated during testing 9 | /Tests/UserDefaultTests/UserDefaultTests*.plist 10 | -------------------------------------------------------------------------------- /Tests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(AtomicWriteTests.allTests), 7 | testCase(ClampingTests.allTests), 8 | testCase(CopyingTests.allTests), 9 | testCase(DefaultValueTests.allTests), 10 | #if canImport(UIKit) { 11 | testCase(DynamicUIColor.allTests), 12 | } 13 | testCase(EnvironmentVariableTests.allTests), 14 | testCase(ExpirableTests.allTests), 15 | testCase(LateInitTests.allTests), 16 | testCase(LazyTests.allTests), 17 | testCase(LazyConstantTests.allTests), 18 | testCase(Trimmed.allTests), 19 | testCase(UndoRedoTests.allTests), 20 | testCase(UserDefaultTests.allTests), 21 | ] 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /Tests/EnvironmentVariableTests/EnvironmentVariableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentVariableTests.swift 3 | // 4 | // 5 | // Created by Luciano Almeida on 30/06/19. 6 | // 7 | 8 | import XCTest 9 | @testable import EnvironmentVariable 10 | 11 | final class EnvironmentVariableTests: XCTestCase { 12 | 13 | @EnvironmentVariable(name: "ENV_VAR") var envVar: String? 14 | 15 | func testGetAndSet() { 16 | XCTAssertNil(envVar) 17 | 18 | envVar = "ENV_VALUE" 19 | XCTAssertEqual(envVar, "ENV_VALUE") 20 | } 21 | 22 | func testSetNil() { 23 | envVar = "ENV_VALUE" 24 | XCTAssertEqual(envVar, "ENV_VALUE") 25 | 26 | envVar = nil 27 | XCTAssertNil(envVar) 28 | } 29 | 30 | static var allTests = [ 31 | ("testGetAndSet", testGetAndSet), 32 | ("testSetNil", testSetNil) 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import AtomicWriteTests 4 | import ClampingTests 5 | import CopyingTests 6 | import DefaultValueTests 7 | import EnvironmentVariableTests 8 | import ExpirableTests 9 | import LateInitTests 10 | import LazyTests 11 | import TrimmedTests 12 | import UndoRedoTests 13 | import UserDefaultTests 14 | 15 | var tests = [XCTestCaseEntry]() 16 | tests += AtomicWriteTests.allTests() 17 | tests += ClampingTests.allTests() 18 | tests += CopyingTests.allTests() 19 | tests += DefaultValueTests.allTests() 20 | // DynamicUIColor is only supported in iOS (UIKit) 21 | tests += EnvironmentVariableTests.allTests() 22 | tests += ExpirableTests.allTests() 23 | tests += LateInitTests.allTests() 24 | tests += LazyTests.allTests() 25 | tests += LazyConstantTests.allTests() 26 | tests += Trimmed.allTests() 27 | tests += UndoRedoTests.allTests() 28 | tests += UserDefaultTests.allTests() 29 | 30 | XCTMain(tests) 31 | -------------------------------------------------------------------------------- /Tests/LateInitTests/LateInitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LateInitTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 17/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import LateInit 10 | 11 | final class LateInitTests: XCTestCase { 12 | 13 | @LateInit var text: String 14 | 15 | override func setUp() { 16 | _text.storage = nil 17 | } 18 | 19 | func testInternalStorage() { 20 | XCTAssertNil(_text.storage) 21 | } 22 | 23 | func testGet() { 24 | // TODO: Test for fatalError() requires work. 25 | } 26 | 27 | func testSet() { 28 | text = "New text" 29 | 30 | XCTAssertEqual(text, "New text") 31 | XCTAssertEqual(_text.storage, "New text") 32 | } 33 | 34 | static var allTests = [ 35 | ("testInternalStorage", testInternalStorage), 36 | ("testGet", testGet), 37 | ("testSet", testSet), 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Copying/Copying.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 17/06/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A property wrapper that copies the value both on initialization and reassignment. 11 | /// 12 | /// - Source: 13 | /// [Proposal SE-0258](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#nscopying) 14 | /// [WWDC 2019 Modern Swift API Design](https://developer.apple.com/videos/play/wwdc2019/415/) 15 | @available(iOS 2.0, OSX 10.0, tvOS 9.0, watchOS 2.0, *) 16 | @propertyWrapper 17 | public struct Copying { 18 | var storage: Value 19 | 20 | public init(wrappedValue: Value) { 21 | storage = wrappedValue.copy() as! Value 22 | } 23 | 24 | public init(withoutCopying value: Value) { 25 | storage = value 26 | } 27 | 28 | public var wrappedValue: Value { 29 | get { return storage } 30 | set { storage = newValue.copy() as! Value } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Guillermo Muntaner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Tests/LazyTests/LazyConstantTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 26/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import LazyConstant 10 | 11 | final class LazyConstantTests: XCTestCase { 12 | 13 | @LazyConstant var text = "Hello, World!" 14 | 15 | override func setUp() { 16 | _text = LazyConstant(wrappedValue: "Hello, World!") 17 | } 18 | 19 | func testLazyInternalStorage() { 20 | XCTAssertNil(_text.storage) 21 | } 22 | 23 | func testGet() { 24 | XCTAssertEqual(text, "Hello, World!") 25 | XCTAssertEqual(_text.storage, "Hello, World!") 26 | } 27 | 28 | func testReset() { 29 | _ = text // Force init 30 | 31 | _text.reset() 32 | XCTAssertNil(_text.storage) 33 | } 34 | 35 | // MARK : LazyConstant 36 | 37 | static var allTests = [ 38 | ("testLazyInternalStorage", testLazyInternalStorage), 39 | ("testGet", testGet), 40 | ("testReset", testReset), 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Tests/TrimmedTests/TrimmedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrimmedTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 03/07/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import Trimmed 10 | 11 | final class TrimmedTests: XCTestCase { 12 | 13 | @Trimmed var text = " Hello, World! \n \n" 14 | 15 | override func setUp() { 16 | _text = Trimmed(wrappedValue: " Hello, World! \n \n") 17 | } 18 | 19 | func testGet() { 20 | XCTAssertEqual(text, "Hello, World!") 21 | } 22 | 23 | func testSet() { 24 | text = " \n Hi \n" 25 | XCTAssertEqual(text, "Hi") 26 | } 27 | 28 | func testCustomCharacterSet() { 29 | _text = Trimmed(wrappedValue: "", characterSet: CharacterSet(charactersIn: "abcde")) 30 | text = "abcdeHello World!abcde" 31 | XCTAssertEqual(text, "Hello World!") 32 | } 33 | 34 | static var allTests = [ 35 | ("testGet", testGet), 36 | ("testSet", testSet), 37 | ("testCustomCharacterSet", testCustomCharacterSet), 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Feel free to open pull requests to add new property wrappers (aka burritos!). In order to keep the project organized please follow those guidelines: 4 | * 1 pull request for 1 property wrapper. 5 | * Include a test for your wrapper covering, at least, its basic get/set logic. 6 | * Follow the folder structure and name convention of: 7 | * `Sources/{PropertyWrapperStructName}/{PropertyWrapperStructName}.swift` 8 | * `Tests/{PropertyWrapperStructName}Tests/{PropertyWrapperStructName}Tests.swift` 9 | * Add a `static var allTests = []` to your `XCTestCase` with references to your tests. Reference this property from both `LinuxMain.swift` and `XCTestManifests.swift`. 10 | * Configure the `Package.swift` manifest with a new `.target` and `.testTarget`. Add the target to the library inside products. 11 | * Update the `README.md` with a new section including a description and sample usage. 12 | * Updated the `Burritos.podspec` with a new subspec for your wrapper pointing to the source files and listing the dependencies. Check existing wrappers as examples. 13 | 14 | Feel free to open issues or pull requests for any other bug, improvement or fix. 15 | 16 | Thank you. 17 | -------------------------------------------------------------------------------- /Tests/LazyTests/LazyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 16/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import Lazy 10 | 11 | final class LazyTests: XCTestCase { 12 | 13 | @Lazy var text = "Hello, World!" 14 | 15 | override func setUp() { 16 | _text = Lazy(wrappedValue: "Hello, World!") 17 | } 18 | 19 | func testLazyInternalStorage() { 20 | XCTAssertNil(_text.storage) 21 | } 22 | 23 | func testGet() { 24 | XCTAssertEqual(text, "Hello, World!") 25 | XCTAssertEqual(_text.storage, "Hello, World!") 26 | } 27 | 28 | func testSet() { 29 | text = "New text" 30 | 31 | XCTAssertEqual(text, "New text") 32 | XCTAssertEqual(_text.storage, "New text") 33 | } 34 | 35 | func testReset() { 36 | _ = text // Force init 37 | 38 | _text.reset() 39 | XCTAssertNil(_text.storage) 40 | } 41 | 42 | static var allTests = [ 43 | ("testLazyInternalStorage", testLazyInternalStorage), 44 | ("testGet", testGet), 45 | ("testSet", testSet), 46 | ("testReset", testReset), 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /Sources/EnvironmentVariable/EnvironmentVariable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentVariable.swift 3 | // 4 | // 5 | // Created by Luciano Almeida on 30/06/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A property wrapper to set and get system's environment variables values. 11 | /// 12 | /// ``` 13 | /// @EnvironmentVariable(name: "PATH") 14 | /// var path: String? 15 | /// 16 | /// // You can set the environment variable directly: 17 | /// path = "~/opt/bin:" + path! 18 | /// 19 | /// ``` 20 | /// 21 | /// Some related reads & inspiration: 22 | /// [swift-evolution proposal](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) 23 | /// [Environment variables in Mac OSX](https://stackoverflow.com/a/4567308) 24 | @propertyWrapper 25 | public struct EnvironmentVariable { 26 | var name: String 27 | 28 | public var wrappedValue: String? { 29 | get { 30 | guard let pointer = getenv(name) else { return nil } 31 | return String(cString: pointer) 32 | } 33 | set { 34 | guard let value = newValue else { 35 | unsetenv(name) 36 | return 37 | } 38 | setenv(name, value, 1) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Trimmed/Trimmed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Trimmed.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 03/07/2019. 6 | // 7 | import Foundation 8 | 9 | /// A wrapper arround a string that automatically trims both on initialization and reassignment. 10 | /// 11 | /// By default it trimms white spaces and new lines, but the character set to use can be also passed during construction. 12 | /// Usage: 13 | /// ``` 14 | /// @Trimmed 15 | /// var text = " \n Hello, World! \n\n " 16 | /// 17 | /// print(text) // "Hello, World!" 18 | /// 19 | /// // By default trims white spaces and new lines, but it also supports any character set 20 | /// @Trimmed(characterSet: .whitespaces) 21 | /// var text = " \n Hello, World! \n\n " 22 | /// print(text) // "\n Hello, World! \n\n" 23 | /// ``` 24 | @propertyWrapper 25 | public struct Trimmed { 26 | private var value: String! 27 | private let characterSet: CharacterSet 28 | 29 | public var wrappedValue: String { 30 | get { value } 31 | set { value = newValue.trimmingCharacters(in: characterSet) } 32 | } 33 | 34 | public init(wrappedValue: String) { 35 | self.characterSet = .whitespacesAndNewlines 36 | self.wrappedValue = wrappedValue 37 | } 38 | 39 | public init(wrappedValue: String, characterSet: CharacterSet) { 40 | self.characterSet = characterSet 41 | self.wrappedValue = wrappedValue 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Clamping/Clamping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clamping.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 25/06/2019. 6 | // 7 | 8 | /// A property wrapper that automatically clamps its wrapped value in a range. 9 | /// 10 | /// ``` 11 | /// @Clamping(range: 0...1) 12 | /// var alpha: Double = 0.0 13 | /// 14 | /// alpha = 2.5 15 | /// print(alpha) // 1.0 16 | /// 17 | /// alpha = -1.0 18 | /// print(alpha) // 0.0 19 | /// ``` 20 | /// 21 | /// - Note: Using a Type whose capacity fits your range should always be prefered to using this wrapper; e.g. you can use an UInt8 for 0-255 values. 22 | /// 23 | /// [Swift Evolution Proposal example](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#clamping-a-value-within-bounds) 24 | /// [NSHisper article](https://nshipster.com/propertywrapper/) 25 | @propertyWrapper 26 | public struct Clamping { 27 | var value: Value 28 | let range: ClosedRange 29 | 30 | public init(wrappedValue: Value, range: ClosedRange) { 31 | self.range = range 32 | self.value = range.clamp(wrappedValue) 33 | } 34 | 35 | public var wrappedValue: Value { 36 | get { value } 37 | set { value = range.clamp(newValue) } 38 | } 39 | } 40 | 41 | fileprivate extension ClosedRange { 42 | func clamp(_ value : Bound) -> Bound { 43 | return self.lowerBound > value ? self.lowerBound 44 | : self.upperBound < value ? self.upperBound 45 | : value 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/CopyingTests/CopyingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CopyingTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 17/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import Copying 10 | 11 | final class CopyingTests: XCTestCase { 12 | 13 | class SomeClass: NSCopying { 14 | func copy(with zone: NSZone? = nil) -> Any { return SomeClass() } 15 | } 16 | 17 | let initialInstance = SomeClass() 18 | 19 | @Copying var instance: SomeClass = .init() // Dummy value 20 | 21 | override func setUp() { 22 | _instance = Copying(wrappedValue: initialInstance) 23 | } 24 | 25 | func testCopyOnDefaultInit() { 26 | XCTAssert(instance !== initialInstance) 27 | } 28 | 29 | func testInitWithoutCopying() { 30 | _instance = Copying(withoutCopying: initialInstance) 31 | XCTAssert(instance === initialInstance) 32 | } 33 | 34 | func testCopyOnReassign() { 35 | let newInstance = SomeClass() 36 | instance = newInstance 37 | XCTAssert(instance !== newInstance) 38 | } 39 | 40 | func testGetWithoutCopying() { 41 | let newInstance = SomeClass() 42 | _instance.storage = newInstance 43 | XCTAssert(instance === newInstance) 44 | } 45 | 46 | // MARK: - allTests 47 | 48 | static var allTests = [ 49 | ("testCopyOnDefaultInit", testCopyOnDefaultInit), 50 | ("testInitWithoutCopying", testInitWithoutCopying), 51 | ("testCopyOnReassign", testCopyOnReassign), 52 | ("testGetWithoutCopying", testGetWithoutCopying), 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /Tests/AtomicWriteTests/AtomicWriteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 18/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import AtomicWrite 10 | 11 | final class AtomicWriteTests: XCTestCase { 12 | 13 | let iterations = 1000 14 | 15 | @AtomicWrite var count: Int = 0 16 | 17 | override func setUp() { 18 | _count = AtomicWrite(wrappedValue: 0) 19 | } 20 | 21 | func testGet() { 22 | XCTAssertEqual(count, 0) 23 | XCTAssertEqual(_count.value, 0) 24 | } 25 | 26 | func testSet() { 27 | count = 99 28 | 29 | XCTAssertEqual(count, 99) 30 | XCTAssertEqual(_count.value, 99) 31 | } 32 | 33 | /// Tests the issue with this property wrapper which is lack of native read & write exclusivity 34 | func testNonExclusiveReadWrite() { 35 | DispatchQueue.concurrentPerform(iterations: iterations) { index in 36 | count += 1 37 | } 38 | XCTAssertNotEqual(count, iterations) 39 | } 40 | 41 | func testMutateHelperForExclusiveReadWrite() { 42 | DispatchQueue.concurrentPerform(iterations: iterations) { index in 43 | _count.mutate { 44 | $0 += 1 45 | } 46 | } 47 | XCTAssertEqual(count, iterations) 48 | } 49 | 50 | static var allTests = [ 51 | ("testGet", testGet), 52 | ("testSet", testSet), 53 | ("testNonExclusiveReadWrite", testNonExclusiveReadWrite), 54 | ("testMutateHelperForExclusiveReadWrite", testMutateHelperForExclusiveReadWrite), 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /Sources/LateInit/LateInit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LateInit.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 17/06/2019. 6 | // 7 | 8 | /// A property wrapper which lets you left a stored property uninitialized during construction and set its value later. 9 | /// 10 | /// In Swift *classes and structures must set all of their stored properties to an appropriate initial value by the time 11 | /// an instance of that class or structure is created. Stored properties cannot be left in an indeterminate state. *. 12 | /// 13 | /// LateInit lets you work around this restriction and leave a stored properties uninitialized. This also means you are 14 | /// responsible of initializing the property before it is accessed. Failing to do so will result in a fatal error. 15 | /// Sounds familiar? LateInit is an reimplementation of a Swift "Implicitly Unwrapped Optional". 16 | /// 17 | /// Usage: 18 | /// ``` 19 | /// @LateInit var text: String 20 | /// 21 | /// // Note: Access before initialization triggers a fatal error: 22 | /// // print(text) // -> fatalError("Trying to access LateInit.value before setting it.") 23 | /// 24 | /// // Initialize later in your code: 25 | /// text = "Hello, World!" 26 | /// 27 | @propertyWrapper 28 | public struct LateInit { 29 | 30 | var storage: Value? 31 | 32 | public init() { 33 | storage = nil 34 | } 35 | 36 | public var wrappedValue: Value { 37 | get { 38 | guard let storage = storage else { 39 | fatalError("Trying to access LateInit.value before setting it.") 40 | } 41 | return storage 42 | } 43 | set { 44 | storage = newValue 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Lazy/Lazy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lazy.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 16/06/2019. 6 | // 7 | 8 | /// A property wrapper which delays instantiation until first read access. 9 | /// 10 | /// It is a reimplementation of Swift `lazy` modifier using a property wrapper. 11 | /// As an extra on top of `lazy` it offers reseting the wrapper to its "uninitialized" state. 12 | /// 13 | /// Usage: 14 | /// ``` 15 | /// @Lazy var result = expensiveOperation() 16 | /// ... 17 | /// print(result) // expensiveOperation() is executed at this point 18 | /// ``` 19 | /// 20 | /// As an extra on top of `lazy` it offers reseting the wrapper to its "uninitialized" state. 21 | @propertyWrapper 22 | public struct Lazy { 23 | 24 | var storage: Value? 25 | let constructor: () -> Value 26 | 27 | /// Creates a lazy property with the closure to be executed to provide an initial value once the wrapped property is first accessed. 28 | /// 29 | /// This constructor is automatically used when assigning the initial value of the property, so simply use: 30 | /// 31 | /// @Lazy var text = "Hello, World!" 32 | /// 33 | public init(wrappedValue constructor: @autoclosure @escaping () -> Value) { 34 | self.constructor = constructor 35 | } 36 | 37 | public var wrappedValue: Value { 38 | mutating get { 39 | if storage == nil { 40 | self.storage = constructor() 41 | } 42 | return storage! 43 | } 44 | set { 45 | storage = newValue 46 | } 47 | } 48 | 49 | // MARK: Utils 50 | 51 | /// Resets the wrapper to its initial state. The wrapped property will be initialized on next read access. 52 | public mutating func reset() { 53 | storage = nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/LazyConstant/LazyConstant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyConstant.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 26/06/2019. 6 | // 7 | 8 | /// A property wrapper which delays instantiation until first read access and prevents 9 | /// changing or mutating its wrapped value. 10 | /// 11 | /// Usage: 12 | /// ``` 13 | /// @Lazy var result = expensiveOperation() 14 | /// ... 15 | /// print(result) // expensiveOperation() is executed at this point 16 | /// 17 | /// result = newResult // Compiler error 18 | /// ``` 19 | /// 20 | /// As an extra on top of `lazy` it offers reseting the wrapper to its "uninitialized" state. 21 | /// 22 | /// - Note: This wrapper prevents reassigning the wrapped property value but *NOT* the wrapper itself. 23 | /// Reassigning the wrapper `_value = LazyConstant(wrappedValue: "Hola!")` is possible and 24 | /// since wrappers themselves need to be declared variable there is no way to prevent it. 25 | @propertyWrapper 26 | public struct LazyConstant { 27 | 28 | private(set) var storage: Value? 29 | let constructor: () -> Value 30 | 31 | /// Creates a constnat lazy property with the closure to be executed to provide an initial value once the wrapped property is first accessed. 32 | /// 33 | /// This constructor is automatically used when assigning the initial value of the property, so simply use: 34 | /// 35 | /// @Lazy var text = "Hello, World!" 36 | /// 37 | public init(wrappedValue constructor: @autoclosure @escaping () -> Value) { 38 | self.constructor = constructor 39 | } 40 | 41 | public var wrappedValue: Value { 42 | mutating get { 43 | if storage == nil { 44 | storage = constructor() 45 | } 46 | return storage! 47 | } 48 | } 49 | 50 | /// Resets the wrapper to its initial state. The wrapped property will be initialized on next read access. 51 | public mutating func reset() { 52 | storage = nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/DefaultValue/DefaultValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultValue.swift 3 | // Burritos 4 | // 5 | // 6 | // Created by Evgeniy (@own2pwn) on 28/06/2019. 7 | // 8 | 9 | /// A property wrapper arround an implicitly unwrapped optional value which fallbacks to a given default value. 10 | /// 11 | /// Seting `nil` will reset the property to the default value. The wrapper will always return non-null value, 12 | /// thanks to the granted default value. 13 | /// Usage: 14 | /// 15 | /// ```swift 16 | /// @DefaultValue(default: 0) 17 | /// var count 18 | /// count = 100 19 | /// // or 20 | /// @DefaultValue(default: 0) 21 | /// var count = 100 22 | /// 23 | /// // Assigning nil resets to the default value 24 | /// print(count) // 100 25 | /// count = nil 26 | /// print(count) // 0 27 | /// ``` 28 | /// 29 | /// - Note: Since this wrapper relies on an implicitly unwrapped optional, using optionals as the wrapper type is 30 | /// discouraged since they will be flattered. Getting the value is always a flat implicit unwrapped optional and assigning 31 | /// `nil` is always reseting to the default value. Two levels of optionality need to be nested in order to be able to store 32 | /// null values. 33 | @propertyWrapper 34 | public struct DefaultValue { 35 | // MARK: - Members 36 | 37 | private var value: Value? 38 | 39 | private let defaultValue: Value 40 | 41 | // MARK: - Property wrapper interface 42 | 43 | public var wrappedValue: Value! { 44 | get { 45 | if let unboxed = value { 46 | return unboxed 47 | } 48 | 49 | return defaultValue 50 | } 51 | set { 52 | value = newValue 53 | } 54 | } 55 | 56 | // MARK: - Init 57 | 58 | public init(wrappedValue: Value? = nil, default: Value) { 59 | defaultValue = `default` 60 | value = wrappedValue 61 | } 62 | 63 | // MARK: - Public API 64 | 65 | /// Resets the wrapper to its default value. This is equivalent to setting nil. 66 | public mutating func reset() { 67 | value = nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/DefaultValueTests/DefaultValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultValueTests.swift 3 | // Burritos 4 | // 5 | // Created by Evgeniy (@own2pwn) on 28/06/2019. 6 | // 7 | 8 | @testable import DefaultValue 9 | import XCTest 10 | 11 | final class DefaultValueTests: XCTestCase { 12 | 13 | // MARK: - Members 14 | 15 | private static let defaultValue = 4.0 16 | 17 | // MARK: - Members 18 | 19 | @DefaultValue(default: DefaultValueTests.defaultValue) 20 | var double = 5 21 | 22 | @DefaultValue(default: "Hello, World!") 23 | var optional: String? 24 | 25 | // MARK: - Tests 26 | 27 | override func setUp() { 28 | _double = DefaultValue(default: DefaultValueTests.defaultValue) 29 | _optional = DefaultValue(default: "Hello, World!") 30 | } 31 | 32 | func testGetDefaultValue() { 33 | XCTAssertEqual(double, DefaultValueTests.defaultValue) 34 | } 35 | 36 | func testGetNonDefaultValue() { 37 | _double = DefaultValue(wrappedValue: 5, default: DefaultValueTests.defaultValue) 38 | XCTAssertEqual(double, 5) 39 | } 40 | 41 | func testSet() { 42 | double = 6 43 | XCTAssertEqual(double, 6) 44 | } 45 | 46 | func testResetBySettingNil() { 47 | _double = DefaultValue(wrappedValue: 5, default: DefaultValueTests.defaultValue) 48 | double = nil 49 | XCTAssertEqual(double, DefaultValueTests.defaultValue) 50 | } 51 | 52 | func testReset() { 53 | _double = DefaultValue(wrappedValue: 5, default: DefaultValueTests.defaultValue) 54 | _double.reset() 55 | XCTAssertEqual(double, DefaultValueTests.defaultValue) 56 | } 57 | 58 | func testOptionalReset() { 59 | _optional = DefaultValue(default: "Hello, World!") 60 | optional = "Yay" 61 | optional = nil 62 | XCTAssertEqual(optional, "Hello, World!") 63 | } 64 | 65 | // MARK: - Helpers 66 | 67 | static var allTests = [ 68 | ("testGetDefaultValue", testGetDefaultValue), 69 | ("testGetNonDefaultValue", testGetNonDefaultValue), 70 | ("testSet", testSet), 71 | ("testResetBySettingNil", testResetBySettingNil), 72 | ("testReset", testReset), 73 | ("testOptionalReset", testOptionalReset), 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /Tests/DynamicUIColorTests/DynamicUIColorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicUIColorTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 19/06/2019. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | import XCTest 11 | @testable import DynamicUIColor 12 | 13 | final class DynamicUIColorTests: XCTestCase { 14 | 15 | @DynamicUIColor(light: .white, dark: .black) 16 | var backgroundColor: UIColor 17 | 18 | static var style: DynamicUIColor.Style = .light 19 | 20 | func testGetWithDefaultStyle() { 21 | _backgroundColor = DynamicUIColor(light: .white, dark: .black) 22 | 23 | if #available(iOS 13, tvOS 13, *) { 24 | let lightTrait = UITraitCollection(userInterfaceStyle: .light) 25 | XCTAssertEqual(backgroundColor.resolvedColor(with: lightTrait), .white) 26 | let darkTrait = UITraitCollection(userInterfaceStyle: .dark) 27 | XCTAssertEqual(backgroundColor.resolvedColor(with: darkTrait), .black) 28 | } else { 29 | XCTAssertEqual(backgroundColor, .white) 30 | } 31 | } 32 | 33 | func testGetWithNilStyle() { 34 | _backgroundColor = DynamicUIColor(light: .white, dark: .black, style: nil) 35 | 36 | if #available(iOS 13, tvOS 13, *) { 37 | let lightTrait = UITraitCollection(userInterfaceStyle: .light) 38 | XCTAssertEqual(backgroundColor.resolvedColor(with: lightTrait), .white) 39 | let darkTrait = UITraitCollection(userInterfaceStyle: .dark) 40 | XCTAssertEqual(backgroundColor.resolvedColor(with: darkTrait), .black) 41 | } else { 42 | XCTAssertEqual(backgroundColor, .white) 43 | } 44 | } 45 | 46 | func testGetWithCustomStyle() { 47 | _backgroundColor = DynamicUIColor(light: .white, dark: .black, style: DynamicUIColorTests.style) 48 | 49 | DynamicUIColorTests.style = .light 50 | XCTAssertEqual(backgroundColor, .white) 51 | 52 | DynamicUIColorTests.style = .dark 53 | XCTAssertEqual(backgroundColor, .black) 54 | } 55 | 56 | static var allTests = [ 57 | ("testGetWithDefaultStyle", testGetWithDefaultStyle), 58 | ("testGetWithNilStyle", testGetWithNilStyle), 59 | ("testGetWithCustomStyle", testGetWithCustomStyle), 60 | ] 61 | } 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /Tests/ClampingTests/ClampingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClampingTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 25/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import Clamping 10 | 11 | final class ClampingTests: XCTestCase { 12 | 13 | @Clamping(range: 0...1) 14 | var alpha: Double = 0.3 15 | 16 | override func setUp() { 17 | alpha = 0.3 18 | } 19 | 20 | // MARK: Get 21 | 22 | func testGet() { 23 | XCTAssertEqual(alpha, 0.3) 24 | } 25 | 26 | // MARK: Init 27 | 28 | func testInitInRange() { 29 | _alpha = Clamping(wrappedValue: 0.5, range: 0...1) 30 | XCTAssertEqual(alpha, 0.5) 31 | } 32 | 33 | func testInitLessOrEqualThanLowerBound() { 34 | [-Double.greatestFiniteMagnitude, -1.0, 0.0].forEach { value in 35 | _alpha = Clamping(wrappedValue: value, range: 0...1) 36 | XCTAssertEqual(alpha, 0) 37 | } 38 | } 39 | 40 | func testInitBiggerOrEqualThanUpperBound() { 41 | [1.0, 1.5, Double.greatestFiniteMagnitude, Double.infinity].forEach { value in 42 | _alpha = Clamping(wrappedValue: value, range: 0...1) 43 | XCTAssertEqual(alpha, 1) 44 | } 45 | } 46 | 47 | // MARK: Set 48 | 49 | func testSetInRange() { 50 | alpha = 0.5 51 | XCTAssertEqual(alpha, 0.5) 52 | } 53 | 54 | func testSetLessOrEqualThanLowerBound() { 55 | [-Double.greatestFiniteMagnitude, -1.0, 0.0].forEach { value in 56 | alpha = value 57 | XCTAssertEqual(alpha, 0) 58 | } 59 | } 60 | 61 | func testSetBiggerOrEqualThanUpperBound() { 62 | [1.0, 1.5, Double.greatestFiniteMagnitude, Double.infinity].forEach { value in 63 | alpha = value 64 | XCTAssertEqual(alpha, 1) 65 | } 66 | } 67 | 68 | // MARK: Utils 69 | 70 | static var allTests = [ 71 | ("testGet", testGet), 72 | ("testInitInRange", testInitInRange), 73 | ("testInitLessOrEqualThanLowerBound", testInitLessOrEqualThanLowerBound), 74 | ("testInitBiggerOrEqualThanUpperBound", testInitBiggerOrEqualThanUpperBound), 75 | ("testSetInRange", testSetInRange), 76 | ("testSetLessOrEqualThanLowerBound", testSetLessOrEqualThanLowerBound), 77 | ("testSetBiggerOrEqualThanUpperBound", testSetBiggerOrEqualThanUpperBound), 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /Sources/AtomicWrite/AtomicWrite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicWrite.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 18/06/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A property wrapper granting atomic write access to the wrapped property. 11 | /// Atomic mutation (read-modify-write) can be done using the wrapper `mutate` method. 12 | /// Reading access is not atomic but is exclusive with write & mutate operations. 13 | /// 14 | /// - Note: Getting and then setting is not an atomic operation. It is easy to unknowingly 15 | /// trigger a get & a set, e.g. when increasing a counter `count += 1`. Sadly such an atomic 16 | /// modification cannot be simply done with getters and setter, hence we expose the 17 | /// `mutate(_ action: (inout Value) -> Void)` method on the wrapper for this 18 | /// purpose which you can access with a _ prefix. 19 | /// 20 | /// ``` 21 | /// @Atomic var count = 0 22 | /// 23 | /// // You can atomically write (non-derived) values directly: 24 | /// count = 100 25 | /// 26 | /// // To mutate (read-modify-write) always use the wrapper method: 27 | /// _count.mutate { $0 += 1 } 28 | /// 29 | /// print(count) // 101 30 | /// ``` 31 | /// 32 | /// Some related reads & inspiration: 33 | /// [swift-evolution proposal](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) 34 | /// [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Sources/Atomic.swift) 35 | /// [obj.io](https://www.objc.io/blog/2019/01/15/atomic-variables-part-2/) 36 | @available(iOS 2.0, OSX 10.0, tvOS 9.0, watchOS 2.0, *) 37 | @propertyWrapper 38 | public struct AtomicWrite { 39 | 40 | // TODO: Faster version with os_unfair_lock? 41 | 42 | let queue = DispatchQueue(label: "Atomic write access queue", attributes: .concurrent) 43 | var value: Value 44 | 45 | public init(wrappedValue: Value) { 46 | self.value = wrappedValue 47 | } 48 | 49 | public var wrappedValue: Value { 50 | get { 51 | return queue.sync { value } 52 | } 53 | set { 54 | queue.sync(flags: .barrier) { value = newValue } 55 | } 56 | } 57 | 58 | /// Atomically mutate the variable (read-modify-write). 59 | /// 60 | /// - parameter action: A closure executed with atomic in-out access to the wrapped property. 61 | public mutating func mutate(_ mutation: (inout Value) throws -> Void) rethrows { 62 | return try queue.sync(flags: .barrier) { 63 | try mutation(&value) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/ExpirableTests/ExpirableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpirableTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 23/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import Expirable 10 | 11 | final class ExpirableTests: XCTestCase { 12 | 13 | @Expirable(duration: 1) 14 | var token: String? 15 | 16 | override func setUp() { 17 | token = "1234" 18 | } 19 | 20 | func testGet() { 21 | token = "1234" 22 | XCTAssertEqual(token, "1234") 23 | } 24 | 25 | func testGetExpired() { 26 | token = "1234" 27 | Thread.sleep(forTimeInterval: 2) 28 | XCTAssertFalse(_token.isValid) 29 | } 30 | 31 | func testSet() { 32 | token = "abc" 33 | XCTAssertEqual(token, "abc") 34 | } 35 | 36 | func testIsValid() { 37 | XCTAssertTrue(_token.isValid) 38 | } 39 | 40 | func testIsValidExpired() { 41 | token = "1234" 42 | Thread.sleep(forTimeInterval: 2) 43 | XCTAssertNil(token) 44 | } 45 | 46 | func testInitWithExistingValidToken() { 47 | let expirationDate = Date().addingTimeInterval(2) 48 | _token = Expirable(wrappedValue: "abc", expirationDate: expirationDate, duration: 2) 49 | XCTAssertEqual(token, "abc") 50 | Thread.sleep(forTimeInterval: 1) 51 | XCTAssertEqual(token, "abc") 52 | Thread.sleep(forTimeInterval: 3) 53 | XCTAssertNil(token) 54 | } 55 | 56 | func testInitWithExistingExpiredToken() { 57 | let pastDate = Date().addingTimeInterval(-2) 58 | _token = Expirable(wrappedValue: "abc", expirationDate: pastDate, duration: 2) 59 | XCTAssertNil(token) 60 | } 61 | 62 | func testSetWithCustomDate() { 63 | _token.set("abc", expirationDate: Date().addingTimeInterval(2)) 64 | XCTAssertEqual(token, "abc") 65 | Thread.sleep(forTimeInterval: 1) 66 | XCTAssertEqual(token, "abc") 67 | Thread.sleep(forTimeInterval: 3) 68 | XCTAssertNil(token) 69 | } 70 | 71 | static var allTests = [ 72 | ("testGet", testGet), 73 | ("testGetExpired", testGetExpired), 74 | ("testSet", testSet), 75 | ("testIsValid", testIsValid), 76 | ("testIsValidExpired", testIsValidExpired), 77 | ("testInitWithExistingValidToken", testInitWithExistingValidToken), 78 | ("testInitWithExistingExpiredToken", testInitWithExistingExpiredToken), 79 | ("testSetWithCustomDate", testSetWithCustomDate), 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /Tests/UserDefaultTests/UserDefaultTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 16/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import UserDefault 10 | 11 | final class UserDefaultTests: XCTestCase { 12 | 13 | // Note: Property delegates are not yet supported on local properties, hence using stored properties. 14 | 15 | @UserDefault("test", defaultValue: "Hello, World!") 16 | var test: String 17 | 18 | @UserDefault("count", defaultValue: 13) 19 | var count: Int 20 | 21 | func testGetDefaultValue() { 22 | let userDefaults = UserDefaults.makeClearedInstance() 23 | _test.userDefaults = userDefaults 24 | XCTAssertEqual(test, "Hello, World!") 25 | XCTAssertEqual(userDefaults.string(forKey: "test"), nil) 26 | } 27 | 28 | func testGet() { 29 | let userDefaults = UserDefaults.makeClearedInstance() 30 | userDefaults.set("Existing value for test key :D", forKey: "test") 31 | _test.userDefaults = userDefaults 32 | 33 | XCTAssertEqual(test, "Existing value for test key :D") 34 | XCTAssertEqual(userDefaults.string(forKey: "test"), "Existing value for test key :D") 35 | } 36 | 37 | func testSet() { 38 | let userDefaults = UserDefaults.makeClearedInstance() 39 | _test.userDefaults = userDefaults 40 | test = "A new value for test key :P" 41 | 42 | XCTAssertEqual(userDefaults.string(forKey: "test"), "A new value for test key :P") 43 | XCTAssertEqual(test, "A new value for test key :P") 44 | } 45 | 46 | func testInt() { 47 | _count.userDefaults = UserDefaults.makeClearedInstance() 48 | 49 | XCTAssertEqual(count, 13) 50 | count = 7 51 | XCTAssertEqual(count, 7) 52 | } 53 | 54 | static var allTests = [ 55 | ("testGetDefaultValue", testGetDefaultValue), 56 | ("testGet", testGet), 57 | ("testSet", testSet), 58 | ("testInt", testInt), 59 | ] 60 | } 61 | 62 | fileprivate extension UserDefaults { 63 | static func makeClearedInstance( 64 | for functionName: StaticString = #function, 65 | inFile fileName: StaticString = #file 66 | ) -> UserDefaults { 67 | let className = "\(fileName)".split(separator: ".")[0] 68 | let testName = "\(functionName)".split(separator: "(")[0] 69 | let suiteName = "\(className).\(testName)" 70 | 71 | let defaults = self.init(suiteName: suiteName)! 72 | defaults.removePersistentDomain(forName: suiteName) 73 | return defaults 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/UserDefault/UserDefault.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 16/06/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A type safe property wrapper to set and get values from UserDefaults with support for defaults values. 11 | /// 12 | /// Usage: 13 | /// ``` 14 | /// @UserDefault("has_seen_app_introduction", defaultValue: false) 15 | /// static var hasSeenAppIntroduction: Bool 16 | /// ``` 17 | /// 18 | /// [Apple documentation on UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults) 19 | @available(iOS 2.0, OSX 10.0, tvOS 9.0, watchOS 2.0, *) 20 | @propertyWrapper 21 | public struct UserDefault { 22 | let key: String 23 | let defaultValue: Value 24 | var userDefaults: UserDefaults 25 | 26 | public init(_ key: String, defaultValue: Value, userDefaults: UserDefaults = .standard) { 27 | self.key = key 28 | self.defaultValue = defaultValue 29 | self.userDefaults = userDefaults 30 | } 31 | 32 | public var wrappedValue: Value { 33 | get { 34 | return userDefaults.object(forKey: key) as? Value ?? defaultValue 35 | } 36 | set { 37 | userDefaults.set(newValue, forKey: key) 38 | } 39 | } 40 | } 41 | 42 | /// A type than can be stored in `UserDefaults`. 43 | /// 44 | /// - From UserDefaults; 45 | /// The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. 46 | /// For NSArray and NSDictionary objects, their contents must be property list objects. For more information, see What is a 47 | /// Property List? in Property List Programming Guide. 48 | public protocol PropertyListValue {} 49 | 50 | extension Data: PropertyListValue {} 51 | extension NSData: PropertyListValue {} 52 | 53 | extension String: PropertyListValue {} 54 | extension NSString: PropertyListValue {} 55 | 56 | extension Date: PropertyListValue {} 57 | extension NSDate: PropertyListValue {} 58 | 59 | extension NSNumber: PropertyListValue {} 60 | extension Bool: PropertyListValue {} 61 | extension Int: PropertyListValue {} 62 | extension Int8: PropertyListValue {} 63 | extension Int16: PropertyListValue {} 64 | extension Int32: PropertyListValue {} 65 | extension Int64: PropertyListValue {} 66 | extension UInt: PropertyListValue {} 67 | extension UInt8: PropertyListValue {} 68 | extension UInt16: PropertyListValue {} 69 | extension UInt32: PropertyListValue {} 70 | extension UInt64: PropertyListValue {} 71 | extension Double: PropertyListValue {} 72 | extension Float: PropertyListValue {} 73 | #if os(macOS) 74 | extension Float80: PropertyListValue {} 75 | #endif 76 | 77 | extension Array: PropertyListValue where Element: PropertyListValue {} 78 | 79 | extension Dictionary: PropertyListValue where Key == String, Value: PropertyListValue {} 80 | -------------------------------------------------------------------------------- /Burritos.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Burritos' 3 | s.version = '0.0.3' 4 | s.summary = 'A collection of well tested Swift Property Wrappers.' 5 | s.description = <<-DESC 6 | A collection of well tested Swift Property Wrappers. 7 | * @AtomicWrite 8 | * @Clamping 9 | * @Copying 10 | * @DefaultValue 11 | * @DynamicUIColor 12 | * @Expirable 13 | * @LateInit 14 | * @Lazy 15 | * @LazyConstant 16 | * @Trimmed 17 | * @UndoRedo 18 | * @UserDefault 19 | * More coming ... 20 | DESC 21 | 22 | s.homepage = 'https://github.com/guillermomuntaner/Burritos' 23 | s.license = { :type => 'MIT', :file => 'LICENSE' } 24 | s.author = { 'Guillermo Muntaner' => 'guillermomp87@gmail.com' } 25 | s.source = { :git => 'https://github.com/guillermomuntaner/Burritos.git', :tag => s.version.to_s } 26 | s.social_media_url = 'https://twitter.com/guillermomp87' 27 | 28 | s.ios.deployment_target = '8.0' 29 | s.osx.deployment_target = '10.10' 30 | s.tvos.deployment_target = '9.0' 31 | s.watchos.deployment_target = '2.0' 32 | 33 | s.swift_versions = ['5.0', '5.1'] 34 | 35 | ## @AtomicWrite 36 | s.subspec 'AtomicWrite' do |sp| 37 | sp.source_files = 'Sources/AtomicWrite/*' 38 | sp.framework = 'Foundation' 39 | end 40 | 41 | ## @Clamping 42 | s.subspec 'Clamping' do |sp| 43 | sp.source_files = 'Sources/Clamping/*' 44 | end 45 | 46 | ## @Copying 47 | s.subspec 'Copying' do |sp| 48 | sp.source_files = 'Sources/Copying/*' 49 | sp.framework = 'Foundation' 50 | end 51 | 52 | ## @DefaultValue 53 | s.subspec 'DefaultValue' do |sp| 54 | sp.source_files = 'Sources/DefaultValue/*' 55 | end 56 | 57 | ## @Copying 58 | s.subspec 'DynamicUIColor' do |sp| 59 | sp.source_files = 'Sources/DynamicUIColor/*' 60 | sp.ios.framework = 'UIKit' 61 | end 62 | 63 | ## @EnvironmentVariable 64 | s.subspec 'EnvironmentVariable' do |sp| 65 | sp.source_files = 'Sources/EnvironmentVariable/*' 66 | sp.ios.framework = 'Foundation' 67 | end 68 | 69 | ## @Expirable 70 | s.subspec 'Expirable' do |sp| 71 | sp.source_files = 'Sources/Expirable/*' 72 | sp.framework = 'Foundation' 73 | end 74 | 75 | ## @LateInit 76 | s.subspec 'LateInit' do |sp| 77 | sp.source_files = 'Sources/LateInit/*' 78 | end 79 | 80 | ## @Lazy 81 | s.subspec 'Lazy' do |sp| 82 | sp.source_files = 'Sources/Lazy/*' 83 | end 84 | 85 | ## @LazyConstant 86 | s.subspec 'LazyConstant' do |sp| 87 | sp.source_files = 'Sources/LazyConstant/*' 88 | end 89 | 90 | ## @Trimmed 91 | s.subspec 'Trimmed' do |sp| 92 | sp.source_files = 'Sources/Trimmed/*' 93 | sp.framework = 'Foundation' 94 | end 95 | 96 | ## @UndoRedo 97 | s.subspec 'UndoRedo' do |sp| 98 | sp.source_files = 'Sources/UndoRedo/*' 99 | sp.framework = 'Foundation' 100 | end 101 | 102 | ## @UserDefault 103 | s.subspec 'UserDefault' do |sp| 104 | sp.source_files = 'Sources/UserDefault/*' 105 | sp.framework = 'Foundation' 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Burritos", 8 | platforms: [ 9 | .macOS(.v10_10), 10 | .iOS(.v8), 11 | .tvOS(.v9), 12 | .watchOS(.v2) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 16 | .library( 17 | name: "Burritos", 18 | targets: [ 19 | "AtomicWrite", 20 | "Clamping", 21 | "Copying", 22 | "DefaultValue", 23 | "DynamicUIColor", 24 | "EnvironmentVariable", 25 | "Expirable", 26 | "LateInit", 27 | "Lazy", 28 | "LazyConstant", 29 | "Trimmed", 30 | "UndoRedo", 31 | "UserDefault", 32 | ]), 33 | ], 34 | dependencies: [], // No dependencies 35 | targets: [ 36 | // Template to add a new propert wrapped called {Wrap}: 37 | // .target(name: "{Wrap}", dependencies: []), 38 | // .testTarget(name: "{Wrap}Tests", dependencies: ["{Wrap}"]), 39 | // 40 | // Please add the target in alphabetical order. 41 | // Also add "{Wrap}" to the products library targets list. 42 | .target(name: "AtomicWrite", dependencies: []), 43 | .testTarget(name: "AtomicWriteTests", dependencies: ["AtomicWrite"]), 44 | .target(name: "Clamping", dependencies: []), 45 | .testTarget(name: "ClampingTests", dependencies: ["Clamping"]), 46 | .target(name: "Copying", dependencies: []), 47 | .testTarget(name: "CopyingTests", dependencies: ["Copying"]), 48 | .target(name: "DefaultValue", dependencies: []), 49 | .testTarget(name: "DefaultValueTests", dependencies: ["DefaultValue"]), 50 | .target(name: "DynamicUIColor", dependencies: []), 51 | .testTarget(name: "DynamicUIColorTests", dependencies: ["DynamicUIColor"]), 52 | .target(name: "EnvironmentVariable", dependencies: []), 53 | .testTarget(name: "EnvironmentVariableTests", dependencies: ["EnvironmentVariable"]), 54 | .target(name: "Expirable", dependencies: []), 55 | .testTarget(name: "ExpirableTests", dependencies: ["Expirable"]), 56 | .target(name: "LateInit", dependencies: []), 57 | .testTarget(name: "LateInitTests", dependencies: ["LateInit"]), 58 | .target(name: "Lazy", dependencies: []), 59 | .target(name: "LazyConstant", dependencies: []), 60 | .testTarget(name: "LazyTests", dependencies: ["Lazy", "LazyConstant"]), 61 | .target(name: "Trimmed", dependencies: []), 62 | .testTarget(name: "TrimmedTests", dependencies: ["Trimmed"]), 63 | .target(name: "UndoRedo", dependencies: []), 64 | .testTarget(name: "UndoRedoTests", dependencies: ["UndoRedo"]), 65 | .target(name: "UserDefault", dependencies: []), 66 | .testTarget(name: "UserDefaultTests", dependencies: ["UserDefault"]), 67 | ], 68 | swiftLanguageVersions: [.v5] 69 | ) 70 | -------------------------------------------------------------------------------- /Sources/DynamicUIColor/DynamicUIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicUIColor.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 19/06/2019. 6 | // Original credit to @bardonadam 7 | // 8 | 9 | #if canImport(UIKit) 10 | 11 | import UIKit 12 | 13 | /// A property wrapper arround UIColor to support dark mode. 14 | /// 15 | /// By default in iOS >= 13 it uses the new system wide user interface style trait and dynamic 16 | /// UIColor constructor to support dark mode without any extra effort. 17 | /// On prior iOS versions it defaults to light. 18 | /// ``` 19 | /// @DynamicUIColor(light: .white, dark: .black) 20 | /// var backgroundColor: UIColor 21 | /// 22 | /// // The color will automatically update when traits change 23 | /// view.backgroundColor = backgroundColor 24 | /// ``` 25 | /// 26 | /// To support older iOS versions and custom logics (e.g. a switch in your app settings) the 27 | /// constructor can take an extra `style` closure that dynamically dictates which 28 | /// color to use. Returning a `nil` value results in the prior default behaviour. This logic 29 | /// allows easier backwards compatiblity by doing: 30 | /// ``` 31 | /// let color = DynamicUIColor(light: .white, dark: .black) { 32 | /// if #available(iOS 13.0, *) { return nil } 33 | /// else { return Settings.isDarkMode ? .dark : .light } 34 | /// } 35 | /// 36 | /// view.backgroundColor = color.value 37 | /// 38 | /// // On iOS <13 you might need to manually observe your custom dark 39 | /// // mode settings & re-bind your colors on changes: 40 | /// if #available(iOS 13.0, *) {} else { 41 | /// Settings.onDarkModeChange { [weak self] in 42 | /// self?.view.backgroundColor = self?.color.value 43 | /// } 44 | /// } 45 | /// ``` 46 | /// 47 | /// [Courtesy of @bardonadam](https://twitter.com/bardonadam) 48 | @propertyWrapper 49 | public struct DynamicUIColor { 50 | 51 | /// Backwards compatible wrapper arround UIUserInterfaceStyle 52 | public enum Style { 53 | case light, dark 54 | } 55 | 56 | let light: UIColor 57 | let dark: UIColor 58 | let styleProvider: () -> Style? 59 | 60 | public init( 61 | light: UIColor, 62 | dark: UIColor, 63 | style: @autoclosure @escaping () -> Style? = nil 64 | ) { 65 | self.light = light 66 | self.dark = dark 67 | self.styleProvider = style 68 | } 69 | 70 | public var wrappedValue: UIColor { 71 | switch styleProvider() { 72 | case .dark: return dark 73 | case .light: return light 74 | case .none: 75 | // UIColor(dynamicProvider:) only available on iOS >=13+ & tvOS >=13 76 | #if os(iOS) || os(tvOS) 77 | if #available(iOS 13.0, tvOS 13.0, *) { 78 | return UIColor { traitCollection -> UIColor in 79 | switch traitCollection.userInterfaceStyle { 80 | case .dark: return self.dark 81 | case .light, .unspecified: return self.light 82 | @unknown default: return self.light 83 | } 84 | } 85 | } else { 86 | return light 87 | } 88 | #else 89 | return light 90 | #endif 91 | } 92 | } 93 | } 94 | 95 | #endif 96 | -------------------------------------------------------------------------------- /Sources/Expirable/Expirable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 23/06/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A property wrapper arround a value that can expire. 11 | /// 12 | /// Getting the value after given duration or expiration date will return nil. 13 | /// 14 | /// Usage: 15 | /// ``` 16 | /// @Expirable(duration: 60) 17 | /// var apiToken: String? 18 | /// 19 | /// // New values will be valid for 60s 20 | /// apiToken = "123456abcd" 21 | /// print(apiToken) // "123456abcd" 22 | /// sleep(61) 23 | /// print(apiToken) // nil 24 | /// 25 | /// // You can also construct an expirable with an initial value and expiration date: 26 | /// @Expirable(wrappedValue: "zyx987", expirationDate: date, duration: 60) 27 | /// var apiToken: String? 28 | /// // or just update an existing one: 29 | /// _apiToken.set("zyx987", expirationDate: date) 30 | /// ``` 31 | /// 32 | /// [Courtesy of @v_pradeilles](https://twitter.com/v_pradeilles) 33 | @propertyWrapper 34 | public struct Expirable { 35 | 36 | let duration: TimeInterval 37 | 38 | /// Stores a value toguether with its expiration date. 39 | var storage: (value: Value, expirationDate: Date)? 40 | 41 | /// Instantiate the wrapper with no initial value. 42 | public init(duration: TimeInterval) { 43 | self.duration = duration 44 | storage = nil 45 | } 46 | 47 | /// Instantiate the wrapper with an initial value and its expiration date, toguether with a duration. 48 | /// 49 | /// This method is meant to be used when a value is restored from some form of persistent storage and the expiration 50 | /// is well known and doesn't depend on the current date. It is perfectly fine to pass an expiration date in the past; the 51 | /// wrapper will simply treat the initial value as expired inmediatly. 52 | /// 53 | /// The duration will be ignored for this initial value but will be used as soon as a new value is set. 54 | public init(wrappedValue: Value, expirationDate: Date, duration: TimeInterval) { 55 | self.duration = duration 56 | storage = (wrappedValue, expirationDate) 57 | } 58 | 59 | public var wrappedValue: Value? { 60 | get { 61 | isValid ? storage?.value : nil 62 | } 63 | set { 64 | storage = newValue.map { newValue in 65 | let expirationDate = Date().addingTimeInterval(duration) 66 | return (newValue, expirationDate) 67 | } 68 | } 69 | } 70 | 71 | /// A Boolean value that indicates whether the expirable value is still valid or has expired. 72 | public var isValid: Bool { 73 | guard let storage = storage else { return false } 74 | return storage.expirationDate >= Date() 75 | } 76 | 77 | /// Set a new value toguether with its expiration date. 78 | /// 79 | /// By calling this method the duration set while constructing the property wrapper will be ignored for this concrete new value. 80 | /// Setting a new value without using this method will revert back to use the duration to compute the expiration date. 81 | public mutating func set(_ newValue: Value, expirationDate: Date) { 82 | storage = (newValue, expirationDate) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/UndoRedoTests/UndoRedoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UndoRedoTests.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 23/06/2019. 6 | // 7 | 8 | import XCTest 9 | @testable import UndoRedo 10 | 11 | final class UndoRedoTests: XCTestCase { 12 | 13 | @UndoRedo var text = "Hello, World!" 14 | 15 | override func setUp() { 16 | _text = UndoRedo(wrappedValue: "Hello, World!") 17 | } 18 | 19 | func testGet() { 20 | XCTAssertEqual(text, "Hello, World!") 21 | } 22 | 23 | func testSet() { 24 | text = "Hello" 25 | XCTAssertEqual(text, "Hello") 26 | } 27 | 28 | // MARK: Undo 29 | 30 | func testCanUndo() { 31 | text = "Hello" 32 | XCTAssertTrue(_text.canUndo) 33 | XCTAssertEqual(text, "Hello") 34 | } 35 | 36 | func testUndo() { 37 | text = "Hello" 38 | XCTAssertTrue(_text.undo()) 39 | XCTAssertEqual(text, "Hello, World!") 40 | } 41 | 42 | func testCannotUndoFirstValue() { 43 | text = "Hello" 44 | _text.undo() 45 | XCTAssertEqual(text, "Hello, World!") 46 | XCTAssertFalse(_text.canUndo) 47 | XCTAssertFalse(_text.undo()) 48 | XCTAssertEqual(text, "Hello, World!") 49 | } 50 | 51 | // MARK: Redo 52 | 53 | func testCanRedo() { 54 | text = "Hello" 55 | _text.undo() 56 | XCTAssertTrue(_text.canRedo) 57 | XCTAssertEqual(text, "Hello, World!") 58 | } 59 | 60 | func testRedo() { 61 | text = "Hello" 62 | _text.undo() 63 | XCTAssertEqual(text, "Hello, World!") 64 | XCTAssertTrue(_text.redo()) 65 | XCTAssertEqual(text, "Hello") 66 | } 67 | 68 | func testCannotRedoLastValue() { 69 | text = "Hello" 70 | text = "Hello world" 71 | XCTAssertFalse(_text.canRedo) 72 | XCTAssertFalse(_text.redo()) 73 | } 74 | 75 | func testCannotRedoAfterSettingValue() { 76 | text = "Hello" 77 | text = "Hello world" 78 | _text.undo() // text == "Hello" 79 | 80 | XCTAssertTrue(_text.canRedo) 81 | 82 | text = "Hello world!" 83 | 84 | XCTAssertFalse(_text.canRedo) 85 | XCTAssertFalse(_text.redo()) 86 | } 87 | 88 | // MARK: Others 89 | 90 | func testClearHistory() { 91 | text = "Hello, World" 92 | text = "Hello" 93 | text = "Hello world" 94 | text = "Hello world!" 95 | _text.undo() // text == "Hello world" 96 | _text.undo() // text == "Hello" 97 | _text.undo() // text == "Hello, World" 98 | _text.redo() // text == "Hello" 99 | 100 | XCTAssertEqual(text, "Hello") 101 | XCTAssertTrue(_text.canUndo) 102 | XCTAssertTrue(_text.canRedo) 103 | 104 | _text.cleanHistory() 105 | 106 | XCTAssertEqual(text, "Hello") 107 | XCTAssertFalse(_text.canUndo) 108 | XCTAssertFalse(_text.canRedo) 109 | } 110 | 111 | static var allTests = [ 112 | ("testGet", testGet), 113 | ("testSet", testSet), 114 | // Undo 115 | ("testCanUndo", testCanUndo), 116 | ("testUndo", testUndo), 117 | ("testCannotUndoFirstValue", testCannotUndoFirstValue), 118 | // Redo 119 | ("testCanRedo", testCanRedo), 120 | ("testRedo", testRedo), 121 | ("testCannotRedoLastValue", testCannotRedoLastValue), 122 | ("testCannotRedoAfterSettingValue", testCannotRedoAfterSettingValue), 123 | // Others 124 | ("testClearHistory", testClearHistory), 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /Sources/UndoRedo/UndoRedo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UndoRedo.swift 3 | // 4 | // 5 | // Created by Guillermo Muntaner Perelló on 23/06/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// A property wrapper that automatically stores history and supports undo and redo operations. 12 | /// 13 | /// Usage: 14 | /// ``` 15 | /// @UndoRedo var text = "" 16 | /// 17 | /// text = "Hello" 18 | /// text = "Hello, World!" 19 | /// 20 | /// _text.canUndo // true 21 | /// _text.undo() // text == "Hello" 22 | /// 23 | /// _text.canRedo // true 24 | /// _text.redo() // text == "Hello, World!" 25 | /// ``` 26 | /// 27 | /// You can check at any time if there is an undo or a redo stack using `canUndo` & `canRedo` 28 | /// properties, which might be particularly usefull to enable/disable user interface buttons. 29 | /// 30 | /// - Note: This property holds strong references/stores the full history of the wrapped property 31 | /// which can end up consuming lots of memory. We provide a `cleanHistory()` method you 32 | /// can call whenever you want to release resources and just keep the current value. 33 | /// 34 | /// [Original idea by @JeffHurray](https://twitter.com/JeffHurray/status/1137816198689673216) 35 | /// Ideas for API on [Foundation UndoManager](https://developer.apple.com/documentation/foundation/undomanager) 36 | /// [Chris Eidhof blog post](http://chris.eidhof.nl/post/undo-history-in-swift/) 37 | @propertyWrapper 38 | public struct UndoRedo { 39 | 40 | var index: Int 41 | var values: [Value] 42 | 43 | public init(wrappedValue: Value) { 44 | self.values = [wrappedValue] 45 | self.index = 0 46 | } 47 | 48 | public var wrappedValue: Value { 49 | get { 50 | values[index] 51 | } 52 | set { 53 | // Inserting a new value drops any existing redo stack. 54 | if canRedo { 55 | values = Array(values.prefix(through: index)) 56 | } 57 | values.append(newValue) 58 | index += 1 59 | } 60 | } 61 | 62 | // MARK: Wrapper public API 63 | 64 | /// A Boolean value that indicates whether the receiver has any actions to undo. 65 | public var canUndo: Bool { 66 | return index > 0 67 | } 68 | 69 | /// A Boolean value that indicates whether the receiver has any actions to redo. 70 | public var canRedo: Bool { 71 | return index < (values.endIndex - 1) 72 | } 73 | 74 | /// If there are previous values it replaces the current value with the previous one and returns true, otherwise returns false. 75 | @discardableResult 76 | public mutating func undo() -> Bool { 77 | guard canUndo else { return false } 78 | index -= 1 79 | return true 80 | } 81 | 82 | /// It reverts the last `undo()` call and returns true if any, otherwise returns false. 83 | /// Whenever a new value is assigned to the wrapped property any existing "redo stack" is dropped. 84 | @discardableResult 85 | public mutating func redo() -> Bool { 86 | guard canRedo else { return false } 87 | index += 1 88 | return true 89 | } 90 | 91 | /// Cleans both the undo and redo history leaving only the current value. 92 | public mutating func cleanHistory() { 93 | values = [values[index]] 94 | index = 0 95 | } 96 | 97 | // TODO: A way to implement this just storing diffs? 98 | // It might not be particularly usefull since it will require Value to conform to some sort of 99 | // diffable protocol. 100 | // Maybe I could implement one version just for table/collection view data sources. 101 | 102 | // TODO: Add support for limited-size history; e.g. 50 103 | 104 | // TODO: Potential alternative version supporting foundation undomanager https://developer.apple.com/documentation/foundation/undomanager 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌯🌯 Burritos 2 | 3 | [![Bitrise Build Status](https://img.shields.io/bitrise/82996dff101ee00e.svg?token=C1i6-qS1Bfhz1QvbJPV7GA)](https://app.bitrise.io/app/82996dff101ee00e) 4 | [![Swift Package Manager](https://img.shields.io/badge/swift%20package%20manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 5 | [![Platform](https://img.shields.io/cocoapods/p/Burritos.svg)](https://github.com/guillermomuntaner/Burritos) 6 | 7 | 8 | A collection of well tested Swift Property Wrappers. 9 | 10 | - [@AtomicWrite](#AtomicWrite) 11 | - [@Clamping](#Clamping) 12 | - [@Copying](#Copying) 13 | - [@DefaultValue](#DefaultValue) 14 | - [@DynamicUIColor](#DynamicUIColor) 15 | - [@EnvironmentVariable](#EnvironmentVariable) 16 | - [@Expirable](#Expirable) 17 | - [@LateInit](#LateInit) 18 | - [@Lazy](#Lazy) 19 | - [@LazyConstant](#LazyConstant) 20 | - [@Trimmed](#Trimmed) 21 | - [@UndoRedo](#UndoRedo) 22 | - [@UserDefault](#UserDefault) 23 | - More coming ... 24 | 25 | ## Requirements 26 | Xcode 11 & Swift 5 27 | 28 | ## Installation 29 | 30 | ### Swift Package Manager 31 | 32 | #### Xcode 11+ integration 33 | 1. Open `MenuBar` → `File` → `Swift Packages` → `Add Package Dependency...` 34 | 2. Paste the package repository url `https://github.com/guillermomuntaner/Burritos` and hit Next. 35 | 3. Select your rules. Since this package is in pre-release development, I suggest you specify a concrete tag to avoid pulling breaking changes. 36 | 37 | 38 | #### Package.swift 39 | If you already have a Package.swift or you are building your own package simply add a new dependency: 40 | ```swift 41 | dependencies: [ 42 | .package(url: "https://github.com/guillermomuntaner/Burritos", from: "0.0.3") 43 | ] 44 | ``` 45 | 46 | 47 | ### Cocoapods 48 | 49 | Add Burritos to your Podfile: 50 | ```rb 51 | pod 'Burritos', '~> 0.0.3' 52 | ``` 53 | 54 | Each wrapper is a submodule, so you add just the one(s) you want 55 | ```rb 56 | pod 'Burritos/Copying', '~> 0.0.3' 57 | pod 'Burritos/UndoRedo', '~> 0.0.3' 58 | pod 'Burritos/UserDefault', '~> 0.0.3' 59 | ``` 60 | 61 | 62 | ## @AtomicWrite 63 | 64 | A property wrapper granting atomic write access to the wrapped property. 65 | Reading access is not atomic but is exclusive with write & mutate operations. 66 | Atomic mutation (read-modify-write) can be done using the wrapper `mutate` method. 67 | 68 | ```swift 69 | @Atomic var count = 0 70 | 71 | // You can atomically write (non-derived) values directly: 72 | count = 99 73 | 74 | // To mutate (read-modify-write) always use the wrapper method: 75 | DispatchQueue.concurrentPerform(iterations: 1000) { index in 76 | _count.mutate { $0 += 1 } 77 | } 78 | 79 | print(count) // 1099 80 | ``` 81 | 82 | ## @Clamping 83 | 84 | A property wrapper that automatically clamps its wrapped value in a range. 85 | 86 | ```swift 87 | @Clamping(range: 0...1) 88 | var alpha: Double = 0.0 89 | 90 | alpha = 2.5 91 | print(alpha) // 1.0 92 | 93 | alpha = -1.0 94 | print(alpha) // 0.0 95 | ``` 96 | 97 | 98 | ## @Copying 99 | 100 | A property wrapper arround `NSCopying` that copies the value both on initialization and reassignment. 101 | If you are tired of calling `.copy() as! X` you will love this one. 102 | 103 | ```swift 104 | @Copying var path: UIBezierPath = .someInitialValue 105 | 106 | public func updatePath(_ path: UIBezierPath) { 107 | self.path = path 108 | // You don't need to worry whoever called this method mutates the passed by reference path. 109 | // Your stored self.path contains a copy. 110 | } 111 | ``` 112 | 113 | 114 | ## @DefaultValue 115 | 116 | A property wrapper arround an implicitly unwrapped optional value which fallbacks to a given default value. 117 | 118 | ```swift 119 | @DefaultValue(default: 0) 120 | var count 121 | count = 100 122 | // or 123 | @DefaultValue(default: 0) 124 | var count = 100 125 | 126 | // Assigning nil resets to the default value 127 | print(count) // 100 128 | count = nil 129 | print(count) // 0 130 | ``` 131 | 132 | ## @DynamicUIColor 133 | 134 | A property wrapper arround UIColor to support dark mode. 135 | 136 | By default in iOS >= 13 it uses the new system wide user interface style trait and dynamic UIColor constructor to support dark mode without any extra effort. On prior iOS versions it defaults to light. 137 | ```swift 138 | @DynamicUIColor(light: .white, dark: .black) 139 | var backgroundColor: UIColor 140 | 141 | // The color will automatically update when traits change 142 | view.backgroundColor = backgroundColor 143 | ``` 144 | 145 | To support older iOS versions and custom logics (e.g. a switch in your app settings) the constructor can take an extra `style` closure that dynamically dictates which color to use. Returning a `nil` value results in the prior default behaviour. This logic allows easier backwards compatiblity by doing: 146 | 147 | ```swift 148 | let color = DynamicUIColor(light: .white, dark: .black) { 149 | if #available(iOS 13.0, *) { return nil } 150 | else { return Settings.isDarkMode ? .dark : .light } 151 | } 152 | 153 | view.backgroundColor = color.value 154 | 155 | // On iOS <13 you might need to manually observe your custom dark 156 | // mode settings & re-bind your colors on changes: 157 | if #available(iOS 13.0, *) {} else { 158 | Settings.onDarkModeChange { [weak self] in 159 | self?.view.backgroundColor = self?.color.value 160 | } 161 | } 162 | ``` 163 | 164 | Original idea courtesy of [@bardonadam](https://twitter.com/bardonadam) 165 | 166 | ## @EnvironmentVariable 167 | 168 | A property wrapper to set and get system environment variables values. 169 | 170 | ```swift 171 | @EnvironmentVariable(name: "PATH") 172 | var path: String? 173 | 174 | // You can set the environment variable directly: 175 | path = "~/opt/bin:" + path! 176 | 177 | ``` 178 | 179 | ## @Expirable 180 | 181 | A property wrapper arround a value that can expire. Getting the value after given duration or expiration date will return nil. 182 | 183 | ```swift 184 | @Expirable(duration: 60) 185 | var apiToken: String? 186 | 187 | // New values will be valid for 60s 188 | apiToken = "123456abcd" 189 | print(apiToken) // "123456abcd" 190 | sleep(61) 191 | print(apiToken) // nil 192 | 193 | // You can also construct an expirable with an initial value and expiration date: 194 | @Expirable(wrappedValue: "zyx987", expirationDate: date, duration: 60) 195 | var apiToken: String? 196 | // or just update an existing one: 197 | _apiToken.set("zyx987", expirationDate: date) 198 | ``` 199 | 200 | [Courtesy of @v_pradeilles](https://twitter.com/v_pradeilles) 201 | 202 | 203 | ## @LateInit 204 | 205 | A reimplementation of Swift Implicitly Unwrapped Optional using a property wrapper. 206 | 207 | ```swift 208 | var text: String! 209 | // or 210 | @LateInit var text: String 211 | 212 | // Note: Accessing it before initializing will result in a fatal error: 213 | // print(text) // -> fatalError("Trying to access LateInit.value before setting it.") 214 | 215 | // Later in your code: 216 | text = "Hello, World!" 217 | ``` 218 | 219 | 220 | ## @Lazy 221 | 222 | A property wrapper which delays instantiation until first read access. 223 | It is a reimplementation of Swift `lazy` modifier using a property wrapper. 224 | 225 | ```swift 226 | @Lazy var result = expensiveOperation() 227 | ... 228 | print(result) // expensiveOperation() is executed at this point 229 | ``` 230 | 231 | As an extra on top of `lazy` it offers reseting the wrapper to its "uninitialized" state. 232 | 233 | 234 | ## @LazyConstant 235 | 236 | Same as [@Lazy](#Lazy) + prevents changing or mutating its wrapped value. 237 | 238 | ```swift 239 | @LazyConstant var result = expensiveOperation() 240 | ... 241 | print(result) // expensiveOperation() is executed at this point 242 | 243 | result = newResult // Compiler error 244 | ``` 245 | 246 | **Note**: This wrapper prevents reassigning the wrapped property value but **NOT** the wrapper itself. Reassigning the wrapper `_value = LazyConstant(wrappedValue: "Hola!")` is possible and since wrappers themselves need to be declared variable there is no way to prevent it. 247 | 248 | 249 | ## @Trimmed 250 | 251 | A wrapper that automatically trims strings both on initialization and reassignment. 252 | 253 | ```swift 254 | @Trimmed 255 | var text = " \n Hello, World! \n\n " 256 | 257 | print(text) // "Hello, World!" 258 | 259 | // By default trims white spaces and new lines, but it also supports any character set 260 | @Trimmed(characterSet: .whitespaces) 261 | var text = " \n Hello, World! \n\n " 262 | print(text) // "\n Hello, World! \n\n" 263 | ``` 264 | 265 | 266 | ## @UndoRedo 267 | 268 | A property wrapper that automatically stores history and supports undo and redo operations. 269 | 270 | ```swift 271 | @UndoRedo var text = "" 272 | 273 | text = "Hello" 274 | text = "Hello, World!" 275 | 276 | _text.canUndo // true 277 | _text.undo() // text == "Hello" 278 | 279 | _text.canRedo // true 280 | _text.redo() // text == "Hello, World!" 281 | ``` 282 | 283 | You can check at any time if there is an undo or a redo stack using `canUndo` & `canRedo` 284 | properties, which might be particularly usefull to enable/disable user interface buttons. 285 | 286 | Original idea by [@JeffHurray](https://twitter.com/JeffHurray/status/1137816198689673216) 287 | 288 | 289 | ## @UserDefault 290 | 291 | Type safe access to `UserDefaults` with support for default values. 292 | ```swift 293 | @UserDefault("test", defaultValue: "Hello, World!") 294 | var test: String 295 | ``` 296 | 297 | By default it uses the standard user defauls. You can pass any other instance of `UserDefaults` you want to use via its constructor, e.g. when you use app groups: 298 | 299 | ```swift 300 | let userDefaults = UserDefaults(suiteName: "your.app.group") 301 | @UserDefault("test", defaultValue: "Hello, World!", userDefaults: userDefaults) 302 | var test: String 303 | ``` 304 | 305 | ## @Cached 306 | TODO 307 | 308 | ## @Dependency (Service locator pattern) 309 | TODO 310 | 311 | ## Thread safety 312 | TODO 313 | 314 | ## Command line parameters 315 | TODO 316 | 317 | 318 | ## Property observer -> willSet, didSet ! 319 | TODO: Reimplement 320 | 321 | ## Print/Log 322 | TODO: A property wrapper that prints/logs any value set. 323 | 324 | ## About Property Wrappers 325 | 326 | Quoting the [Property Wrappers Proposal](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) description: 327 | > A property wrapper is a mechanism to abstract property implementation patterns that come up repeatedly. 328 | 329 | 👉 Did you know: Property Wrappers were announced by Apple during WWDC 2019. 330 | They are a fundamental component in SwiftUI syntax sugar hence Apple pushed them into the initial Swift 5.1 beta, skipping the normal Swift Evolution process. 331 | This process continued after WWDC and it took 3 reviews to reach their final form on Xcode 11 beta 4. 332 | 333 | Interesting reads: 334 | * [Swift Evolution Property Wrappers Proposal](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) 335 | * [SwiftLee: Property wrappers to remove boilerplate code in Swift](https://www.avanderlee.com/swift/property-wrappers/) 336 | * [Majid's: Understanding Property Wrappers in SwiftUI](https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/) 337 | * [Swift by Sundell: The Swift 5.1 features that power SwiftUI’s API](https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api) 338 | * [NSHipster article](https://nshipster.com/propertywrapper/) 339 | 340 | 341 | Equivalents in other languages: 342 | * Kotlin has [Delegated Properties](https://kotlinlang.org/docs/reference/delegated-properties.html) 343 | 344 | 345 | ## License 346 | 347 | Burritos is released under the [MIT license](https://github.com/guillermomuntaner/Burritos/blob/master/LICENSE). 348 | --------------------------------------------------------------------------------