A collection of useful Swift property wrappers to make coding easier.
- 15 wrappers included.
- Most wrappers are fully unit tested.
- Most wrappers can be observed via
Publishers exposed in theirprojectedValues. - PRs welcome.
Here's the list of all the wrappers included in the package.
- Synchronizes property reads and writes using the provided
DispatchQueue. projectedValueexposes the wrapper itself and allows for using itsmutatemethod that change and set the value at the same time.
Sample use:
@Atomic var value: Int = 0
let read = value // synchonized
value = 5 // also synchronized
$value.mutate { value in value *= 2 } // synchronized mutation- Ensures that the property's
Comparablevalue is always in the bounds of the provided range. The range can be specified either with its minimum and maximum value, or by aClosedRange:- If the value set is lesser than lower bound of the range, the actual value set is the lower bound itself.
- If the value set is greater than upper bound of the range, the actual value set is the upper bound itself.
- The initially assigned value is automatically clamped as well.
projectedValueprovides aPublisherthat emits the new value on set.
Sample use:
@Clamped(min: 10, max: 20) var minMax: Int = 10
@Clamped(5...10) var range: Int = 5
minMax = 25
minMax == 20 // clamped to upper bound
range = 1
range == 5 // clamped to lower bound
minMax = 19
minMax == 19 // the value was already within bounds- A string wrapper that exposes a SwiftUI
Colorvia itsprojectedValueif the value represents a valid hex color code. Multiple color formats are supported based on this solution. - If the string value is not a valid hex color string,
projectedValueisnil.
Sample use:
@ColorHex var colorHex = "fff"
$colorHex == Color(white: 1)
colorHex = "FF0000"
$colorHex == Color(red: 1, green: 0, blue: 0)
colorHex = "fail"
$colorHex == nil- Allows for pass-by-copy by ensuring that
copymethod is invoked whenever a value is assigned. - Property type must conform to the
Copyableprotocol.
Sample use:
class CopyableItem: Copyable {
let name: String
let price: Int
init(name: String, price: Int) {
self.name = name
self.price = price
}
func copy() -> Self {
CopyableItem(name: name, price: price) as! Self
}
}
// ...
@CopyOnWrite var item: CopyableItem = CopyableItem(name: "a", price: 0)
let newItem = CopyableItem(name: "test", price: 1)
item = newItem
item !== newItem // not the same reference
item.name == newItem.name // same copied value- Allows for late initialization of properties, thus working around Swift's init safety checks. This can avoid the need for implicitly-unwrapped optionals in multi-phase initialization.
- If the value is read before being written to for the first time, a
Never-returning block is invoked.- This block can be set manually and defaults to
fatalError.
- This block can be set manually and defaults to
projectedValuereturnstrueif the value is already set, so that you can check without triggering a potentially fatal `get.
Sample use:
@Delayed var value: String
$value == false // not set yet
let read = value // fatal error
value = ""
let read = value // works!
$value == true // was set- Property wrapper for a value that "expires" after a set period of time - trying to read the value
expirationPeriodseconds after it was last set will returnnil. - Useful for properties whose underlying data should periodically be refreshed without having to resort to scheduled notifications.
Sample use:
@Expirable(10) var value: Int? // expires 10 seconds after it is set
value == nil // not set yet
value = 10
value == 10 // works
// sleep for 5 seconds...
value == 10 // still there
// sleep for 5 more seconds
value == nil // expired and nulled- Allows for lazy evaluation of properties with the added option of resetting them, so that the lazy initialization block runs again. In other words, if you have a
lazy varthat should, for whatever reason, be re-evaluated on next get, use this property wrapper. - Lazy init block is an
@autoclosure, which means you can omit braces if there's only one expression in it. projectedValueexposes the wrapper itself and allows for using itsresetmethod.
Sample use:
@LazyWithReset(Date().timeIntervalSince1970) var currentTime: TimeInterval
let time = currentTime
let time2 = currentTime
time == time2 // just one lazy evaluation occurred
$currentTime.reset() // re-evaluate on next get
let time3 = currentTime
time3 != time // new lazy evaluation occurred- Same as
LazyWithReset, but the lazy init closure can take an argument of typeReceiver. This is primarily used to allow for usage ofselfin a lazy init block, which is something that Swiftlazy varcan do. receivercan be changed at any time via the projected value:$myProp.receiver = self.
Sample use:
final class BoundLazyWithResetTest: XCTestCase {
var counter = 0
@BoundLazyWithReset<BoundLazyWithResetTest, Int>({ this in
if this.counter > 3 {
return 5
} else {
return 2
}
}) var transformedCounter: Int
func test() throws {
$transformedCounter.receiver = self // HERE
let c1 = transformedCounter
let c2 = transformedCounter
XCTAssertEqual(c1, c2)
$transformedCounter.reset()
counter = 4
let c3 = transformedCounter
XCTAssertNotEqual(c1, c3)
}
}- Allows for direct mapping of localized keys to their string values without using
NSLocalizedString. - Simply assign the key to the property and you'll get the localized string out.
projectedValueprovides a publisher that emits a new value on set.
Sample use:
@Localized var emailTitle = "email-title-key"
// Providing that your Localized.strings contains "email-title-key" = "Email";
Text(emailTitle) // shows Email- Allows for custom blocks of code to be invoked whenever the property is read or written to. The intended use case for this is to log access to the property, but the generic nature of the callbacks makes this wrapper quite versatile.
- By default, read block is
niland write block prints the newly set value. projectedValueprovides a publisher that emits a new value on set.
Sample use:
@Logged var myValue: Int = 10
myValue = 10 // prints 10 in the log
// custom actions on read and write
@Logged(read: { readLog += "Read: \($0)\n" },
write: { writeLog += "Write: \($0)\n" }) var value: Int = 0- Always returns the value specified by the
mockblock. - Mock block is an
@autoclosure, which means you can omit braces if there's only one expression in it. - The most common use case for this is to easily inject temporary mock functionality in a single place without having to modify code anywhere else.
- While assignments don't have effect on the returned value, they are still accessible via `projectedValue.
Sample use:
protocol ItemRepo {
func fetch() -> [Item]
func upsert(item: Item)
}
class RealRepo: ItemRepo {
private var items = Set<Item>()
func fetch() -> [Item] {
Array(items)
}
func upsert(item: Item) {
items.insert(item)
}
}
class MockRepo: ItemRepo {
static let shared = MockRepo()
func fetch() -> [Item] {
[Item(name: "test", price: 1)]
}
func upsert(item: Item) { }
}
@Mocked(MockRepo.shared) var repo: ItemRepo = RealRepo()
// always returns the mocked value
let fetched = repo.fetch()
fetched == [Item(name: "test", price: 1)]
repo.upsert(item: Item(name: "new", price: 2))
let fetchedAgain = repo.fetch()
fetchedAgain == fetched // no change as upsert in mock doesn't do anything
// projected value accesses real value
let fetched = $repo.fetch() // uses actual repo assigned
fetched == []
$repo.upsert(item: Item(name: "new", price: 2))
let fetchedAgain = $repo.fetch()
fetchedAgain == [Item(name: "new", price: 2)]- Ensures that the floating-point value of this property is always rounded to the specified number of decimal places.
- You can also specify the
FloatingPointRoundingRule.
- You can also specify the
projectedValueprovides a publisher that emits a new value whenever it is set.
Sample use:
@Rounded(0) var zero: Float = 1.1
@Rounded(1) var one: Float = 1.15
@Rounded(2) var two: Float = 1.125
@Rounded(2, rule: .down) var twoDown: Float = 1.135
zero == 1
zero = 2.23
zero == 2
one == 1.2
two == 1.12
twoDown == 1.13- Transforms the assigned value using the provided block, allowing for a wide array of applications, from automatically formatting strings, transforming numbers, etc.
projectedValueprovides a publisher that emits a new value whenever it is set.
Sample use:
@Transformed({ -$0 }) var negated: Int = 0
@Transformed({ $0.trimmingCharacters(in: .whitespaces).lowercased() }) var formatted = ""
negated = 5
negated == -5
formatted = " AbCDe "
formatted == "abcde"- Normalizes the assigned value to a value between 0 and 1 based on the provided range. E.g, color components are normally expressed as values between 0 and 255, while iOS requires them to be set as values between 0 and 1.
projectedValueprovides a publisher that emits a new value whenever it is set.
Sample use:
@UnitInterval(0...255) var red: CGFloat = 0
red == 0
red = 255
red == 1
red = 25.5
red == 0.1- Only sets the new value is it passes validation by the provided block, which allows for vetoing new values.
projectedValueprovides a publisher that emits a new value whenever it is set.
Sample use:
// only non-negative values, please
@Validated({ $0 >= 0 }) var value: Int = 0
value == 0
value = -1
value == 0 // -1 isn't a valid value so the old one is used
value = 1
value == 1 // 1 is a valid value so it overwrites the old oneThis component is distributed as a Swift package. Just add this URL to your package list:
https://github.com/globulus/swift-property-wrappers
You can also use CocoaPods:
pod 'SwiftPropertyWrappers', '~> 1.2.0'- 1.2.1 - Fixed issue with
Expirableinternal initializer. - 1.2.0 - Added
BoundLazyWithReset, added@autoclosuretoLazyWithResetandMocked. - 1.1.1 - Added CocoaPods.
- 1.1.0 - Addded
LazyWithReset. - 1.0.0 - Initial release.