Original post at http://www.gfrigerio.com/property-wrappers/
This year’s WWDC was packed with new stuff for Swift developers, and one of them is property wrappers, formerly known as property delegate. Actually we knew about property delegate before WWDC, as there was a proposal in Swift.org a few months ago, you can read it at this link but we didn’t know about SwiftUI back then, and now it is clear what property wrappers were made for.
I started to play with SwiftUI right after I saw the sessions, but I wanted to get familiar with property wrappers first as they’re a key part of SwiftUI and I like to have a basic understanding of the whole picture. Actually you can write an app in SwiftUI and just know you can use @State or @EnvironmentObject without knowing what this @ in front of the name stands for, and this post is about a couple of examples of property wrappers in the project I made to play with Combine, there is no SwiftUI here so you can get familiar with property wrappers using them with UIKit.
Intro
What is a property wrapper?
I’ll try to explain it this way: a property wrapper allows you to define a common set of functionalities and wrap a single variable with all of them. For example Combine defines @Published, and you can assign it to a variable so you get a publisher for the value of that variable. They can be use to remove a lot of redundant code, as you’ll see in my example, and with the use of generics you can use the same property wrapper for any kind of variable, just like Combine allows with @Published.
The sample project
I’m using the project I made to write about Combine, you can find it here on GitHub
The project is described in my two previous posts about Combine: https://dev.to/gualtierofr/combine-first-example-3ie9
https://dev.to/gualtierofr/networking-example-with-combine-4be2
so here I’ll only describe how I implemented property wrappers to refactor part of the code providing the same functionalities.
Filtering an array
I’ll start with the simpler example, a property wrapper able to add a filtering functionality to an array.
Instead of calling .filter on the array directly we’ll use a property wrapper, so the actual filter will be performed by a code defined into it.
Although the wrapper is generic I expect the array to contain objects conforming to the Filterable protocol, so I can perform the filter on a given field.
protocol Filterable {
var filterField:String { get }
}
struct User:Codable, Filterable {
var filterField:String {
return username
}
...
}
This way if I have an array of User and I want to filter them I’ll do it with their username field.
Now let’s see how to define a property wrapper, you can find the implementation here
@propertyWrapper
struct Filtered where T: Filterable {
var filter:String
var filtered:[T] {
if filter.count > 0 {
return value.filter({
$0.filterField.lowercased().contains(self.filter.lowercased())
})
}
return value
}
var value:[T]
init(initialFilter:String) {
self.value = [] as! [T]
self.filter = initialFilter
}
}
by using @propertyWrapper before declaring the struct we're telling the compiler that every time a variable is declared with @Filtered it can be wrapped inside the struct. It is mandatory to provide value, this is where the wrapped variable is stored. We can use getters and setters on value if necessary, in my example I wanted to have a filter, of type String, and provide a filtered property returning the filtered elements of the array. As you can see I expect my array to contain elements conforming to Filterable, so in the filter closure I can use $0.filterField and I know it is a String, so I can lower case it and see if it contains the lowercased filter.
Let's take a look at the TableViewController where the filtered array is used as data source, you can find the implementation here
@Filtered(initialFilter:"") var users:[User]
...
func setUsers(_ users:[User]) {
self.users = users
...
}
private func applyFilter(_ filter:String) {
$users.filter = filter
tableView.reloadData()
}
First we need to declare our users array and we specify @Filtered, so the compiler knows this value is going to be wrapped inside the struct Filtered. As you can see we can initialise the struct with parameters, in my example I'm setting the filter to be an empty string.
What happens when we want to apply a new filter? We can access the Filtered struct by writing _users. So if we want the simple User array we can refer it via user, if we need to access to the property wrapper's variable and functions we use $users. You'll see plenty of examples like that in SwiftUI, just remember what _ does to your variable when you define a property wrapper.
Now we have an array of User, and we set a filter, how can we fill our UITableView? We can access to its filtered property like this
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return _users.filtered.count
}
...
// make the cell
let user = $users.filtered[indexPath.row]
cell.textLabel?.text = user.username
What if we want to implement a different filter? We can change the property wrapper, expose a new variable with a different implementation or just change the same one and only touch a single file.
Getting remote entities
In my second post about Combine I implemented a simple app to request 3 entities using public REST APIs, merge them and show them in a TableView.
As you can see in my implementation of DataSource I had similar code repeated 3 times
private func getEntity(_ entity:Entity) -> AnyPublisher {
guard let url = getUrl(forEntity: entity) else {
return Fail(error:DataSource.makeError(withString: "cannot get url")).eraseToAnyPublisher()
}
return RESTClient.getData(atURL: url)
.catch { _ in
Fail(error:DataSource.makeError(withString: "error converting data")).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
private func getAlbums() -> AnyPublisher<[Album], Never> {
return getEntity(.Album)
.decode(type: [Album].self, decoder: JSONDecoder())
.catch { error in
Just<[Album]>([])
}
.eraseToAnyPublisher()
}
private func getPictures() -> AnyPublisher<[Picture], Never> {
return getEntity(.Picture)
.decode(type: [Picture].self, decoder: JSONDecoder())
.catch { error in
Just<[Picture]>([])
}
.eraseToAnyPublisher()
}
private func getUsers() -> AnyPublisher<[User], Never> {
return getEntity(.User)
.decode(type: [User].self, decoder: JSONDecoder())
.catch { error in
Just<[User]>([])
}
.eraseToAnyPublisher()
}
I already made a generic getEntity function to remove some boilerplate (like checking every time if the URL is valid), but I'm not satisfied, I think I can shrink down this class and have less code provide the same functionality. I'll use a property wrapper for that, let's see how
@propertyWrapper
struct RemoteEntity where T:Decodable {
let url:URL?
let baseURLString = "https://jsonplaceholder.typicode.com"
var defaultValue:T
var value:T
var publisher:AnyPublisher {
guard let url = url else {
return Just(self.defaultValue).eraseToAnyPublisher()
}
return RESTClient.getData(atURL: url)
.decode(type: T.self, decoder: JSONDecoder())
.catch { error in
Just(self.defaultValue).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
init(entity:Entity) {
self.defaultValue = [] as! T
self.url = URL(string:baseURLString + entity.endPoint)
self.value = defaultValue
}
}
I expect the remote entities to be represented by JSONs, so the type is Decodable as I want to be able to decode the JSON into a struct. All of my entities are Codable, so I'll be fine.
What this property wrapper does it give me a publisher for a particular entity, and this publisher will either return the default value provided at creation if something goes wrong or return the decoded value.
Let's see how we can use RemoteEntity in our modified data source
class DataSourcePW {
@RemoteEntity(entity:.Album) var albums:[Album]
@RemoteEntity(entity:.Picture) var pictures:[Picture]
@RemoteEntity(entity:.User) var users:[User]
func getUsersWithMergedData() -> AnyPublisher<[User], Never> {
return Publishers.Zip3($pictures.publisher, $albums.publisher, $users.publisher)
.map {
let mergedAlbums = DataSourcePW.mergeAlbums($1, withPictures: $0)
return DataSourcePW.mergeUsers($2, withAlbums: mergedAlbums)
}
.eraseToAnyPublisher()
}
}
That's it, no need to have getUsers, getAlbums and getPictures like before. We were able to remove some boilerplate code and the implementation of the data source is smaller and easier to read.
Among the two example I made this is maybe more complex, but it is the best way to show you how to remove duplication and boilerplate from your codebase.
Hope you enjoyed this introduction, happy coding!
edited after beta 4, now you can access property wrappers fields by prefixing _ instead of $
Top comments (0)