// // PropertyWrappers.swift // Unilib // // Created by David James on 2019-10-01. // Copyright © 2019-2021 David B James. All rights reserved. // import Foundation /// Property that is guaranteed to never be less than 0 /// with a projected boolean value to indicate when /// the value is greater than 0. @propertyWrapper public struct MinZero { private var number = 0 public var projectedValue = false public var wrappedValue:Int { get { return number } set { if newValue < 0 { number = 0 } else { number = newValue } projectedValue = number > 0 } } public init(wrappedValue:Int) { // NOTE: This specific initializer is necessary in case // you must create this property with an initial value. // Example: @MinZero var test = 3 // On the other hand, to create this property with any // other value you must create another initializer for // that value. Example: @Min(min:2) var test. // To combine both a custom initial value and the initial // value of the wrapped value, you must create yet another // initializer that does both, with wrappedValue first: // init(wrappedValue:Int, min:Int) and call it like // @Min(min:2) var test = 5 // which synthesizes to: Min(wrappedValue:5, min:2). number = wrappedValue } } // DEV NOTE: @Cycled may only be used in a SwiftUI context // because SwiftUI manages its state (with updates) within a View. // To get the same functionality (but without updates) use @CycledRaw. // This has been tested on a UIViewController. // @Cycled does not work there, @CycledRaw does (but without updates). // MAINTENANCE: Sync the two blocks with and without SwiftUI #if canImport(SwiftUI) import SwiftUI /// Property that cycles through numbers /// resetting to 0 when `max` is reached. /// /// Alternatively, pass `min` to reset to /// a number other than 0 and/or decrement /// the number instead of incrementing. /// You may also pass an initial starting /// number from where to begin in the cycle. /// /// @Cycled(min:1, max:5) var counter:Int = 2 /// .. /// counter = 1 // increment /// counter = -1 // decrement /// Optionally, make the cycling `reversible` /// in either direction, so instead of starting /// again, it reverses when it reaches min/max. /// Alternatively, make this `finite` so that /// it stops when its bound is reached. @propertyWrapper public struct Cycled : DynamicProperty { /// Type of cycling behavior public enum Mode { /// Cycle numbers from beginning and when /// reaching the end, start at the beginning. /// Default cycling behavior. case repeatable /// Cycle numbers from beginning and when /// reaching the end, go back to the beginning /// and vice versa. case reversible /// Cycle numbers from beginning and when /// reaching the end, stop. case finite } private let min:T private let max:T private let mode:Mode private var reversible:Bool { mode == .reversible } private var finite:Bool { mode == .finite } @State private var number:T = 0 @State private var shouldReverse:Bool = false public var wrappedValue:T { get { return number } nonmutating set { guard newValue != T.zero else { return } let value = shouldReverse ? -newValue : newValue let newNumber = number + value if value < T.zero { // decrementing if newNumber < min { if reversible || finite { // Past min value, clamp to min. // It will either start going back the opposite way // for "reversible" or just stay there for "finite". number = min } else { // Past min, start again at max. "repeatable" number = max } } else { number = newNumber } } else { // incrementing if newNumber > max { if reversible || finite { number = max } else { number = min } } else { number = newNumber } } if reversible && (number == min || number == max) { shouldReverse.toggle() } } } public var projectedValue:Binding { Binding( get: { wrappedValue }, set: { wrappedValue = $0 } ) } public init(min:T = T.zero, max:T, mode:Mode? = nil) { guard min < max else { preconditionFailure("Attempt to initialize @Cycled property wrapper with min value that is greater than or equal to max value.") } self.min = min self.max = max self.mode = mode ?? .repeatable self._number = State(wrappedValue:min) } public init(wrappedValue startingAt:T, min:T = T.zero, max:T, mode:Mode? = nil) { guard min < max else { preconditionFailure("Attempt to initialize @Cycled property wrapper with min value that is greater than or equal to max value.") } guard startingAt >= min && startingAt <= max else { preconditionFailure("Attempt to initialize @Cycled property wrapper with initial start value (\(startingAt)) that is not within min (\(min)) and max (\(max)) values inclusive.") } self.min = min self.max = max self.mode = mode ?? .repeatable self._number = State(wrappedValue:startingAt) } public var atMin:Bool { wrappedValue <= min } public var atMax:Bool { wrappedValue >= max } // Couldn't get this to work.. would just hit Int += // Wanted syntax like: counter += 1 // public static func +=(cycled:inout Self, value:Int) { // cycled.wrappedValue = value // } } #endif /// Property that cycles through numbers /// resetting to 0 when `max` is reached. /// /// Alternatively, pass `min` to reset to /// a number other than 0 and/or decrement /// the number instead of incrementing. /// You may also pass an initial starting /// number from where to begin in the cycle. /// /// @CycledRaw(min:1, max:5) var counter:Int = 2 /// .. /// counter = 1 // increment /// counter = -1 // decrement /// Optionally, make the cycling `reversible` /// in either direction, so instead of starting /// again, it reverses when it reaches min/max. /// Alternatively, make this `finite` so that /// it stops when its bound is reached. /// See also @Cycled if this is used in SwiftUI. @propertyWrapper public struct CycledRaw { public enum Mode { case repeatable, reversible, finite } private let min:T private let max:T private let mode:Mode private var reversible:Bool { mode == .reversible } private var finite:Bool { mode == .finite } private var shouldReverse:Bool = false private var number:T public var wrappedValue:T { get { return number } set { guard newValue != T.zero else { return } let value = shouldReverse ? -newValue : newValue let newNumber = number + value if newValue < T.zero { // decrementing if newNumber < min { if reversible || finite { number = min } else { number = max } } else { number = newNumber } } else { // incrementing if newNumber > max { if reversible || finite { number = max } else { number = min } } else { number = newNumber } } if reversible && (number == min || number == max) { shouldReverse.toggle() } } } public init(min:T = T.zero, max:T, mode:Mode? = nil) { guard min < max else { preconditionFailure("Attempt to initialize @Cycled property wrapper with min value that is greater than or equal to max value.") } self.min = min self.max = max self.mode = mode ?? .repeatable self.number = min } public init(wrappedValue startingAt:T, min:T = T.zero, max:T, mode:Mode? = nil) { guard min < max else { preconditionFailure("Attempt to initialize @Cycled property wrapper with min value that is greater than or equal to max value.") } guard startingAt >= min && startingAt <= max else { preconditionFailure("Attempt to initialize @Cycled property wrapper with initial start value (\(startingAt)) that is not within min (\(min)) and max (\(max)) values inclusive.") } self.min = min self.max = max self.mode = mode ?? .repeatable self.number = startingAt } public var atMin:Bool { wrappedValue <= min } public var atMax:Bool { wrappedValue >= max } } /// Property that stores a closure, initially running /// that closure and storing the return value as the /// projected value of that type. This supports both /// immediate use of the returned "projected" value /// (e.g. "$foo") and the ability to rerun/refresh the /// closure (e.g. "foo()"). The result of refreshing /// may be used in a local variable (keeping the original /// projected value intact) or assigned directly back to /// the projected value, to update it (e.g. "$foo = foo()"). /// Alternatively, assign a new closure to this property /// to update both the refreshable closure and projected value. @propertyWrapper public struct Refreshable { public typealias Closure = ()->T private var closure:Closure public var projectedValue:T public var wrappedValue:Closure { get { return closure } set { closure = newValue projectedValue = self.closure() } } public init(wrappedValue closure:@escaping Closure) { self.closure = closure self.projectedValue = closure() } }