Swift Property Wrappers
A Collection of useful Swift property wrappers to make coding easier.
- 13 wrappers included.
- Most wrappers are fully unit tested.
- Most wrappers can be observed via
Publisher
s exposed in theirprojectedValue
s. - PRs welcome.
Contents
Here's the list of all the wrappers included in the package.
Atomic
- Synchronizes property reads and writes using the provided
DispatchQueue
. projectedValue
exposes the wrapper itself and allows for using itsmutate
method 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
Clamped
- Ensures that the property's
Comparable
value 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.
projectedValue
provides aPublisher
that 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
ColorHex
- A string wrapper that exposes a SwiftUI
Color
via itsprojectedValue
if 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,
projectedValue
isnil
.
Sample use:
@ColorHex var colorHex = "fff"
$colorHex == Color(white: 1)
colorHex = "FF0000"
$colorHex == Color(red: 1, green: 0, blue: 0)
colorHex = "fail"
$colorHex == nil
CopyOnWrite
- Allows for pass-by-copy by ensuring that
copy
method is invoked whenever a value is assigned. - Property type must conform to the
Copyable
protocol.
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
Delayed
- 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
projectedValue
returnstrue
if 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
Expirable
- Property wrapper for a value that "expires" after a set period of time - trying to read the value
expirationPeriod
seconds 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
Localized
- 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.
projectedValue
provides 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
Logged
- 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
nil
and write block prints the newly set value. projectedValue
provides 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
Mocked
- Always returns the value specified by the
mock
block. - 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)]
Rounded
- 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
projectedValue
provides 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
Tranformed
- Transforms the assigned value using the provided block, allowing for a wide array of applications, from automatically formatting strings, transforming numbers, etc.
projectedValue
provides 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"
UnitInterval
- 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.
projectedValue
provides 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
Validated
- Only sets the new value is it passes validation by the provided block, which allows for vetoing new values.
projectedValue
provides 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 one
Installation
This component is distrubuted as a Swift package. Just add this URL to your package list:
https://github.com/globulus/swift-property-wrappers
Changelog
- 1.0.0 - Initial release.