Property Wrappers with Combine
In this post I’ll look into making more out of Combine code by using the newly introduced in Swift 5.1: Property Wrappers.
Let’s very quickly go into what property wrappers are in general and then dive into a Combine example.
Swift Property Wrappers
Property wrappers are a new feature in Swift 5.1 that allows you to abstract redundant functionality around handling type properties.
Property wrappers are very handy in (my experience so far π€ ) two ways:
- You can control the value of a property via your potentially complex logic absolutely opaquely to the consumer, and
- You can have a “projected value” which is an automatically added to your type property, which you can create/update in relation to the original property you “wrap”.
Let’s have a look at the first of these below and in the next section we’ll cover number two. π¬
You’re building a view model and you’d like to have a property called progress
like so:
struct ViewModel {
var progress: Int = 0
}
Since the value is a completion precentage no values outside the [0, 100]
range would make sense. You can “wrap” the progress
property with a custom wrapper to limit the property’s potential values.
Here’s how to write that custom wrapper code:
@propertyWrapper
public struct Percentage {
var value: Int
public var wrappedValue: Int {
get { value }
set { value = max(0, min(100, newValue)) }
}
public init(wrappedValue: Int) {
value = wrappedValue
}
}
I declare a struct called Percentage
which features the attribute @propertyWrapper
that makes @Percentage
part of the Swift syntax (thanks to syntactic sugar). The type’s members are as follows:
value
is the wrapper data storage,wrappedValue
is the proxy to the original property (e.g. this is what you’re accessing when you get/set the property being wrapped),init(wrappedValue:)
is the wrapper initializer that takes the initial property value as declared in code.
Now I can prepend @Percentage
to any property and make it a perecentage value limited to the range [0, 100]
.
Let’s see an example that will demonstrate the above. Let’s revise the ViewModel
type from earlier and wrap its progress
property:
struct ViewModel {
@Percentage var progress: Int = 0
}
Now a new instance of Percentage
will automatically manage the progress
property by proxying it through its wrappedValue
like so:
model.progress = 34
print(model.progress)
// prints "34"
model.progress = -10
print(model.progress)
// prints "0"
model.progress = 100000
print(model.progress)
// prints "100"
Property wrappers let you abstract redundant code and make Swift’s syntax more semantic. However, they also somewhat obscure code that is being trigger automatically. So, as with any powerful tool - moderation is key :)
But let’s see how in practice we can leverage the power of property wrappers with some Combine code.
Property Wrappers and Combine
In Simple custom Combine operators and Building a custom sample
operator we looked into creating very simple custom operators by abstracting redundant code.
These simple operators allow you to save yourself some typing when you consume some API and need to repeatedly process the stream in the same way. In other words if you always need to map, filter, and then remove duplicates from a stream you are free to group those operators under a “new” operator whenever that makes sense.
But how about if you are providing the API and your consumers need to always do the same operations on your output stream?
Let’s look at our view model but this time Combine-ified:
class CourseViewModel {
@Published var name: String
@Published var surname: String
@Published var progress: Int
...
}
The users of your view model would most likely bind the three published properties to some UI controls - labels or other views. This will force them to write code like this to ensure they always update the UI on the main queue π:
viewModel.$name
.receive(on: DispatchQueue.main)
.assign(to: \.text, on: nameLabel)
viewModel.$surname
.receive(on: DispatchQueue.main)
.assign(to: \.text, on: surnameLabel)
viewModel.$progress
.map { "Course progress: \($0)%" }
.receive(on: DispatchQueue.main)
.assign(to: \.text, on: progressLabel)
To avoid this kind of repetition (always having to switch explicitly to the main thread) we can abstract that via a new property wrapper.
So, let’s do it! π
First we’ll add almost exactly the same code as for the @Percentage
wrapper and call the new type PublishedOnMain
(as opposite to just @Published
):
@propertyWrapper
public class PublishedOnMain<Value> {
var value: Value
public var wrappedValue: Value {
get { value }
set { value = newValue }
}
public init(wrappedValue initialValue: Value) {
value = initialValue
}
}
As you see this time there is no custom logic of any sort for wrapping the original property. PublishedOnMain
is generic over its storage value Value
so it’s just a property wrapper that can be added to any type of property.
First of all let’s add a publisher to the storage property, like so:
@Published var value: Value
This adds a $value
to PublishedOnMain
which is a publisher emitting each time value
is updated.
Note:If you want to learn more about
@Published
and other ways to create Combine publishers definitely check out Combine: Asynchronous programming with Swift.
In this example we’ll use another property a wrapper might include called a projectedValue
. Let’s have a look - I’ll add projectedValue
to the wrapper type:
Now I’ll add the projected value which will subscribe $value
and make sure it receives on the main queue:
public var projectedValue: AnyPublisher<Value, Never> {
return $value
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Just like explained in subscribe(on:) vs receive(on:) I’m using receive(on:)
to switch the downstream scheduler to the main queue.
And that’s pretty much it - the completed property wrapper code is:
@propertyWrapper
public class PublishedOnMain<Value> {
@Published var value: Value
public var wrappedValue: Value {
get { value }
set { value = newValue }
}
public var projectedValue: AnyPublisher<Value, Never> {
return $value
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
public init(wrappedValue initialValue: Value) {
value = initialValue
}
}
Now I can use @PublishedOnMain
on any property to add a “mirror” property which is a publisher on the main queue other types can subscribe. This effectively removes the need for consumers to always ensure receiving on the main queue for each subscription. π€―
Now I can improve my CourseViewModel
from earlier like so:
struct CourseViewModel {
@PublishedOnMain var name: String
@PublishedOnMain var surname: String
@PublishedOnMain var progress: Int
}
And safely bind the publishers to my UI controls:
viewModel.$name
.assign(to: \.text, on: nameLabel)
viewModel.$surname
.assign(to: \.text, on: surnameLabel)
viewModel.$progress
.map { "Course progress: \($0)%" }
.assign(to: \.text, on: progressLabel)
Isn’t that just awesome?
Further Combine Property Wrappers
What else could be a useful abstraction via property wrappers? The sky is the limit as long as the code is clear at the point of usage and the property wrapper names are semantic.
Let’s look at one final example - let’s make a property wrapper ensuring a property emits a maxium of one value:
@propertyWrapper
public class Single<Value> {
@Published var value: Value
public var wrappedValue: Value {
get { value }
set { value = newValue }
}
public var projectedValue: AnyPublisher<Value, Never> {
return $value
.prefix(1)
.eraseToAnyPublisher()
}
public init(wrappedValue initialValue: Value) {
value = initialValue
}
}
The code is largely the same as before - the only difference being that the propejectedValue
publisher will emit either zero or one elements thanks to the prefix(1)
operator.
That’s all for today - wasn’t that fun? π
Where to go from here?
To learn about all the Combine operators which you can use to create your own property wrappers have a look at Combine: Asynchronous programming with Swift - this is where you can see all updates, discuss in the website forums, and more.