As iOS developers we have to track what is new in the iOS platform. All developers want to deliver the best designed application in code and design. Each year Apple as an owner of the platform prepares WWDC. Last year at the Worldwide Developer Conference 2019 Apple introduced many new things that all developers should know, especially Combine and SwiftUI. Today I would like to focus on the Combine framework.
First time I met with Combine… of course while watching WWDC 2019, trying to catch up with all new things, all new features that Apple has provided for developers. Trying to do some coding with Combine just reading through Apple’s documentation and tutorials can be painful, so I decided to use our in-company training system to buy one of the books. I chose Combine: Asynchronous Programming with Swift from Raywenderlich by Scoot Gardner, Shai Mishali, Florent Pillet and Marin Todorov.
Before jumping into the Combine world, I have to explain where reactive programming comes from. All reactive solutions came from Microsoft library Rx.NET, Reactive Extensions for .NET. Since Microsoft distributed the code an open-source community started to implement flavours for other languages. From Rx.NET we can read that currently, flavours of RX based on Microsoft solution are available for other languages language - RxJS (JavaScript), RxJava (Java), RxScala (Scala), RxCpp (C++), Rx.rb (Ruby), RxPy (Python 3).
Before the Apple solution (Combine), the iOS community introduced frameworks such as ReactiveSwift and RxSwift to make code more readable, easy to use and easy to maintain.
Table of Contents:
1. What are ways to achieve asynchronous programming on iOS?
2. What is Combine?
3. Basics of Combine.
4. Combine pros and cons.
5. Conclusion.
What are ways to achieve asynchronous programming on iOS?
On the iOS platform, developers have various possibilities to do the coding asynchronously. Apple over the years improved their APIs to help to achieve it. Here programmers decide what kind of mechanism they will use to achieve the expected result. Developers use asynchronous ways of programming e.g. for network connections, data calculations etc. Platforms give us as developers ways to achieve asynchronous using e.g.:
- Notification Center
- Grand Central Dispatch
- Operations
- Closures callbacks
- Delegates
- Timers
OK, but for what kind of reason do we need an extra library in our ecosystem if we already have a solution to write the code...?
What is Combine?
As I mentioned before Combine is the framework introduced by Apple on WWDC 2019 as an Apple’s Swift solution for reactive programming. In Apple Developer Combine documentation we can read that adopting Combine makes code easier to read and maintain, by centralizing your event-processing code. Using one solution over the application will eliminate different techniques of handling asynchronous programming such as nested closures, delegates, etc.
The solution introduced by Apple has its own limitations. Since Apple created and introduced the world new solution for reactive streams they declared framework to be compatible only with these systems:
- iOS 13.0 and newer
- macOS 10.15 and newer
- Mac Catalyst 13.0 and newer
- tvOS 13.0 and newer
- watchOS 6.0 and newer
This is a big limitation because if we decide to create applications with Combine we cannot release applications for devices with older systems. But if we look into statistics provided by Apple measured in App Store from 27 January 2020 we can read that the current adaptation of iOS 13 on devices introduced in the last 4 years is around 77% and 70% on all devices. On the iPad devices introduced in the last 4 years are 79% and 57% on all devices.
Adaptation of iOS 13 measured by App Store on January 27, 2020.
Basics of Combine
In Combine, we can point to the main pieces - publishers and subscribers. Publisher is a protocol which declares that a type can transmit a sequence of values over time, while Subscriber is the protocol that declares a type that can receive input from a publisher.
Publisher
Publisher is a type that emits values over time to elements such as subscribers. A publisher can emit zero or more output values. Here is an important thing, if the publisher received success or a failure, it will not emit any other events. Publisher contains two associated types. Output and Failure. Output declares what kind of type we have to expect on stream output as success. If it is declared as String, we are not able to emit any different data type. Failure declares what kind of type we have to expect to be thrown by the publisher if it fails. Here if you are sure that the publisher will never throw any error, we can declare as Never type.
Apple provided few already implemented publishers such as:
- AnyPublisher - a publisher that performs type erasure by wrapping other publisher
- Future - A publisher that eventually produces a single value and then finishes or fails
- Just - A publisher that emits an output to each subscriber just once, and the finishes
- Deferred - A publisher that waits subscription before running the supplied closure to create a publisher for the new subscriber
- Empty - a publisher that never publishes any values, and optionally finishes immediately
- Fail - A publisher that immediately terminates with the specified error
- Record - A publisher that allows for recording a series of inputs and a completion, for later playback to each subscriber
Operators
Operators are specially designed methods which are called on publisher instance. As a return value from the operator, we can get the same publisher or a different one. With the help of operators, we can manage data, catch errors, and replace values if necessary. Due to operators we can in a few lines e.g. receive responses from the server, get Data from the response, decode JSON to our model, filter out, zip or even remove unnecessary values and so on.
Most of the operators will look almost the same as those which we know from Swift to do sth on collections. To check what kind of operators are available please check Publisher documentation.
Subscriber
Subscriber is a type that receives a stream from a Publisher, completion or failure events. Subscriber also contains two associated types. In this case is Input and Failure. Subscriber’s Input and Failure must match the Output and Failure of its corresponding publisher.
Combine pros and cons
Combine introduced by Apple is a new reactive framework for processing asynchronous events over time with Swift which can be helpful in the process of application development. Before taking Combine and throwing it to our applications we have to take into consideration a few things.
- First, we have to think about the users. Apple gave restrictions in using Combine, requires iOS 13 or above. It means if we want to use it in our production code we will have to stop supporting applications on previous versions.
- Second, we have to think about our coworkers. Reactive programming is not easy to understand for less experienced developers. The way of programming has to be changed to compare with the “normal” programming on iOS with Swift. It requires days or even months to get used to using it and reach knowledge.
Of course, besides these two things which can cause some problems in using Apple’s framework, there are few pros that I have to mention.
- Combine does not require us to rewrite the whole code to use it. It can be used only in the places where we’re decided that reactive programming will speed up and will simplify complex code.
- Combine is Apple’s framework. It means that the community around it will grow immediately, there will be more tutorials, more books, more topics conferences and more questions on StackOverflow about it… Already looking for some more Apple tutorials? Here we have two, that you may like:
A Complete Apple Pay Integration Tutorial - How to Do it Right?
How to Enroll in the Apple Developer Program and Why It’s So Important?
- And the last thing. Combination of SwiftUI and Combine can decrease the complexity of the application, so it means it will decrease hours spent over the code. SwiftUI has those same requirements as Combine, it requires iOS 13 or newer.
Combine Code Examples
Importing Combine into the project gives us many already created publishers. In examples, I will show how you can combine Combine in daily cases. I will focus on NotificationCenter and here we will use the sink method. Later we will focus on an easy Timer with assign method, you will see how fast and easy update your model. And at least I will provide some sample code for API connection and decoding JSON.
Example of Publisher - NotificationCenter
Here is an example of creating a publisher from NotificationCenter. The publisher is a type of NotificationCenter.Publisher.
let notificationCenter = NotificationCenter.default
// 1 - Create test notification
let testNotification = Notification.Name("Test Notification")
// 2 - Create publisher from default notification center
let publisher = notificationCenter.publisher(for: testNotification, object: nil)
Now instead of creating an observer we will create a subscription. Here is a good example of how to use a sink method.
// 3 - Create subscription from publisher
let subscription = publisher
.sink(receiveValue: { notification in
print("Test Notification received from the publisher")
})
// 4 - Post notification
notificationCenter.post(name: testNotification, object: nil)
// 5 - Cancel subscription
subscription.cancel()
Timer implementation with combine
Ok, it’s time for everyone to know about Timer. Each of the developers has to know how to deal with timers in the Apple environment. First, we need some kind of model. In this case, I created a simple MyObject class with property updateAt.
class MyObject {
var updatedAt: Date = Date()
}
let myObject = MyObject()
Let’s assume this model pretends to be a model responsible for keeping data about a conversation between two users. You have been asked to refresh the status of the conversation each 5 sec. Here we go:
// Create Timer publisher
// automatically connect to timer publisher
// Update data model using key path
let cancellable = Timer.publish(every: 5, on: .main, in: .default)
.autoconnect()
.assign(to: \.updatedAt, on: myObject)
That's all. In the first line, you're creating an AnyCancellable publisher. In the second line, you’re requesting to auto-connect to the publisher. And in the last line your assigning retrieved date to updateAt property in our sample object. If you need to cancel subscription your just calling:
// Cancel subscription if needed
cancellable.cancel()
An AnyCancellable instance automatically calls cancel() when deinitialized. API request and JSON decoding Ok, now is time to use combine to receive response from the server. First, we need a declared structure which will be representing our JSON data. Here we will use Decodable protocol. For more information about Encoding and Decoding Custom Types you can read from Apple documentation.
struct Item: Decodable {
let id: UUID
let value: String
let createdAt: Date
let updateAt: Date
}
Now is time to create an APIClient class which will be responsible for creating requests and receiving responses from our REST server. In this example I provided a fake URL address for the server, but do not hesitate to replace it with yours.
class APIClient {
private let decoder = JSONDecoder()
private let url = URL(string: "https://www.example.com/api")!
func getItem() -> AnyPublisher<item, error=""> {
var request = URLRequest(url: url)
request.allHTTPHeaderFields = [
"Accept": "application/json",
"Content-Type": "application/json"
]
return URLSession.shared
.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Item.self, decoder: decoder)
.eraseToAnyPublisher()
Let’s focus on the “main” part of the code.
return URLSession.shared
.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Item.self, decoder: decoder)
.eraseToAnyPublisher()
As you can see for URLSession Apple provides us method dataTaskPublisher(for:_) which wraps URL session data task with given URL request. By calling map(\.data) we’re expecting Data type which we are decoding by previously created JSONDecoder. In the end we’re exposing an instance of AnyPublisher to the downstream subscriber. How to use it?
var subscriptions = Set()
let api = APIClient()
api
.getItem()
.sink(receiveCompletion: { print($0) },
receiveValue: { print("Received value \($0)")})
.store(in: &subscriptions)
First we are declaring a Set of AnyCancellable and at the end we store a subscription using .store(in: &subscriptions). We have to store subscription, without keeping references the publisher will terminate immediately and subscription will be cancelled.
By using the sink method we can handle completion and received value.
Conclusion
As you can see Combine is quite an interesting way of reactive programming which you can introduce to your development team, to your project. You may wonder why I had interested in Combine. The first reason is that reactive programming is on the top right now in many companies, teams are introducing RxSwift and ReactiveSwift to their project as a good way to speed up the development process. In both mentioned cases we are using third companies solutions which can also have some cons and pros. The second reason is that I watched WWDC 2019 each year from the start of my career as an iOS developer and I really wanted to try what Apple gave developers.
It is really hard to develop stunning mobile apps without knowing the latest mobile trends. Fortunately, we have a whole article about it. You can check it immediately here:
Sources:
- https://developer.apple.com/documentation/combine
- https://developer.apple.com/videos/wwdc2019/
- https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift
- https://www.raywenderlich.com/7864801-combine-getting-started
- http://reactivex.io