Contents Practical Combine: An introduction to Combine with real examples 6 Chapter overview Chapter 1 - Introducing Functional Reactive Programming . . . . . . . . . . . . . Chapter 2 - Exploring publishers and subscribers . . . . . . . . . . . . . . . . . . Chapter 3 - Transforming publishers . . . . . . . . . . . . . . . . . . . . . . . . . Chapter 4 - Updating the User Interface . . . . . . . . . . . . . . . . . . . . . . . Chapter 5 - Using Combine to respond to user input . . . . . . . . . . . . . . . . Chapter 6 - Using Combine for networking . . . . . . . . . . . . . . . . . . . . . Chapter 7 - Wrapping existing asynchronous processes with Futures in Combine . Chapter 8 - Understanding Combine’s Schedulers . . . . . . . . . . . . . . . . . Chapter 9 - Building your own Publishers, Subscribers, and Subscriptions . . . . Chapter 10 - Debugging your Combine code . . . . . . . . . . . . . . . . . . . . . Chapter 11 - Testing code that uses Combine . . . . . . . . . . . . . . . . . . . . Chapter 12 - Driving publishers and flows with subjects . . . . . . . . . . . . . . Chapter 13 - Where to go from here . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 8 8 9 9 10 10 11 11 11 12 12 12 . . . . 13 13 15 18 19 . . . . 20 20 23 26 28 Transforming publishers Applying common transformations to a publisher . . . . . . . . . . . . . . . . . . Understanding the differences between map, flatMap and compactMap . . . . . . Using compactMap in Combine . . . . . . . . . . . . . . . . . . . . . . . . . 30 30 35 35 Introducing Functional Reactive Programming Understanding Functional Reactive Programming Enhancing readability with FRP . . . . . . . . . . Comparing Combine to RxSwift . . . . . . . . . . In Summary . . . . . . . . . . . . . . . . . . . . . Exploring Publishers and Subscribers Creating your first Publisher . . . . . . Subscribing to a Publisher’s stream . . Understanding the lifecycle of a stream In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Practical Combine Using flatMap in Combine . . . . . . . . . . . . . . . . . . . . . . . . Limiting the number of active publishers that are produced by flatMap Applying operators that might fail . . . . . . . . . . . . . . . . . . . . . . . Defining custom operators . . . . . . . . . . . . . . . . . . . . . . . . . . . In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Updating the User Interface Creating publishers for your models and data . . . . . . . . . . . . . . . . . . . . Using a PassthroughSubject to send a stream of values . . . . . . . . . . . . Using a CurrentValueSubject to represent a stateful stream of values . . . . . Wrapping properties with the @Published property wrapper to turn them into publishers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Choosing the appropriate mechanism to publish information . . . . . . . . . Directly assigning the output of a publisher with assign(to:on:) . . . . . . . . . . . Using Combine to drive Collection Views . . . . . . . . . . . . . . . . . . . . . . . Using Combine to update a collection view’s data source . . . . . . . . . . . Driving collection view cells with Combine . . . . . . . . . . . . . . . . . . . Assigning the output of a publisher to an @Published property with assign(to:) Creating a simple theming system with Combine . . . . . . . . . . . . . . . . . . . In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using Combine to respond to user input Updating the UI based on user input . . . . . . . . . . . . Limiting the frequency of user input . . . . . . . . . . . . . Combining multiple user inputs into a single publisher . . Combining publishers with Publishers.Zip . . . . . . Combining publishers with Publishers.Merge . . . . . Combining publishers with Publishers.CombineLatest In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . Using Combine for networking Creating a simple networking layer in Combine . Handling JSON responses from a network call . User-friendly networking with Combine . . . . . Building a complex chain of network calls . . . . Donny Wals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 42 45 46 49 51 52 52 55 58 60 60 64 65 69 72 76 81 . . . . . . . 82 82 87 97 99 104 106 108 . . . . 110 110 115 123 126 3 Practical Combine In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wrapping existing asynchronous processes with Futures in Combine Understanding how Futures work . . . . . . . . . . . . . . . . . . . Using Combine to ask for push permissions . . . . . . . . . . . . . . Using Futures to fetch data from Core Data . . . . . . . . . . . . . . In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 132 132 141 145 147 Understanding Combine’s Schedulers 148 Exploring the Scheduler protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 Understanding receive(on:) and subscribe(on:) . . . . . . . . . . . . . . . . . . . . 150 In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 Building your own Publishers, Subscribers, and Subscriptions Understanding what happens when you subscribe to a publisher . . . . . . . Implementing a custom Subscriber . . . . . . . . . . . . . . . . . . . . Understanding backpressure . . . . . . . . . . . . . . . . . . . . . . . Understanding how you can create a custom publisher and subscription Extending UIControl with a custom publisher . . . . . . . . . . . . . . . . . . In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Debugging your Combine code Using print to debug your code . . . . . . . . . . . . . . Using Timelane to understand and debug your code . . . Preparing to use Timelane . . . . . . . . . . . . . . Using Timelane to debug a simple sequence . . . . Using Timelane to debug a sequence of publishers . In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testing code that uses Combine Writing tests for code that uses Combine . . . . . . . . . . . . . Optimizing Combine code for testability . . . . . . . . . . . . . Architecting code that uses a NotificationCenter publisher Architecting code that requires a networking layer . . . . . Creating helpers to make testing Combine code easier . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Donny Wals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 159 161 164 166 170 174 . . . . . . 176 176 177 178 181 189 192 . . . . . . 193 194 201 202 206 216 223 4 Practical Combine Driving publishers and flows with subjects Retrieving partial data in Combine . . . . . . . . . . . . . Recursively loading data with subjects and flatMap . . . Automatically retrying a network call after a token refresh In Summary . . . . . . . . . . . . . . . . . . . . . . . . . Where to go from here? Donny Wals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224 224 229 233 239 240 5 Practical Combine Practical Combine: An introduction to Combine with real examples At WWDC 2019, Apple announced a framework called Combine. It’s Apple’s take on a familiar concept that’s been popularized by RxSwift over the past couple of years, and it’s one of the driving forces behind the new SwiftUI framework. Getting started with new technology like this can be complicated and confusing, especially since Apple’s documentation isn’t always up to snuff when it comes to being a learning resource. To help bridge the gap between your existing codebase and learning functional reactive programming with Combine I decided I wanted to write this book. And not just to write a book that covers the framework on a theoretical level. Instead, I wanted to write a book that uses examples that are based on real-world usage of functional reactive programming. I learn best when I can see how new concepts can be applied to the code I write every day. And I know many other folks like learning the same way. Throughout this book, I will start you off with some theory. Without it, I don’t think any of the examples would make sense and there would be a ton of friction in making sense of the code I’d show you. But once the theory is out of the way, we’re off to the races. You will learn how you can use Combine to drive your UI, how you can use it to respond to user input, how you can integrate it with networking and much, much more. In the final chapter, I will show you how you can write custom Combine publishers to extend the framework if needed to make it fit your requirements. I hope you will enjoy the journey I’m about to take you on, and that you will come out with a good idea of how Combine can help you bring functional reactive programming into your codebase. This book would not have been possible without folks from the community who have been asking me questions, and telling me what they want to learn. So if you’ve ever reached out to me with a question or comment, I want to thank you for that. Your feedback helped me write this book. I also want to thank all of my friends from the iOS community that reviewed this book, and those that have encouraged me to write it. In no particular order, I want to mention a couple of these folks. Nic Laughter for always being so positive and supportive. Daniel Steinberg for encouraging me to continue doing what I love, and helping me to get Donny Wals 6 Practical Combine better at it. Benedikt Terhechte and Vadim Drobinin for giving me feedback on the pre-release edition of this book. Paul Hudson for helping me promote my book and for everything he does to support folks in our community. Marin Todorov for reviewing my chapter on debugging and his tool Timelane. Joe Fabisevich for asking me countless questions about this book, keeping on my toes and helping me improve the book in several areas. And lastly, I want to thank my fiancee, Dorien for always putting up with me when I decide to take on yet another writing project. If you find any mistakes, errors or inconsistencies in this book don’t hesitate to send me an email at feedback@donnywals.com. I’ve put a lot of care and attention into this book but I’m only human and I need your feedback to make this book the best resource it can be. Make sure you also reach out if you have any questions that aren’t answered by this book even though you hoped it would so I can answer your questions directly, or possibly update the book if needed. Cheers, Donny Donny Wals 7 Practical Combine Chapter overview Chapter 1 - Introducing Functional Reactive Programming Learn the basic building blocks of Combine and Functional Reactive Programming (FRP). I explain what FRP is, and how it can help you write better and cleaner code. You’ll immediately see little bits and pieces of Combine code to help you see how FRP enhances code readability in certain cases. You’ll also learn about some of the very superficial differences between RxSwift and Combine. This will help you understand where Combine fits in the FRP ecosystem, and it will help you choose between the two frameworks if your app is for iOS 13 and above. Chapter 2 - Exploring publishers and subscribers Publishers are the bread and butter of the Combine framework. Without publishers, we’d have nothing to subscribe to. In this chapter you will learn how publishers work, how you can subscribe to the values they publish, and more importantly, you’ll learn what all of this means. After going through a basic example, you will learn about the lifecycle of a subscription stream, and how subscribing to a publisher works exactly. Chapter 3 - Transforming publishers In most cases, the values that are published by a publisher need to be modified before they can be used by a subscriber. In this chapter, I demonstrate the usage of several of Combine’s built-in operators like map, flatMap and compactMap. We’ll also cover some more advanced operators like replaceNil and setFailureType. You will also learn about operators that might throw an error to end the publisher stream, like tryMap, tryCompactMap and Donny Wals 8 Practical Combine more. We’ll wrap up the chapter by defining a custom operator on Publisher that bundles a couple of other operators in a convenient wrapper. Chapter 4 - Updating the User Interface In this fourth chapter, we’re finally going to get truly practical. You will learn how you can use Combine to update the value of a label with a new subscriber called assign(to:on:). You will also learn how Combine can be used in an MVVM-like architecture, and how it can be used to drive a collection view’s diffable data source without implementing any specific architecture. In addition to showing you how to update a collection view data source, I also demonstrate how you can use Combine to download images that need to be displayed in a collection view cell. And to top it all off, you’ll learn how to build a Combine-driven theme manager that can be used to allow a user to switch your app between dark- and light mode separate from the system setting. Chapter 5 - Using Combine to respond to user input In this chapter, you’ll learn how to implement several simple bindings between UI components and a model. Once you understand how this works in UIKit, I’ll demonstrate how the same principle is implemented in SwiftUI because a lot of this functionality comes for free in SwiftUI. You’ll also learn how you can restrict the processing of user input through a feature called debouncing. This is especially useful if you’re building a feature where user input results in expensive processing like networking for example. You will also learn about the different mechanisms that Combine offers to merge and combine the output from multiple publishers into a single publisher. By the end of this chapter, you should have a very good understanding of how publishers in Combine can be updated, and how they can be used to integrate a model and user interface element with Combine. Donny Wals 9 Practical Combine Chapter 6 - Using Combine for networking Throughout the preceding chapters, I often use networking as an example of a Combine publisher. The usage of networking examples so far has been very basic though. In this chapter, you will gain a more advanced understanding of networking in Combine, and how Combine can help you support features like low data mode on iOS, or how you can implement graceful error handling. In addition to networking, you will learn about some interesting new operators like tryCatch and switchToLatest. You’ll implement a pretty complicated token refresh flow with these new operators, and a couple of operators you’re familiar with at this point. This chapter doesn’t stop there. I will also show you how you can orchestrate several network calls at once, and merge their outputs into a single publisher. By the end of this chapter, you will have all the knowledge needed to implement a robust networking layer with Combine. Chapter 7 - Wrapping existing asynchronous processes with Futures in Combine Not all of your asynchronous code is easy to integrate with Combine. In this chapter, you will learn about a very special publisher called a Future. This publisher doesn’t quite play by the same rules as other publishers, but it’s a very convenient publisher that allows us to kick off asynchronous work, and broadcast the result of this work to its subscribers. You will learn how to implement a simple Future based interface on top of UNUserNotificationCenter, and I will show you how to wrap code in a Future. Donny Wals 10 Practical Combine Chapter 8 - Understanding Combine’s Schedulers In Combine, a lot of threading and dispatch queue related logic is hidden from you, and that’s extremely convenient. However, there are times where you might need to do some manual work to make sure a certain publisher sends its events on the main queue, or you might want to make sure that it doesn’t run code on the main queue. Combine abstracts dispatch queues behind schedulers. Learn how schedulers work in this chapter, and find out how they impact the code you write in your apps. Chapter 9 - Building your own Publishers, Subscribers, and Subscriptions This chapter comes with a huge warning. Nothing in this chapter should be used in production. However, we will conceptually pull apart what happens when you subscribe to a publisher using sink by reimplementing the functionality. Apple does not recommend that users of Combine do this, and it’s for a good reason. The fine details of how subscriptions work are hidden from us as users of the framework. Regardless, I like to re-implement functionality in an attempt to better understand how it might work which can lead to interesting insights. Chapter 10 - Debugging your Combine code Debugging is hard, especially when you’re dealing with asynchronous code. In this chapter, you will learn more about Combine’s print operator and you will learn about a community tool called Timelane that can be used to visualize the lifecycle and emitted values of your Combine subscriptions. Donny Wals 11 Practical Combine Chapter 11 - Testing code that uses Combine In this chapter, you will learn how you can use XCTest to write unit tests for your Combine code. You will learn things that you should, and shouldn’t do when testing Combine code. I will also show you how you can optimize your code for testability and I will show you two convenient helpers that can help you improve and clean up your test suite. Chapter 12 - Driving publishers and flows with subjects Learn how you can build really powerful and complex features using Combine’s PassthroughSubject, CurrentValueSubject and some clever operators. You will learn how to deal with a paginated API in Combine, how you can recursively load data and I will show you an alternative approach to retrying network calls after a token refresh. Chapter 13 - Where to go from here While this book should prepare you for a lot of Combine use-cases, there’s also a lot that I don’t cover. In this chapter, I aim to give you some insights into the features I didn’t cover, and resources you might want to pick up to expand your Combine knowledge and expertise. Donny Wals 12 Practical Combine Introducing Functional Reactive Programming Combine is a so-called Functional Reactive Programming framework. This means that it has something to do with functional programming, and reacting to an ever-changing environment. Throughout this book, you will learn everything you need to know to adopt Combine, and Functional Reactive Programming, in your projects. As you might expect, I will take a very practical approach to teach you Combine to make sure you’re up and running with this powerful framework as soon as possible. To make sure that you understand what Combine is, and what it should be used for, I want to make sure that you have a solid foundation first. This means that we’ll need to chew through some less practical but fundamentally important chapters first. In this first chapter of Practical Combine you will learn about the following topics: • Understanding Functional Reactive Programming (FRP) • Enhancing readability with FRP • Comparing Combine to RxSwift By the end of this chapter, you should be able to rationalize choosing Combine in your projects, and it should be clear what FRP is, and how it benefits you as a developer. Understanding Functional Reactive Programming As I mentioned, Combine is a Functional Reactive Programming framework. Through this chapter and the rest of the book, I will refer to Functional Reactive Programming as FRP which is a common abbreviation used in the community. FRP is built upon principles that come from Functional Programming. This means that FRP enables us to write code that can be composed using many small functions that operate only on their inputs without changing anything that’s outside of the function itself. An example of this is the well-known map function in Swift: Donny Wals 13 Practical Combine [1, 2, 3].map { $0 * 2 } The preceding code takes an array of integers and calls map on it. This allows us to transform the array of integers into something else. In this case by multiplying the argument that is received by map (we use $0 as a short-form notation in Swift) and multiplying it by two. The map function only operates on the array that it’s called on and instead of changing the existing array, it returns a brand-new, mapped array. The closure passed to map acts as a function in functional programming and it only operates on the arguments that it receives. This means that we were able to create a new array with different contents than the original, without performing any work that’s not encapsulated by a function. A function that takes another function or closure as its parameter is called a higher-order function in Functional Programming. A function that only operates on the arguments it receives is called a pure function. These two terms are extremely important because they form the basis for many of Combine’s features. Since my goal is not to teach you Functional Programming, I will explain bits and pieces about Functional Programming throughout the book where needed. Some explanations might be simplified to make sure we don’t get lost in all the small details and nuances of Functional Programming because I want to make sure that we focus on the goal which is to get you up and running with Combine. So at this point, you should have enough information to understand what the functional bit in FRP means. So what does the reactive bit mean? The reactive part of FRP means that we don’t operate on objects synchronously. Instead, we compose functions and operations in such a way that whenever something happens and we receive a new value of something, we perform operations on it, to get to a certain outcome. If you consider the map example I showed you earlier, you can envision how that works. First, we have an array with [1, 2, 3] as its contents and when the map is executed, it creates a new array that has [2, 4, 6] as its contents. Simple enough. Everything runs synchronously. Similar to how we can map over an array of known values, we can map over an array of unknown values. Consider a scenario where values or events are emitted over time. We can take each new value as it’s emitted, and we can transform it using a map until we have a result. Let’s look at an example so you can get an idea of what I mean: Donny Wals 14 Practical Combine someButton.onTap.map { _ in return Bool.random() }.sink { didWin in if didWin { print("Congratulations! You won a price") } else { print("You didn't win! Better luck next time") } } Tip: If you want to learn more about the preceding code, check out Chapter 5 - Using Combine to respond to user input The preceding code is an example where we take tap events that are emitted by a button when a user taps this button. We call these events a stream. As you will learn later, everything in FRP is considered a stream. You can think of a stream as an array of values where the values are delivered over time. Whenever a new tap value is emitted, it is passed to map. The map implementation that I just showed you ignores the value, and transforms it into a random boolean, much like how we multiplied integers in the map example that I showed you earlier. Finally, a message is printed based on the received boolean. In Combine, we use the sink method to receive values. It’s okay to perform mutations or to operate on the world that surrounds the sink because it is not part of the publisher chain. You will learn much more about sink in Chapter 2 - Exploring publishers and subscribers. Based on the examples above, you can say that FRP uses Functional Programming to react to events that occur over time, or asynchronously. Enhancing readability with FRP When you’re just starting out with FRP, you might wonder how it’s any better than not using FRP. As you will find out throughout this book, FRP can increase code readability and it reduces code complexity dramatically. FRP’s most significant drawback is that it tends to have quite a steep learning curve. Once the principles click, and you understand how FRP and Combine work, it starts to become very clear how FRP enhances readability. Donny Wals 15 Practical Combine Even though you haven’t learned any Combine yet, I want to show you an example of how Combine can increase readability when comparing it to a traditional callback-based approach. If you perform a network call without Combine, you might write code that looks something like the following: let myUrl = URL(string: "https://www.donnywals.com")! func requestData(_ completion: @escaping (Result<Data, Error>) -> ,→ Void) { URLSession.shared.dataTask(with: myUrl) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { preconditionFailure("If there is no error, data should be ,→ present...") } completion(.success(data)) }.resume() } While the code above isn’t complex, you can see how it would be hard to chain together several data tasks because you would have to nest completion handlers deeper and deeper. And not just that, there is a large amount of boilerplate code involved with this simple request. First, we need to do some optional unwrapping in the completion handler for the request itself, and then there is more boilerplate code involved where requestData(_:) is called because the Result object needs to be unpacked. Let’s look at the same request in Combine: let myUrl = URL(string: "https://www.donnywals.com")! func requestData() -> AnyPublisher<Data, URLError> { URLSession.shared.dataTaskPublisher(for: myUrl) Donny Wals 16 Practical Combine .map(\.data) .eraseToAnyPublisher() } Instead of handling error and success in both the completion handler that’s passed to URLSession, and then again in the method that calls requestData, the completion and error events are passed down Combine’s value stream. If you’re not yet sure what that means, don’t worry. I will explain it all in-depth in the next chapter, Exploring publishers and subscribers. Also note that instead of returning an object that has a generic Error as its failure type, we can return a publisher that has a URLError as its failure type. This is much more convenient for callers of requestData. All in all, Combine can help you get rid of lots of boilerplate code which will help you focus on what matters; coding your app. Also note that because Combine uses several small and pure functions called operators to transform values as they are passed down the chain, you can perform lots of readable, boilerplate-free transformations that almost read like normal sentences. Let’s briefly look at the example below. Even though I haven’t explained Combine yet, I’m sure you have an idea of what this code does: requestData() .decode(type: SomeModel.self, decoder: JSONDecoder()) .map { $0.name } .sink(receiveCompletion: { _ in }, receiveValue: { print($0) }) If you weren’t sure, the code above calls the requestData method I showed you earlier, it decodes the data returned by the request into a SomeModel, it maps over the result to extract the name of the decoded data, and it then prints the name. You will learn more about all of this in the chapters to come but I wanted to show you how readable a chain of Combine operators can be, and how you can compose them together to create a chain of transformations that make small, isolated changes to data. Donny Wals 17 Practical Combine Comparing Combine to RxSwift If you have experience with FRP, you might have skipped the previous two sections to get to this section right away. You’d probably be wondering, how is Combine different from RxSwift? And why would I switch? Or if you’re just learning about FRP, you might be wondering which one of the two frameworks you should choose based on their similarities and differences. There are several FRP frameworks available for iOS but and I chose to look at RxSwift and Combine only. A framework that I won’t look at but wanted to mention anyway just because of its user base is ReactiveCocoa. The ReactiveCocoa framework has many users, maybe even more than RxSwift has based on their GitHub stars at the time of writing, but its focus on UI elements makes it too different from RxSwift and Combine for me to consider it in this brief comparison. RxSwift is a framework that implements the cross-platform ReactiveX API. ReactiveX is a standard for reactive programming that is adopted on many platforms. There are flavors of Javascript, Java, and more available. If you’re working in a cross-platform team, this means that code from one platform can sometimes be pretty much copied and pasted to other projects. That on its own is a huge benefit. On top of that RxSwift is open source which means that you have insight into its development, and you can even contribute bug fixes if you’d like. That said, RxSwift is a third-party dependency. And just like any other third-party dependency, you have to trust the project’s maintainers to ship bug-free, safe code on a timeline that fits yours. And on top of that, RxSwift’s maintainers can decide to stop maintaining the project whenever they please. This, of course, is unlikely but not impossible. With Combine, you buy into Apple’s ecosystem. This means that you’re at their mercy for bug fixes to arrive in new iOS versions, and the framework’s behavior might change between iOS versions. On the flip side, you can be pretty sure that Apple will keep Combine around for a long time, even if it’s just for compatibility reasons. And more importantly, you don’t need to import any external code into your codebase. Overall, I think Combine is probably a better choice for the sole reason that it’s Apple’s FRP framework. It’s tightly integrated with some of Apple’s existing APIs and it’s the driving force behind SwiftUI. So if you have plans to use SwiftUI or any of the integrated Combine features, it makes more sense to use Combine in my opinion because you would otherwise mix different Donny Wals 18 Practical Combine FRP frameworks in one project. In the end, it’s your choice but since you’re reading this book, I think you may have made up your mind. And if you haven’t, this book should provide you with enough examples and information to help you make a decision. In Summary In this chapter, I have explained the basics of FRP to you. You learned that FRP borrows a lot of principles from Functional Programming and that it uses pure functions and higherorder functions to compose complex behaviors with several small operators. You saw some examples, like how you could transform a button tap into a random boolean value using a custom Combine publisher. I also showed you how Combine can help you clean up callback-based APIs by hiding complexity in small operators that each perform a little bit of work to get to the desired results. Lastly, you learned about some of the similarities and differences between RxSwift and Combine. In the end, they are very similar and you might even consider them to be two sides of the same coin. I personally like Combine a lot, and I’m sure you will too. In the next chapter, I will show you Combine’s publishers and subscribers, and you will learn more about the bits and pieces of code that you saw in this chapter. Donny Wals 19 Practical Combine Exploring Publishers and Subscribers In the previous chapter, you learned about what Combine is, and the principles it is built upon. I think it’s important to understand these principles, or at least be aware of them before we move on to using Combine. Because you know what FRP is, and what its characteristics are, I hope that the content in this chapter and all chapters that come after it are a lot clearer than they would be if I had started with this chapter. In this chapter, you will learn about some of Combine’s fundamental building blocks; publishers and subscribers. You will learn about the following topics: • Creating your first Publisher • Subscribing to a publisher’s stream • Understanding the lifecycle of a stream Once you understand all of the three topics listed above, you understand a huge part of the Combine framework. In my opinion, its complexity often doesn’t lie in Combine itself. It lies in how you can use it. And that’s the whole reason that a large part of this book is about practical examples of Combine that you are likely to encounter in code that you will either find in the wild or write yourself. But we’re not quite there yet. There’s still some fundamental knowledge to be gained. Creating your first Publisher In Combine, everything starts with a publisher. Without a publisher, there is nothing to subscribe to and nothing to receive values from. So whenever you want to do something that involves Combine, it’s a given that there will be a publisher involved in some way. In the previous chapter I’ve already shown you some Combine code to illustrate some of the points I was making, but let’s rewind a bit to create and examine a very basic publisher: let publisher = [1, 2, 3].publisher The preceding code converts an array to a publisher that emits the contents of this array one by one. It will do this as soon as an object subscribes to it. I will explain this in-depth in the Donny Wals 20 Practical Combine next section. If you look at the type of this publisher, you will find that it’s Publishers.Sequence<[Int], Never>. This type signature tells us a lot about Combine. First, it tells us that Combine contains an object called Publishers and that it likely defines several publishers. We can explore this by looking at the documentation for Publishers. If you do this, you will find that Publishers is an enum and that it’s “A namespace for types that serve as publishers.”. In other words, this enum contains a lot of Combine’s built-in publishers. The Sequence publisher is one of these built-in publishers. If you scroll through the list of publishers you will find that publishers like Publishers.Map and Publishers.Filter exist. If you’re familiar with Swift’s map and filter functions, you probably already know what these publishers do. If you’re not familiar with these functions, or you’re not sure what the Publishers.Map and Publishers.Filter publishers might do, don’t worry. I will explain all of them in the next chapter. If you look at the documentation for the Publishers.Sequence publisher, you will find that it’s “A publisher that publishes a given sequence of elements.”. If you look at its type signature you’ll see that it takes an object that conforms to Sequence as its first generic argument, and an Error as its second argument. This shouldn’t be too surprising based on the type of publisher we created in the example earlier. What’s more interesting is that Publishers.Sequence conforms to the Publisher protocol. If you look up this protocol in the documentation, you’ll find that it provides a ton of functionality but what’s important for now are Publisher’s associated types: Output and Failure. Every publisher in Combine has an Output and a Failure. The Output of a publisher is the type of value that it produces. For the Publishers.Sequence publisher that we created earlier, the Output is Int. This means that subscribers of a sequence publisher will receive Int objects as their input. Even though the publisher itself takes a Sequence as its generic argument, it produces single elements as its output. The failure type of the Publisher.Sequence from before is Never. This means that this publisher can only complete successfully. It never emits error events. In FRP, it’s common to reason about publishers and operations like map in a Marble Diagram. The following image shows an example of a marble diagram that resembles the publisher created in the code above. Donny Wals 21 Practical Combine Figure 1: A marble diagram for a publisher that emits three values I wouldn’t be surprised if you’ve been reading this section up until now, wondering when you would ever use a publisher like the one I just defined. This book is all about getting you to use Combine in practice, after all. The answer is somewhat unsatisfying. I don’t think it’s likely that you will be creating publishers of sequences like we just did regularly. A simple publisher like that does a fantastic job of showing what a publisher is though, and it allowed us to explore what a publisher is. When you start using Combine, you will find that Apple has created a bunch of publishers that are defined as extensions of objects that are available in the UIKit and Foundation frameworks. One example in the previous chapter was a publisher for a URLSession data task: let myUrl = URL(string: "https://www.donnywals.com")! let publisher = URLSession.shared.dataTaskPublisher(for: myUrl) The publisher created by the preceding code is a URLSession.DataTaskPublisher. This publisher conforms to the Publisher protocol just like any other publisher. Its Output is (data: Data, response: URLResponse) and its Failure is URLError. This means that we’ll receive a tuple of (data: Data, response: URLResponse) when the request succeeds, and an error of type URLError if the request fails. Since a request only executes once, the publisher completes as soon as the request has either succeeded or failed. The following image shows a marble diagram that resembles a successful request. Donny Wals 22 Practical Combine Figure 2: A marble diagram that represents a network request Apple has also added a specialized publisher for NotificationCenter: let publisher = NotificationCenter.default.publisher(for: ,→ UIResponder.keyboardWillShowNotification) The publisher created in this code is a NotificationCenter.Publisher that has Notification as its Output and Never as its Failure. What’s interesting about this publisher when compared to URLSession.DataTaskPublisher is that it never actually completes. As long as an app is alive, the NotificationCenter.Publisher can emit values for the Notification that we are subscribed to. Because the publisher never completes, it also never errors which is why Never is the appropriate error for this publisher. I will show you how to use publishers in the context of a real app in later chapters. So far, I have shown you how to create instances of publishers. But only instantiating publishers isn’t very useful. Let’s see how you can receive, and react to values that are emitted by a Combine publisher. Subscribing to a Publisher’s stream Publishers in Combine emit values and we call this a stream. Objects that receive these values are called subscribers. A subscriber, as the name suggests, subscribes to the output of a publisher. Combine provides us with two very convenient general-purpose subscribers out of the box. I will show them one by one, starting with the sink subscriber: Donny Wals 23 Practical Combine [1, 2, 3].publisher.sink(receiveCompletion: { completion in print("publisher completed: \(completion)") }, receiveValue: { value in print("received a value: \(value)") }) The sink method that’s used in the preceding code is defined as an extension on Publisher. This means that we can use sink to subscribe to every possible publisher in Combine because all publishers must conform to the Publisher protocol. The sink method takes two closures. One is called for every value that is emitted by the publisher, and the other is called after the publisher has emitted its last value. If you were to paste the preceding code into a playground, you will find the following output: received a value: 1 received a value: 2 received a value: 3 publisher completed: finished The completion event that is sent to the receiveCompletion closure has Subscribers.Completion<Self.Failure> as its type. This event is very similar to Swift’s Result type, except its success case is simply .finished without an associated value. If you’d want to check for errors in the receiveCompletion closure, which you should, you could use a switch and the following code: [1, 2, 3].publisher.sink(receiveCompletion: { completion in switch completion { case .finished: print("finished succesfully") case .failure(let error): print(error) } }, receiveValue: { value in print("received a value: \(value)") }) Donny Wals 24 Practical Combine If you’ve been paying attention, you might wonder why we’d check for errors in the preceding code. This specific publisher never emits an error because its Failure type is Never! First off, kudos to you. That’s a sharp observation. And second, you’re right! In fact, sink comes with a special flavor for publishers that have Never as their Failure type. It allows us to omit the receiveCompletion closure: [1, 2, 3].publisher.sink(receiveValue: { value in print("received a value: \(value)") }) This shorthand version of sink only works for publishers that never fail. If your publisher can fail, you are required to handle the completion event using a receiveCompletion closure. The second very convenient built-in subscriber is assign. The assign subscriber is also defined on Publisher and it allows us to directly assign publisher values to a property on an object: var user = User() ["test@email.com"].publisher.assign(to: \.email, on: user) The preceding code creates a publisher that emits strings. We use assign to directly assign every published string from this publisher to the user object’s email property. This can be very convenient but it comes with some rules. The assign method requires that the key path that we want to assign values to is a ReferenceWriteableKeyPath. This pretty much means that the key path must belong to a class. For the preceding example, I used the following User class: class User { var email = "default" } So while the assign subscriber is very convenient, it’s not always feasible to use. Donny Wals 25 Practical Combine Understanding the lifecycle of a stream In Combine, a publisher that doesn’t have a subscriber will not emit any values. This means that publishers won’t perform any work unless they have a subscriber to send the result of their work to. This is a powerful feature of Combine, and it’s one of Combine’s core principles. In Combine, publishers only perform work, and they only publish values if there is a subscriber listening and if the subscriber is willing to receive values. Typically, you will not need to worry about this. Both sink and assign create subscribers that are always willing to receive values so the publishers they subscribe to will immediately perform work when needed, and they will emit values as often as needed. The principle of making the emission of values subscriber-driven is called backpressure management. If you want to learn more about this, you can skip ahead to Chapter 8 - Understanding Combine’s Schedulers. In that chapter you will learn about the finer details of publishers and subscribers, and how you implement backpressure management. For now, it’s important that in the lifecycle of a stream, publishers will only begin emitting values after a subscriber is attached to them. Both the sink and assign methods return a very important object that I have not mentioned yet. This object is an AnyCancellable and it’s a crucial object in the lifecycle of a subscriber. When an AnyCancellable is deallocated, the subscription that is associated with this object is torn down along with it. Let me demonstrate with an example: let myNotification = ,→ Notification.Name("com.donnywals.customNotification") func listenToNotifications() { NotificationCenter.default.publisher(for: myNotification) .sink(receiveValue: { notification in print("Received a notification!") }) NotificationCenter.default.post(Notification(name: myNotification)) } Donny Wals 26 Practical Combine listenToNotifications() NotificationCenter.default.post(Notification(name: myNotification)) The preceding code snippet creates a notification center publisher that listens for a specific notification. When a notification is received, a message is printed to the console. When you run this code in a playground, you will find that the "Received a notification!" text is only printed once. This happens because the first notification is posted inside of the listenToNotifications function. The AnyCancellable that is returned by sink still exists at that time. The second notification is posted outside of the listenToNotifications function. Even though it’s posted after the function runs, the notification is not received by the subscriber because the AnyCancellable is already torn down. You can fix this by holding on to the AnyCancellable outside of the function body. One way to do is by assigning the AnyCancellable to a property outside of the function scope: let myNotification = ,→ Notification.Name("com.donnywals.customNotification") var subscription: AnyCancellable? func listenToNotifications() { subscription = NotificationCenter.default.publisher(for: ,→ myNotification) .sink(receiveValue: { notification in print("Received a notification!") }) NotificationCenter.default.post(Notification(name: myNotification)) } The subscription property would normally be an instance variable on a class or struct. This means that the AnyCancellable can stick around for as long as the containing instance is around. While assigning to a property is fine for a single subscription, it might be less than ideal if you have multiple subscriptions that you want to hold on to. Luckily, Combine has a second way of storing AnyCancellable instances: Donny Wals 27 Practical Combine let myNotification = ,→ Notification.Name("com.donnywals.customNotification") var cancellables = Set<AnyCancellable>() func listenToNotifications() { NotificationCenter.default.publisher(for: myNotification) .sink(receiveValue: { notification in print("Received a notification!") }).store(in: &cancellables) NotificationCenter.default.post(Notification(name: myNotification)) } Every AnyCancellable has a store(in:) method. This method takes an inout parameter which means that this method can append an AnyCancellable to the Set that you pass it. In this case a set of AnyCancellable objects. When a publisher completes and you have a subscription (AnyCancellable) for that publisher stored in a set or property, the AnyCancellable is not deallocated automatically. Typically, you don’t need to worry about this. The publisher and subscription will usually do enough cleanup to prevent any major memory leaks, and the objects that hold on to the AnyCancellable objects are typically not around for the entire lifetime of your application. Regardless, it’s good to be aware of this and I would recommend that you keep an eye out for any potential memory problems related to persisted AnyCancellable instances even though in my experience you shouldn’t run into problems with them. In Summary In this chapter, you learned a lot more about Combine’s publishers, subscribing to them and also about the AnyCancellable that is created when you subscribe to a publisher. With this new knowledge that you have about Combine’s sink and assign methods, you should be able to subscribe to any publisher that is handed to you! One of the things that I find so satisfying about Combine is that its foundation is so simple. Donny Wals 28 Practical Combine Once you understand the principles that I have explained in this chapter, you already have a lot of knowledge about the set of rules that Combine operates in. Publishers send values over time to their subscribers. They can only complete (or error) once. When you subscribe to a publisher you can react to incoming values and the completion event using sink, or you can assign values directly to a key path. Every subscription can be wrapped in an AnyCancellable object that tears down its subscriber when it’s deallocated. There are some nuances to the summary I have just provided but don’t sweat those for now. You will discover and learn about the details as you move forward in this book. So far, I have mentioned transformations and operators a few times but I haven’t taken the time to explain them to you in-depth. The reason for that is simple; it’s what the next chapter is about! Donny Wals 29 Practical Combine Transforming publishers So far, you have learned that in Combine, all values are pushed to subscribers by publishers. In some cases, the values are exactly what you need, but in most cases, the value that you receive from a publisher is not quite what you need. Of course, you can manipulate a received value in your sink, and that might work well but it can also get messy real quick. Luckily, Combine comes with the ability to use operations like map to transform incoming values into something else before delivering them to a subscriber. And their power doesn’t stop there, we can even catch and handle errors, or limit the number of items we want to receive from a publisher. In this chapter, you will learn the following about transformations: • • • • Applying custom transformations to a publisher Understanding the differences between map, flatMap, and compactMap Applying transformations that might fail Defining custom transformations You won’t learn everything about all the different operators that Combine has to offer. Instead, I will explain how these transforming operators work in Combine so you have a solid understanding of them when we take a better look at them in some examples. By the end of this chapter, you should be able to look at an operator and have a rough understanding of what it does. Applying common transformations to a publisher Normally, when you subscribe to a publisher you receive its values immediately, as is. Consider the following example: let myLabel = UILabel() [1, 2, 3].publisher .sink(receiveValue: { int in Donny Wals 30 Practical Combine myLabel.text = "Current value: \(int)" }) In this code, we have a publisher that publishes integer values. In the sink, these integer values are converted to strings so the received integer can be displayed on a label. While there is nothing inherently wrong with this code, it’s common to avoid any processing or manipulation in subscribers if we can. Especially if the transformations are complex. In this case, we’re not doing much of a complex transformation, but it’s nice to improve this code either way. let myLabel = UILabel() [1, 2, 3] .publisher .map({ int in return "Current value: \(int)" }) .sink(receiveValue: { string in myLabel.text = string }) If you’ve worked with collections before in Swift, map probably looks familiar to you. Calling map on an array works much like calling map on a publisher. The only difference is that a publisher delivers its values over time, or asynchronously while an array can deliver them all at once. The output for calling map on an array is a new array. The output for calling map on a publisher is, in fact, a new publisher. Let’s look at a marble diagram: Donny Wals 31 Practical Combine Figure 3: A marble diagram that applies a map operator In Combine, marble diagrams are a common way to visualize how publishers change over time. They are typically read top to bottom and left to right, so in this case, we’re looking at a publisher that publishes values. Underneath this publisher, I wrote map to indicate that a map operation is applied to this publisher. The resulting publisher is shown underneath the map. It publishes the same number of values, except they have a different color to indicate that the value was transformed. Going back to the code, you can inspect the type of the publisher that is returned by map. The returned type is the following: Publishers.Sequence<[String], Never> This is quite convenient, we were able to turn a Publishers.Sequence<[Int], Never> into a Publishers.Sequence<[String], Never> without any trouble. Unfortunately, this isn’t always how map works. Let’s look at a brief example of a network request in Combine: Donny Wals 32 Practical Combine // URLSession.DataTaskPublisher let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for: someURL) ,→ // Publishers.Map<URLSession.DataTaskPublisher, Data> let mappedPublisher = dataTaskPublisher .map({ response in return response.data }) Notice that instead of having a single publisher, we are now looking at the initial data task publisher, which is wrapped in Publishers.Map. This is vastly different from what map did for the first publisher, so what’s going on here? Tip: If you want to learn more about networking in Combine, it’s explained in-depth with many examples in Chapter 6 - Using Combine for networking. The explanation is quite simple. Combine comes with a ton of built-in operators that are defined on the Publisher protocol. You can quickly find these operators in Apple’s documentation. If you read this page, you’ll find that towards the end of the overview it says the following: For example, the map(_:) operator returns an instance of Publishers.Map. This is clearly what we saw when mapping the data task publisher. But it’s not what happened when I showed you how to map over a sequence publisher. The reason it’s so different is that map has a default implementation that’s implemented as a protocol extension on the Publisher protocol. This means that specific publishers are free to provide overrides, which is exactly what Apple has done for Publishers.Sequence. You can verify this by looking at the documentation for Publishers.Sequence.map. Instead of returning an instance of Publishers.Map, this specific implementation of map returns a Publishers.Sequence<[T], Failure> where T is the type of the element we’re mapping to. Combine has many built-in publishers and you can find them all in Apple’s documentation for Publishers. There are several interesting publishers like Publishers.Scan, Donny Wals 33 Practical Combine Publishers.Drop, Publisher.Merge or even Publishers.Merge2, Publishers.Merge3, Publishers.Merge4 and so forth. Each of these publishers takes a publisher as input and transforms its output so it can be used as an output for that specific publisher. Sometimes publishers take a single publisher and they modify or ignore values from that publisher. Other times, a publisher might take several publishers, merging them all into a publisher that publishes values from all of the merged publishers. I will show you some of these publishers in action later in the chapter, and you will find plenty more in this book. A function like map that wraps a publisher into another publisher is called an operator, and I will try to refer to them as operators where possible. Alongside publishers and subscribers, operators are what makes Combine such a powerful framework. By chaining publishers and operators together, you can turn complex asynchronous code into a declarative list of operations that is much simpler to reason about than its procedural counterpart. When you look at the following code, I want you to focus on what you think it does. Don’t worry if you don’t understand how, or why, this code works. We’ll get to that soon: let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for: someURL) ,→ .retry(1) .map({ $0.data }) .decode(type: User.self, decoder: JSONDecoder()) .map({ $0.name }) .replaceError(with: "unknown") .assign(to: \.text, on: userNameLabel) The preceding code goes through the following steps. • • • • • • • Make a request to someURL. Retry the request once if it fails. Grab data from the network response. Decode the data into a model of type User. Grab the user’s name. Replace any errors that we’ve encountered along the way with the string “unknown”. Set the obtained string to a label’s text. Donny Wals 34 Practical Combine Don’t worry if you didn’t quite get that, you’ve just started learning Combine and the chain of operations above is quite sophisticated. If you take another look at the code and consider how you would implement this without using Combine, you’ll soon realize how much more complex the code would be. By chaining builtin operators together you can achieve extremely complicated results, simply by taking the output of a publisher, doing something with it, and outputting a new value. This is functional programming at its best. Understanding the differences between map, flatMap and compactMap In the previous sections, I have shown you a couple of examples that demonstrated the usage of map in Combine. If you’re familiar with Swift, you’ll know that in addition to map, it has compactMap and flatMap. Both of these additional mapping functions are present in Combine and they work in the same ways that their imperative counterparts that you know from Swift’s Sequence and Collection types. In this section, I will show you how and when it’s appropriate to use each of these operators and how they work. Using compactMap in Combine When you call compactMap on a Collection in Swift it works a lot like a normal map, except all nil results are filtered. Let’s look at an example: let result = ["one", "2", "three", "4", "5"].compactMap({ Int($0) }) print(result) // [2, 4, 5] In this example, every element in the initial array of strings is converted to an Int. This particular initializer for Int takes a String and returns either a valid Int or nil if the String couldn’t be converted to an Int. A normal map would preserve the nil values resulting in an array of Int?. Because we used compactMap, all nil results are dropped and we get an array of Int instead. Donny Wals 35 Practical Combine It’s possible to use compactMap on publishers in Combine. This works similar to how map works, and it applies the same rules that an imperative compactMap is built on. Let’s look at another example: ["one", "2", "three", "4", "5"].publisher .compactMap({ Int($0) }) .sink(receiveValue: { int in print(int) }) The sink in this code will only receive the values 2, 4 and 5. Because we transform the publisher’s output using compactMap, all nil values are dropped as expected. If you’d use a regular map instead of compactMap to transform the output of this publisher, you would end up with nil, 2, nil, 4 and lastly 5 being passed to the sink’s receiveValue closure. Using compactMap can save you some nil checks and guard statements if you want to make sure that you don’t receive any nil values in your subscriber. Keep in mind though that a nil value is dropped entirely. If you want to convert nil values to a default value, you can use a regular map, and apply the replaceNil operator on the resulting publisher: ["one", "2", "three", "4", "5"].publisher .map({ Int($0) }) .replaceNil(with: 0) .sink(receiveValue: { int in print(int) }) This has the benefit of filtering out nil values and replacing them with a non-nil value, without completely dropping any values. The type of the object that you receive in the sink is still Int? because replaceNil doesn’t change the output type of the publisher it’s applied to, so for good measure, you could automatically unwrap every value by applying compactMap to the output of replaceNil, but of course that’s entirely up to you: Donny Wals 36 Practical Combine ["one", "2", "three", "4", "5"].publisher .map({ Int($0) }) .replaceNil(with: 0) .compactMap({ $0 }) .sink(receiveValue: { int in print(int) // int is now a non-optional Int }) If you need to decide whether you should use replaceNil or compactMap, the decision should depend on two important factors: • Is it okay to drop nil values completely? You should probably use compactMap. • Can you replace nil with a sensible and valid default? Then replaceNil is likely a good decision. Having options like these available is what makes Combine into the powerhouse that it is. And interestingly, there are often multiple ways to achieve the same result. For example, in the earlier example, we could have used a map that returned Int($0) ?? 0 to ensure that we’d never return nil. This would eliminate the need for both compactMap and replaceNil. Each way of achieving a task has a different impact on the readability and performance of your code, and it’s always important to consider multiple options and if you think performance might be a factor, make sure to measure the impact that each possible choice has on your app. Using flatMap in Combine The last of the three mapping flavors that I wanted to show you is flatMap. When I was just becoming familiar with concepts like map and flatMap in Functional Programming, I was somewhat confused about this curious function. And let’s be honest, the short description for flatMap in the documentation isn’t fantastic: Returns an array containing the concatenated results of calling the given transformation with each element of this sequence. Luckily, we can find a somewhat better explanation in the Discussion section of the documentation: Donny Wals 37 Practical Combine Use this method to receive a single-level collection when your transformation produces a sequence or collection for each element. And to top it off, Apple provides an easy to follow example that I’ve decided copy and paste: let numbers = [1, 2, 3, 4] let mapped = numbers.map { Array(repeating: $0, count: $0) } // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]] let flatMapped = numbers.flatMap { Array(repeating: $0, count: $0) } // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] In this example code, an array of numbers is transformed with map and flatMap respectively. The transformation that is applied to each element turns the array of Int into an array of [Int], which means it’s an array that contains other arrays. In the regular map, we get back exactly that, an array of arrays. The flatMap example returns an array of Int. It has “flattened” the nested arrays to make sure we’d get back an array with one removed level of nesting. Using flatMap on an array is equivalent to using map and then calling joined() on the resulting collection. You can try that out for yourself if you’d like. So far we’ve been able to think of publishers in Combine as almost analogous to Swift’s collections. For flatMap, this same analogy holds. If we want to apply an operator to the output of a publisher that would transform that output into a new publisher, we’d have a publisher that publishes other publishers. Let’s look at an example: var baseURL = URL(string: "https://www.donnywals.com")! ["/", "/the-blog", "/speaking", "/newsletter"].publisher .map({ path in let url = baseURL.appendingPathComponent(path) return URLSession.shared.dataTaskPublisher(for: url) }) .sink(receiveCompletion: { completion in print("Completed with: \(completion)") Donny Wals 38 Practical Combine }, receiveValue: { result in print(result) }) This example uses a sequence publisher to publish several strings that point to pages on my website. Each string is used to create a URL, and this URL is then used to create a new DataTaskPublisher. The values that end up in the sink are not the results of the data tasks. Instead, the publishers themselves are delivered to the sink. This is isn’t particularly useful, and we can use flatMap to change this: var baseURL = URL(string: "https://www.donnywals.com")! ["/", "/the-blog", "/speaking", "/newsletter"].publisher .flatMap({ path -> URLSession.DataTaskPublisher in let url = baseURL.appendingPathComponent(path) return URLSession.shared.dataTaskPublisher(for: url) }) .sink(receiveCompletion: { completion in print("Completed with: \(completion)") }, receiveValue: { result in print(result) }) The code above doesn’t compile on iOS 13 but works fine for iOS 14. The sequence publisher’s error type is Never, and the DataTaskPublisher has URLError as its error type. When you use flatMap in Combine, the error type of the new publisher must match that of the source publisher (unless you’re on iOS 14 and the source publisher’s Failure type is Never). For iOS 13 we need to make sure that the publisher that we flatMap over has the same failure type as the publisher that we create in the flatMap. So that means that we either need to make sure that all failures from the data task that’s created in the flatMap are replaced with a default value to make its failure type Never, or we need to change the sequence publisher’s failure type to URLError. Since we shouldn’t hide any URLErrors that are emitted by the data tasks created in the flatMap above, we need to change the sequence publisher’s error type to match URLError. Donny Wals 39 Practical Combine We can do this with the setFailureType operator. This operator creates a new publisher with an unchanged Output, but it changes the Failure to the error type you supply. You would insert this operator before the flatMap operator and call it as follows for the example above: .setFailureType(to: URLError.self). This code compiles, but if you’d run it in a playground, nothing happens. That’s because this code runs asynchronously which means that the AnyCancellable that we get from the sink method is deallocated as soon as the current execution scope is exited, and the subscription stream is torn down like I explained in the previous chapter. To fix this, you need to hold on to the AnyCancellable like this: let baseURL = URL(string: "https://www.donnywals.com")! var cancellables = Set<AnyCancellable>() ["/", "/the-blog", "/speaking", "/newsletter"].publisher .setFailureType(to: URLError.self) // This is only needed on iOS 13 .flatMap({ path -> URLSession.DataTaskPublisher in let url = baseURL.appendingPathComponent(path) return URLSession.shared.dataTaskPublisher(for: url) }) .sink(receiveCompletion: { completion in print("Completed with: \(completion)") }, receiveValue: { result in print(result) }).store(in: &cancellables) If you run this code in a playground, you’ll find that the sink now receives the result of each data task. You’ll also find that receiveCompletion isn’t called until the last data task has finished. Neat, right? We used flatMap to transform a publisher that publishes string values into a publisher that publishes data task publishers, and we flattened this hierarchy with flatMap which made the intermediate data task publishers invisible to the sink! Refer to the following marble diagram to see what this process looks like when visualized. Donny Wals 40 Practical Combine Figure 4: A marble diagram that visualizes flatMap Donny Wals 41 Practical Combine In this case, we wanted to get all of the results from the data tasks. But what if we’re in a situation where that isn’t the case? Limiting the number of active publishers that are produced by flatMap So far I have shown you how you can use flatMap in a pretty immediate fashion. We’d receive values, change them into a new publisher and the nested publisher’s results were then delivered to the sink. There are times when this isn’t what you want. Consider a scenario where you make API requests based on something a user does. If the user would perform this action quick enough, and our code doesn’t handle this appropriately, we could end up with many simultaneous API calls being executed at the same time. In the previous chapter I briefly mentioned backpressure, and I will continue to mention this term throughout this book. I briefly explained that backpressure relates to how Combine allows subscribers to communicate how many values they wish to receive from the publisher they’re subscribed to. In other words, they can communicate how much input they want to receive from their upstream publisher. As it turns out, publishers can also limit the input they receive from their upstream publisher. The flatMap operator supports this through its maxPublisher argument. I would like to highly recommend that you open up a Playground in Xcode and run the following code: [1, 2, 3].publisher .print() .flatMap({ int in return Array(repeating: int, count: 2).publisher }) .sink(receiveValue: { value in print("got: \(value)") }) The following code should look familiar but there’s a new operator; print. With Combine’s print operator, you can take a look at what’s happening upstream from that print operator. Donny Wals 42 Practical Combine So in this case, we get to take a peek at what the sequence publisher does. This code would produce the following output: receive subscription: ([1, 2, 3]) request unlimited receive value: (1) got: 1 got: 1 receive value: (2) got: 2 got: 2 receive value: (3) got: 3 got: 3 receive finished I want to focus on the first two lines only: receive subscription: ([1, 2, 3]) request unlimited These two lines don’t look like much but they contain a ton of information. First, it tells us that the publisher receives a subscription at some point and that it then receives an unlimited request. What this means is that the publisher is asked to produce as many items as it wants, no limits. The publisher works in service of the subscriber so it immediately fulfills this request and it begins sending values. But what happens if we replace the flatMap with flatMap(maxPublishers:)? Let’s find out: [1, 2, 3].publisher .print() .flatMap(maxPublishers: .max(1), { int in return Array(repeating: int, count: 2).publisher }) .sink(receiveValue: { value in Donny Wals 43 Practical Combine print("got: \(value)") }) The code hasn’t changed much. The only difference is that the flatMap is passed a maxPublishers value of .max(1). We can supply any Int value for the maximum number of publishers. Alternative options are to pass .unlimited (the default), or .none which would mean that we never receive any values at all. When you examine the output of this code, you’ll find the following: receive subscription: ([1, 2, 3]) request max: (1) receive value: (1) got: 1 got: 1 request max: (1) receive value: (2) got: 2 got: 2 request max: (1) receive value: (3) got: 3 got: 3 request max: (1) receive finished Notice how instead of request unlimited, the output now shows request max: (1). This means that the flatMap has told the upstream publisher that it only wants to receive a single value. Once the value is received, flatMap will wait for the created publisher to complete before requesting a new value, again with max: (1). It continues to do this until the upstream publisher completes and sends a completion event. I will come back to Combine’s print operator and other debugging techniques in Chapter 10 - Debugging your Combine code. This example shows you backpressure management at its finest. It allows flatMap to manage the number of publishers that it produces. The upstream publisher can choose to either buffer Donny Wals 44 Practical Combine events while flatMap isn’t ready to receive them, or the publisher can decide to drop them. That’s an implementation detail of the publisher you’re working with. Throughout this book I will come back to backpressure management, flatMap and limiting the number of active publishers, but for now I think we should move on and explore some other operators. Don’t worry if you’re slightly confused about flatMap and backpressure management at this point. The flatMap operator is probably one of the more powerful and complex ones I have seen, especially because it integrates with backpressure so tightly. It will all become clear as we go along on our journey to learn Combine. Applying operators that might fail All the operators you’ve used so far didn’t allow you to throw errors from the operator. This means that every transformation you applied to the values that are emitted by a publisher has to succeed. Often this is perfectly fine, it’s not common to have transformations that might fail. This doesn’t mean that your transformation can’t ever fail, or that it’s bad if they do. Many of Combine’s built-in operators come with versions that are prefixed with the word try. For example tryMap, tryCompactMap, and others. Operators with a try prefix work identical to their regular counterparts with the only exception being that you can throw errors from the try versions. Let’s look at an example: enum MyError: Error { case outOfBounds } [1, 2, 3].publisher .tryMap({ int in guard int < 3 else { throw MyError.outOfBounds } Donny Wals 45 Practical Combine return int * 2 }) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { val in print(val) }) This example uses tryMap to map over a publisher that emits integer values. If we encounter an integer that isn’t smaller than three, that’s considered an error and an error is thrown. Keep in mind that publishers can only complete or emit an error once. This means that after an error is thrown, the publisher can’t emit new values. This is important to consider when you throw an error from an operator. Once the error is thrown, there is no going back. The example I just showed you might not be the best use case of a tryMap. Its purpose wasn’t to show you an elaborate use case of tryMap. Instead, I wanted to show you how you can use it, and give you something to play with. I’m sure that if you encounter a situation where throwing an error from an operator makes sense, you’ll know to look for the try prefixed operator you want to use. I do think that there is one very important detail to point out. When I showed you how to use flatMap in the previous section, you learned that on iOS 13 you have to use the setFailureType operator to change the sequence publisher’s Failure from Never to URLError. In the example I just showed you, we were able to change the error from Never to MyError without doing so explicitly. Even on iOS 13. The reason for this is that when an operator directly influences the error type like this example does, Combine can safely infer and change the error type that is exposed downstream to other operators or subscribers. Defining custom operators Combine has loads of built-in operators but there will be cases where the built-in operators don’t match what you need. This typically happens if you have repetitive, or long code inside of a single operator. When this happens, defining an operator of your own might help to make Donny Wals 46 Practical Combine your code more readable, and easier to reason about. Let’s take another look at some code I’ve shown you earlier in this chapter: var baseURL = URL(string: "https://www.donnywals.com")! ["/", "/the-blog", "/speaking", "/newsletter"].publisher .setFailureType(to: URLError.self) // This is only needed on iOS 13 .flatMap({ path -> URLSession.DataTaskPublisher in let url = baseURL.appendingPathComponent(path) return URLSession.shared.dataTaskPublisher(for: url) }) .sink(receiveCompletion: { completion in print("Completed with: \(completion)") }, receiveValue: { result in print(result) }).store(in: &cancellables) This code applies two operators to a publisher that emits strings. One to set the publisher’s failure type to URLError, and one to convert the string to a data task publisher. This code isn’t necessarily hard to read, but the two operators we apply in this example are coupled pretty tightly. And you’re also writing code that’s only needed on iOS 13 where it’s not strictly needed. While it’s not a performance issue or anything, it would be nice to clean up the code a bit and combine the setFailureType and flatMap operators into a single operator. This makes the code shorter, easier to read and easier to maintain because you can hide the iOS 13 specific operator from the rest of your code. Let’s see how this can be done: extension Publisher where Output == String, Failure == Never { func toURLSessionDataTask(baseURL: URL) -> ,→ AnyPublisher<URLSession.DataTaskPublisher.Output, URLError> { if #available(iOS 14, *) { return self .flatMap({ path -> URLSession.DataTaskPublisher in let url = baseURL.appendingPathComponent(path) return URLSession.shared.dataTaskPublisher(for: url) }) Donny Wals 47 Practical Combine .eraseToAnyPublisher() } else { return self .setFailureType(to: URLError.self) .flatMap({ path -> URLSession.DataTaskPublisher in let url = baseURL.appendingPathComponent(path) return URLSession.shared.dataTaskPublisher(for: url) }) .eraseToAnyPublisher() } } } Tip: I’m using Swift’s #available to check if iOS 14 or newer is available. If it is, I know that I don’t need the .setFailureType(to:) operator. If iOS 14 or newer is not available this means that we need to apply setFailureType(to:). All operators in Combine are defined as an extension on Publisher. These extensions can be constrained to ensure that a publisher has a certain output or failure type. In this case, I constrained the extension to publishers that have String as their output, and Never as their error. This matches the output and error of the string publisher from the code we’re refactoring. At its core, this custom operator applies the same two operators that were applied in the original code. Note that a third operator is applied; eraseToAnyPublisher(). This operator removes all type information from the publisher and wraps it in an AnyPublisher. This is a good thing because the publisher that we end up with after applying flatMap is Publishers.FlatMap<P, Publishers.SetFailureType<Self, URLError>>. This isn’t the most readable and useful return type. It’s also an implementation detail of our operator. By erasing this implementation detail, we can return AnyPublisher<URLSession.DataTaskPublisher.Output, URLError>. This tells users of our custom operator everything they need to know. This custom operator can be used as follows: var baseURL = URL(string: "https://www.donnywals.com")! Donny Wals 48 Practical Combine ["/", "/the-blog", "/speaking", "/newsletter"].publisher .toURLSessionDataTask(baseURL: baseURL) .sink(receiveCompletion: { completion in print("Completed with: \(completion)") }, receiveValue: { result in print(result) }).store(in: &cancellables) The code is a little bit shorter, but it’s also easier to read. Readers of this code will understand that every string that is published by the publisher is converted to a data task. While you can shorten code and make it more readable with custom operators, doing so comes with some cost. People that are familiar with Combine but are new to your codebase won’t be familiar with your custom operators. This might introduce unneeded complexity and friction for developers on your team. On the other hand, a couple of well-defined and well-documented custom operators can be a huge asset for your codebase. Whenever you’re about to introduce a custom operator, consider the implications and ask yourself whether the pattern you’re abstracting is common enough to warrant a custom operator. In Summary In this chapter I wanted to show what operators in Combine are, and how you can use them. I started by giving you a high-level overview, and we went more in-depth later. You learned how you can transform a publisher’s output with operators like map, compactMap and flatMap. You saw how these operators work, what their similarities are and what their differences are. Along the way, I introduced you to several other operators like setFailureType, replaceNil and replaceError. You saw that all operators in Combine work in a similar manner. They take a publisher’s output and/or error and return a new publisher with a modified output and/or error. You also learned that operators don’t typically throw errors and that most Combine operators come with a separate version that has a try prefix that allows you to throw errors when Donny Wals 49 Practical Combine appropriate. When using these operators, keep in mind that a publisher chain can only emit an error once. When a publisher emits an error, the stream is considered completed and it can’t emit any new values. Lastly, I showed you how you can define custom operators by extending Publisher and constraining this extension as needed. It’s not very common to need or define custom operators, but it’s good to know how to do it because a well-placed custom operator can dramatically improve a codebase. This chapter wraps up the introductory, theoretical part of this book. You now know about all of Combine’s basic building blocks. In Chapter 1 - Introducing Combine you learned what Combine and Functional Reactive Programming are. In Chapter 2 - Exploring publishers and subscribers I explained what publishers are, how they work, and how you can subscribe to them using sink and assign(to:on:). Now that you learned about operators, we’re ready to move on to the fun part, seeing how Combine fits in your existing projects, and how you can gradually introduce Combine into your toolbox! Donny Wals 50 Practical Combine Updating the User Interface In the first section of this book, I focussed on teaching you the fundamental basics that you need to know and understand to use Combine effectively in your apps. In this chapter and the chapters to come, you will see how Combine integrates with applications using patterns and principles that you are likely to be familiar with. In this chapter, we’ll get started with one of the most important aspects of almost every application, the user interface. I know that SwiftUI is the new and modern way to build user interfaces on iOS 13 and onward, but I will focus on using Combine with UIKit rather than SwiftUI in this chapter. The reason you’ll learn Combine with UIKit instead of SwiftUI is that SwiftUI is so tightly integrated with Combine that it can be really difficult to understand where Combine ends and SwiftUI begins. By teaching you how to use Combine in a UIKit or UI framework agnostic environment I hope to show you exactly what Combine does for your UI, and what it’s good at. If you don’t have any UIKit experience and went all-in on SwiftUI, I’m sure that you will be able to follow along. A lot of the code that you’ll encounter is abstracted in such a way that it only interfaces with UIKit where needed. And most importantly, I will spare the complications of building full apps. Instead, you’ll see small and self-contained examples that you can integrate with your projects. In this chapter, we’re going to get started with the following topics: • • • • • Creating publishers for your models and data. Directly assigning the output of a publisher with assign(to:on:). Using Combine to drive Collection Views. Assigning the output of a publisher to an @Published property with assign(to:). Creating a simple theming system with Combine. Before we get started I should mention that I will use a simple and loose version of MVVM to demonstrate most principles of using Combine to update your user interface. This doesn’t mean that it’s the only way to effectively use Combine. It’s just a way to show you how you can separate code in lightweight objects, and everything you’ll learn can be adapted to any other architecture you might want to use with relative ease. Donny Wals 51 Practical Combine Creating publishers for your models and data In the first couple of chapters of this book, I used a handful of publishers to explain the basics of Combine to you. I made extensive use of the sequence publisher in particular because it’s a nice way of demonstrating how Combine works with a stream of strings, integers or other values. And while it’s nice for that purpose, it’s not particularly useful in a lot of real-world scenarios. In this section, I will show you how you can create publishers that wrap values from your models so you can expose them to your user interface and other parts of your app. There are three techniques that you’ll learn in this section: • Using a PassthroughSubject to send a stream of values. • Using a CurrentValueSubject to represent a stateful stream of values. • Wrapping properties with the @Published property wrapper to turn them into publishers. Each of these techniques serves a different purpose in Combine, and I will go over these techniques one by one. Let’s start with the PassthroughSubject. Using a PassthroughSubject to send a stream of values Combine’s PassthroughSubject is one of two Subject publishers in the framework. A subject in Combine is a special kind of publisher that allows the developer, that’s you, to inject values into its stream. In other words, a subject allows you to determine which values are published, and more importantly, when. Combine subjects are especially useful when your code is (partially) written in an imperative style. This is often the case for existing applications, or for code that is close to the user interface. Subjects in Combine have a send(_:) method that allows you to send values down the publisher’s stream of values. It’s also possible to use send(completion:) to complete a stream of values if needed. The purpose of a PassthroughSubject in Combine is to send a stream of values from their origin, through the publisher, to its subscribers. The origin of the value stream is often Donny Wals 52 Practical Combine existing, imperative code that doesn’t hold a state. This makes a PassthroughSubject a good fit for publishing values that represent ephemeral data, like events. For example, in a game, you might use a PassthroughSubject to communicate that a user has collected an item, or that they completed a level. The PassthroughSubject’s subscribers can then use this information to update the state of the program or to handle the occurred event. If you’ve built iOS applications before, you might be familiar with NotificationCenter. The NotficationCenter in iOS is an object that broadcasts events throughout your application to all interested objects. These events range from application lifecycle events to events that inform you when the keyboard is about to appear or disappear. Apple has created a built-in publisher for NotificationCenter that can be used as follows: var cancellables = Set<AnyCancellable>() let notificationCenter = NotificationCenter.default let notificationName = UIResponder.keyboardWillShowNotification let publisher = notificationCenter.publisher(for: notificationName) publisher .sink(receiveValue: { notification in print(notification) }).store(in: &cancellables) notificationCenter.post(Notification(name: notificationName)) If you run this code in a Playground, you’ll find that as soon as you post a notification to the notification center, you receive this value in the sink. The type of publisher that is created by calling publisher(_:) on NotificationCenter is a NotificationCenter.Publisher. However, it fits my description of a PassthroughSubject really well because it displays the same behavior as I describe for PassthroughSubject. Let’s reimplement NotificationCenter.Publisher using a PassthroughSubject to see just how similar they are. Donny Wals 53 Practical Combine var cancellables = Set<AnyCancellable>() let notificationSubject = PassthroughSubject<Notification, Never>() let notificationName = UIResponder.keyboardWillShowNotification let notificationCenter = NotificationCenter.default notificationCenter.addObserver(forName: notificationName, object: nil, ,→ queue: nil) { notification in notificationSubject.send(notification) } notificationSubject .sink(receiveValue: { notification in print(notification) }).store(in: &cancellables) notificationCenter.post(Notification(name: notificationName)) This example subscribes to the NotificationCenter using the old addObserver(forName:object: method to subscribe to a notification using a closure that is executed whenever a specific notification is dispatched by the NotificationCenter. The code also defines a PassthroughSubject<Notification, Never>. This is the publisher that relays the notifications generated by NotificationCenter to its subscribers. Every time the notification that we subscribed to is posted by the notification center, the line notificationSubject.send(notification) is executed. This sends the received notification directly to the PassthroughSubject which will deliver it to its subscribers immediately. Note that the PassthroughSubject does not hold on to any of the values that it has sent in the past. All it does is accept the value that you want to send, in this case, a notification, and that value is immediately sent to all subscribers and discarded afterward. If you do need to have a sense of state for a property, like when you have a model with mutable values, you need the second type of Subject publisher that’s provided by Combine, the CurrentValueSubject. Donny Wals 54 Practical Combine Using a CurrentValueSubject to represent a stateful stream of values A CurrentValueSubject in Combine looks and feels very similar to a PassthroughSubject. This is because they both conform to the Subject protocol. However, don’t be fooled by their similarities because they serve very different purposes. Consider the following code, that you might have written or seen at some point in the past: class Car { var onBatteryChargeChanged: ((Double) -> Void)? var kwhInBattery = 50.0 { didSet { onBatteryChargeChanged?(kwhInBattery) } } let kwhPerKilometer = 0.14 func drive(kilometers: Double) { let kwhNeeded = kilometers * kwhPerKilometer assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough ,→ charge in battery") kwhInBattery -= kwhNeeded } } The preceding code is relatively simple, and the part that I want you to focus on is the didSet. Whenever the car’s kwhInBattery is updated, an optional closure is called. This closure can be set by the owner of this car model as follows: let car = Car() someLabel.text = "The car now has \(car.kwhInBattery)kwh in its ,→ battery" Donny Wals 55 Practical Combine car.onBatteryChargeChanged = { newCharge in someLabel.text = "The car now has \(newCharge)kwh in its battery" } If you were to implement a pattern like this in your app, your someLabel would automatically be updated every time kwhInBattery changes. We can implement a very similar pattern using Combine’s CurrentValueSubject, and the code would be a bit cleaner too. Let’s see how: class Car { var kwhInBattery = CurrentValueSubject<Double, Never>(50.0) let kwhPerKilometer = 0.14 func drive(kilometers: Double) { let kwhNeeded = kilometers * kwhPerKilometer assert(kwhNeeded <= kwhInBattery.value, "Can't make trip, not ,→ enough charge in battery") kwhInBattery.value -= kwhNeeded } } let car = Car() // don't forget to store the AnyCancellable if you're using this in a real app ,→ car.kwhInBattery .sink(receiveValue: { newCharge in someLabel.text = "The car now has \(newCharge)kwh in its battery" }) In this new version of Car, kwhInBattery is no longer Double. Instead, it’s a CurrentValueSubject with an output of Double and a failure type of Never. Notice that in the Donny Wals 56 Practical Combine drive(kilometers:) method, I had to change the code a little bit. Instead of accessing kwhInBattery directly, its current value is accessed by reading the subject’s value property. A CurrentValueSubject holds state which means that we can read its current value through the value property. This is something we can’t do with a PassthroughSubject. Notice that there is no explicit call to send(_:) in this example. When you change a CurrentValueSubject’s value property, it automatically sends this new value downstream to its subscribers. We don’t need to do this ourselves. Also, note that we don’t read the initial value of kwhInBattery to configure the label. That’s not a mistake. A CurrentValueSubject immediately sends its current value to any new subscribers. And because a CurrentValueSubject must always be initialized with an initial value, it will always have an initial value to send to its subscribers. So in the example I just showed you, your label should display the battery’s initial charge which is 50. If you call drive after subscribing to kwhInBattery, you will see that the label updates immediately to show the battery’s new charge: // Don't forget to store your cancellable when you're not running this in a Playground! ,→ car.kwhInBattery .sink(receiveValue: { newCharge in someLabel.text = "The car now has \(newCharge)kWh in its battery" ,→ }) car.drive(kilometers: 100) // label will now show that there's 36kwh ,→ remaining As you’ll see throughout this chapter, CurrentValueSubject is a fantastic fit to expose a model’s properties through a reactive interface. There is one more very special way to publish property values in Combine by using the @Published property wrapper. Donny Wals 57 Practical Combine Wrapping properties with the @Published property wrapper to turn them into publishers Those familiar with SwiftUI have likely encountered the @Published property wrapper, and you may have considered it to be a part of SwiftUI. In reality, @Published is a Combine construct. You can use the @Published property wrapper for similar purposes as CurrentValueSubject with some minor differences. Before I cover these differences, I would like to show you how to use @Published by refactoring the car example one last time: class Car { @Published var kwhInBattery = 50.0 let kwhPerKilometer = 0.14 func drive(kilometers: Double) { let kwhNeeded = kilometers * kwhPerKilometer assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough ,→ charge in battery") kwhInBattery -= kwhNeeded } } let car = Car() // don't forget to store the AnyCancellable if you're using this in a real app ,→ car.$kwhInBattery .sink(receiveValue: { newCharge in someLabel.text = "The car now has \(newCharge)kwh in its battery" }) When you use @Published, you can access and modify the value of kwhInBattery directly. This is really convenient and makes the code look nice and clean. However, because kwhInBattery now refers to the underlying Double value, we can’t subscribe to it directly. Donny Wals 58 Practical Combine To subscribe to a @Published property’s changes, you need to use a $ prefix on the property name. This is a special convention for property wrappers that allows you to access the wrapper itself, also known as a projected value rather than the value that is wrapped by the property. In this case, the wrapper’s projected value is a publisher, so we can subscribe to the $kwhInBattery property. Like I mentioned earlier, properties that are annotated with the @Published property wrapper mostly exhibit the same behaviors as CurrentValueSubject from the perspective of subscribers. The main difference is that a @Published value will update its underlying value after emitting the value to its subscribers, the CurrentValueSubject will update its value before emitting the value to its subscribers. The following example does a nice job of demonstrating this difference: class Counter { @Published var publishedValue = 1 var subjectValue = CurrentValueSubject<Int, Never>(1) } let c = Counter() c.$publishedValue.sink(receiveValue: { int in print("published", int == c.publishedValue) }) c.subjectValue.sink(receiveValue: { int in print("subject", int == c.subjectValue.value) }) c.publishedValue = 2 c.subjectValue.value = 2 The output of this code would be the following: published, true subject, true published, false subject, true Donny Wals 59 Practical Combine There is also a limitation when using @Published though. You can only use this property wrapper on properties of classes while CurrentValueSubject is available for both structs and classes. In addition to this limitation, it’s also not possible to call send on a @Published property because it’s not a Subject. In practice, this means that you can’t emit a completion event for @Published properties like you can for a Subject. Assigning a new value to the @Published property automatically emits this new value to subscribers which is equivalent to calling send(_:) with a new value. Choosing the appropriate mechanism to publish information Which publishing mechanism is best for you depends on several factors. If you’re publishing a stream of events without a concept of state, you’re probably looking for a PassthroughSubject. These are not commonly found on models, and they are better suited for publishing a stream of user interactions, or as I’ve shown, an event stream like NotificationCenter emits. If you have a model that’s defined as a struct, you can expose new data through a CurrentValueSubject. This allows you to continuously send new values to subscribers while keeping a concept of the current state, or value. And lastly, if you have a class and you want to have similar behavior to a CurrentValueSubject except for not being to end the stream of values if needed, the @Published property wrapper is likely to fit your needs perfectly. Throughout this book, I will use all three of these mechanisms so you can see how they can be used, and what their usage looks like. Directly assigning the output of a publisher with assign(to:on:) In the previous section, I’ve shown you the following code: Donny Wals 60 Practical Combine class Car { @Published var kwhInBattery = 50.0 let kwhPerKilometer = 0.14 func drive(kilometers: Double) { let kwhNeeded = kilometers * kwhPerKilometer assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough ,→ charge in battery") kwhInBattery -= kwhNeeded } } let car = Car() // don't forget to store the AnyCancellable if you're using this in a real app ,→ car.$kwhInBattery .sink(receiveValue: { newCharge in someLabel.text = "The car now has \(newCharge)kwh in its battery" }) The text property of someLabel in this example is always set to a new string that reflects the car’s current battery status. Let’s expand this example a little bit and introduce a view controller and a view model: class Car { @Published var kwhInBattery = 50.0 let kwhPerKilometer = 0.14 } struct CarViewModel { var car: Car mutating func drive(kilometers: Double) { Donny Wals 61 Practical Combine let kwhNeeded = kilometers * car.kwhPerKilometer assert(kwhNeeded <= car.kwhInBattery, "Can't make trip, not ,→ enough charge in battery") car.kwhInBattery -= kwhNeeded } } class CarStatusViewController { let label = UILabel() let button = UIButton() var viewModel: CarViewModel var cancellables = Set<AnyCancellable>() init(viewModel: CarViewModel) { self.viewModel = viewModel } // setup code goes here func setupLabel() { // label setup will go here } func buttonTapped() { viewModel.drive(kilometers: 10) } } I have left out some repetitive and irrelevant code here, like the initializers for CarStatusViewController and the code needed to add a tap handler to the button in the view controller. Either way, I’m sure the code above shouldn’t hold too many surprises for you. Regardless of whether you’re familiar with MVVM, and regardless of whether you think this is a good example of MVVM. The main reason I’ve used a view model in this example is to add Donny Wals 62 Practical Combine a layer between the Car which holds the @Published property, and the label that will ultimately show the amount of battery power that’s left in the car’s battery. Because we ultimately want to have the view model in this code prepare a string for the label, the view model will need to both subscribe to the car’s @Published kwhInBattery, and expose a new publisher to send formatted strings to the label. We can do this by adding the following property to the view model: lazy var batterySubject: AnyPublisher<String?, Never> = { return car.$kwhInBattery.map({ newCharge in return "The car now has \(newCharge)kwh in its battery" }).eraseToAnyPublisher() }() This code defines a lazy property. This means that it will not be initialized until the first time it is read. Notice that the property’s value is wrapped in { ... }(). This means that we want to initialize this property with the result of the code between the { and }. The reason batterySubject is lazy is due to the fact that we need to access the car property to create the batterySubject. If we don’t make batterySubject lazy the code wouldn’t compile because self would not be initialized when batterySubject would be initialized. The batterySubject is an AnyPublisher<String?, Never>. We create this publisher by grabbing the car’s $kwhInBattery publisher, mapping over it to convert the current charge into a string, and erasing the result to AnyPublisher to ensure we have a nice and clean type for this publisher. The reason the publisher’s value type is String? and not String will become clear in a moment. Now that we have a property to subscribe to in the view model, it’s time to subscribe to the view model’s publisher in the view controller. To do this, we need to update the view controller’s empty setupLabel() method from the earlier code snippet: func setupLabel() { viewModel.batterySubject .assign(to: \.text, on: label) .store(in: &cancellables) } Donny Wals 63 Practical Combine Notice how clean this code is. By using a new subscriber that I haven’t talked about before, we don’t need to provide any closures or do any processing on the view model’s output. Instead, the values that are published by batterySubject are assigned to the label’s text property immediately. To do this, the label’s text and the batterySubject output must match. Since text is a String?, we needed to use String? as the output type for batterySubject. I hope that this is a limitation in Combine that could be lifted in future releases, but as of now there isn’t much else we can do. The assign(to:on:) operator in Combine is similar to sink. It creates a new Subscriber, and it returns an AnyCancellable that we must retain to keep the subscription alive. When you use assign(to:on), you don’t provide any closures or do any extra processing at all. Instead, all published values are assigned to the keypath that you pass as to:, on the object that you pass as on:. Note that currently there is an issue with assigning to keypaths on self. Doing this will cause a retain cycle to occur. You can read more about this issue, and whether it is resolved, on the Swift forums. Now that you’ve seen how you can take a published value from a model, modify it in a view model and assign it to a label’s text using assign(to:on:), you should be starting to see how Combine can help you move your code from being the old, imperative way to the new, reactive way. There are many ways to achieve your goals, and what I’ve demonstrated in this section is far from the only way to implement reactive programming and MVVM, but it’s a way that works for me, and it’s simple enough to explain and follow without letting it get in the way of our main goal, which is to learn Combine. Using Combine to drive Collection Views Tons of apps on the App Store make use of table views and collection views, so I think it’s absolutely key to cover these components in the context of Combine. I will focus on collection views in this section, and due to the similarities between table views and collection views, I won’t cover table views separately. All principles in this section should apply to both. The example code will not show you how to set up a collection view from scratch. I’m going to Donny Wals 64 Practical Combine assume you have a fair knowledge of collection views, and that you know how to set up a collection view layout. Possible with a compositional layout, which I’ve covered in one of my blog posts. I will also be using iOS 13’s diffable data source, which you can learn more about on my blog. I will explain the most important bits as I touch them in this post, but since this is a book about Combine, I will focus on the Combine part of building collection views. This section is split up in two parts: • Using Combine to update a collection view’s data source • Driving collection view cells with Combine Using Combine to update a collection view’s data source There are several ways to drive collection view data sources with Combine. However, no matter what kind of design you come up with, the basic principle will always be the same. When the collection view’s underlying data changes, you want to update the collection view so it displays this new data, which is achieved by applying a new snapshot to the diffable data source. Because of this, I’m not going to focus on implementing an extremely clever mechanism that tells you which sections or items have changed. That’s what the collection view’s diffable data source is for! Instead, I will show you a simple yet effective setup that will get you up and running with a reactive collection view in no-time. First, let’s explore how data can be sent from a data provider to the view controller where it will be assigned to the collection view: struct CardModel: Hashable, Decodable { let title: String let subTitle: String let imageName: String } class DataProvider { let dataSubject = CurrentValueSubject<[CardModel], Never>([]) func fetch() { let cards = (0..<20).map { i in Donny Wals 65 Practical Combine CardModel(title: "Title \(i)", subTitle: "Subtitle \(i)", ,→ imageName: "image_\(i)") } dataSubject.value = cards } } This code is relatively simple, and shouldn’t contain anything you don’t already know. The principle here is that we have a property called dataSubject. This is a CurrentValueSubject which allows other objects to subscribe to this subject and obtain the currently fetched data. In this example, I assumed that we’re going to build something where every time we fetch data, the new data replaces the existing data. By decoupling the fetch() logic and the dataSubject like this, we can have multiple objects that subscribe to dataSubject, and when any object calls fetch(), all objects that subscribe to dataSubject automatically receive the updated data. An alternative design could have been the following: class DataProvider { func fetch() -> AnyPublisher<[CardModel], Never> { let cards = (0..<20).map { i in CardModel(title: "Title \(i)", subTitle: "Subtitle \(i)", ,→ imageName: "image_\(i)") } return Just(cards).eraseToAnyPublisher() } } In this alternative example, the fetch() method would return a publisher. Because this example executes synchronously and doesn’t make any network calls, this is pretty simple. I generate data, and I return Just(cards).eraseToAnyPublisher(). Both approaches of implementing DataProvider are perfectly fine, and the right choice for your app depends on your use case. If you only want to fetch data once, without appending new data to previous data sets or updating other objects that might be interested in the Donny Wals 66 Practical Combine same new data, then the second approach is perfect for you. If you’re building something where data should be broadcast to one or more interested subscribers, and where the fetched data should remain available at any time, then a CurrentValueSubject is for you. A CurrentValueSubject is also very convenient if you’re building a collection view that needs to implement infinite scrolling. Before I show you how to update a collection view, I want to quickly show you an example of what a simple, naive implementation of DataProvider looks like if it supported an infinitely scrolling collection view: class DataProvider { let dataSubject = CurrentValueSubject<[CardModel], Never>([]) var currentPage = 0 var cancellables = Set<AnyCancellable>() func fetchNextPage() { let url = URL(string: ,→ "https://myserver.com/page/\(currentPage)")! currentPage += 1 URLSession.shared.dataTaskPublisher(for: url) .sink(receiveCompletion: { _ in // handle completion }, receiveValue: { [weak self] value in guard let self = self else { return } let jsonDecoder = JSONDecoder() if let models = try? jsonDecoder.decode([CardModel].self, ,→ from: value.data) { self.dataSubject.value += models } }).store(in: &cancellables) } } There is a better way to write this code that I’ll cover in Chapter 12 - Driving publishers and flows with subjects. For now, it’s important that you understand what this code does, Donny Wals 67 Practical Combine and why it works. Every time fetchNextPage() is called, we increment the current page counter, and a URL is created. This URL points to the page of data that we want to fetch from a server. Most web APIs that provide pagination functionality will require you to provide a page number so it knows which set of data to return. In this code, a URLSession.DataTaskPublisher is used to fetch the required data, which is then decoded into an array of models and this array of models is then appended to the existing data in the dataSubject. Every time fetchNextPage() is called and it managed to retrieve data successfully, all subscribers of dataSubject automatically receive the latest state of the complete data set. Cool stuff, right! In the following code, I’m going to assume that we’re using the very first DataProvider implementation I’ve shown you. It’s the one that generates data and then assigns the result to a CurrentValueSubject. I’m going to assume that you have a diffable data source set up already. If you’re not sure how to do this, refer to the blog post I mentioned earlier in this section. Let’s dive right in, and look at some code: override func viewDidLoad() { dataProvider.fetch() dataProvider.dataSubject .sink(receiveValue: self.applySnapshot) .store(in: &cancellables) } func applySnapshot(_ models: [CardModel]) { var snapshot = NSDiffableDataSourceSnapshot<Int, CardModel>() snapshot.appendSections([0]) snapshot.appendItems(models) datasource.apply(snapshot) } If you already have a datasource property set up for your collection view, this is enough code to fetch data and update your collection view using a diffable data source. Donny Wals 68 Practical Combine Note that I call fetch() before subscribing to dataProvider.dataSubject. The reason I do this could be considered a cheat. For some reason, diffable data sources don’t like being updated rapidly. If you would subscribe to dataProvider.dataSubject before calling fetch(), you’d immediately get an empty array as a current value, and then as soon as fetch() is called, you’d get a populated array which wouldn’t be picked up by the diffable data source. So to work around that in this example, I’ve placed fetch() before my subscription. This is only a problem because my example here doesn’t make any network calls. In an application that fetches data from the network, you wouldn’t see this problem because it would take longer for data to become available and the data source would not be updated as rapidly. There is a very neat way to prevent publishers from emitting values too rapidly, you’ll learn about that technique in the next chapter when I show you how to handle user input with Combine. By passing applySnapshot(_:) to the sink(receiveValue:) method, we make sure that a new snapshot for the diffable data source is generated every time that new model data is available. And when we call apply(_:) to activate the snapshot, all the complicated diffing is done by the diffable data source. This makes our job a heck of a lot easier than you may have expected. What’s nice about this approach is that you wouldn’t have to change a thing if you’ want to implement a paginated version of the DataProvider, and if you’d change to the version where fetch() returned an AnyPublisher, all you’d need to do is subscribe to the result of fetch() instead of the dataSubject. Driving collection view cells with Combine A slightly more complicated concept is using Combine to drive collection view cells. Of course, it depends on how far you want to take Combine for your cells, and before you blindly use Combine to update all of your cells, you have to wonder whether your cells contain data that will change dynamically and over time. In my experience, the models that drive collection view cells don’t change much with one huge exception. When your cells display images from the web, there’s a very good chance that you will load those images asynchronously, and your cell will need to update once the image is loaded. You can achieve functionality like this Donny Wals 69 Practical Combine without Combine just fine. I’m going to point you to yet another blog post that I wrote where I explain how you can asynchronously load images for table views and collection views. In this section, I will show you a mechanism where Combine is used to retrieve and set images on a collection view cell. We’ll start with an update to the DataProvider to have it create URLSession.DataTaskPublisher instances that can be used to retrieve images: func fetchImage(named imageName: String) -> AnyPublisher<UIImage?, ,→ URLError> { let url = URL(string: "https://imageserver.com/\(imageName)")! return URLSession.shared.dataTaskPublisher(for: url) .map { result in return UIImage(data: result.data) } .eraseToAnyPublisher() } This method is pretty straightforward. It creates a data task publisher and tries to transform a successful response into a UIImage which will be displayed on a collection view cell’s image view. To use this method effectively, we’re going to need to make an important decision. Where should fetchImage(named:) be called from, who should subscribe to it, and where should the returned Cancellable be stored. Deciding where the Cancellable should be stored is somewhat of a no-brainer. If we don’t store the AnyCancellable that is returned by sink or assign anywhere, the subscription stream is torn down and we never receive a result. This kind of cancellation needs to be done every time a collection view cell is reused because a cell should only be subscribed to the publisher that is fetching the image for its current model. What this means is that if a collection view cell has a variable that can hold on to a Cancellable, we can set that cancellable to the image publisher for the current model, which will automatically deallocate and tear down any previous subscriptions. Let’s look at a collection view cell with such a variable. Donny Wals 70 Practical Combine class CardCell: UICollectionViewCell { var cancellable: Cancellable? let imageView = UIImageView() // all other code is omitted for brevity } This example cell has an image and a cancellable property. Note that I used Cancellable and not AnyCancellable as its type. I like to write my code as flexible as reasonably possible, and since AnyCancellable conforms to the Cancellable protocol, I figured this would be a good choice. Now let’s see how we’d tie the DataFetcher to the CardCell and update its imageView: let datasource = UICollectionViewDiffableDataSource<Int, ,→ CardModel>.init(collectionView: collectionView) { collectionView, ,→ indexPath, item in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CardCell.identifier, for: indexPath) as! CardCell ,→ cell.cancellable = self.dataProvider.fetchImage(named: ,→ item.imageName) .sink(receiveCompletion: { completion in // handle errors if needed }, receiveValue: { image in DispatchQueue.main.async { cell.imageView.image = image } }) return cell } By calling fetchImage(named:) in the diffable data source’s cell provider and subscribing to it right there, we can assign the returned AnyCancellable to the cell’s cancellable Donny Wals 71 Practical Combine property. We can also assign the fetched image to the imageView here. And because the subscription stream is torn down as soon as a new AnyCancellable is assigned to the cell’s cancellable property, we don’t have to worry about receiving old images, or receiving images out of order. All we need to do is make sure to set the cell’s imageView.image to nil in the cell’s prepareForReuse method to prevent old images from showing up while the new image is loading. Note that I’m using DispatchQueue.main.async here to set the imageView’s image on the main thread to prevent crashes. In Chapter 8 - Understanding Combine’s Schedulers you will learn about the receive(on:) operator that will make dispatching to the main queue obsolete because it allows you to receive values from a publisher on the main thread. You can give it a try by placing .receive(on: DispatchQueue.main) before the sink in the code snippet you just saw. This technique of subscribing your cell to publishers in the cell provider is a common way to make your collection or table view cells reactive. The example I’ve shown you is one of the cases where your cells actually have to be dynamic due to the asynchronous loading of images. In other cases, ask yourself if you’re not overcomplicating things by using Combine to drive your cells. Chances are that you don’t need to make it this hard for yourself, but if you do, now you know one way of driving your cells with a Combine publisher. Assigning the output of a publisher to an @Published property with assign(to:) In iOS 14 Apple added a new way of subscribing to publishers in the form of assign(to:). This subscriber was created to link the output of a publisher to a @Published property without causing any retain cycles like assign(to:on:) does when assigning to a keypath on self. Having the ability to set up a direct chain you like can with assign(to:) is especially useful when you’re building an object that exposes a @Published property that represents some kind of state that’s updated due to external factors. For example, every time you call a function that uses a publisher to asynchronously perform some work and Donny Wals 72 Practical Combine communicates the result of this work back through the @Published property. In the previous section, I showed you a very simple and basic DataFetcher that supports pagination. Let’s take another quick look at that code: class DataProvider { let dataSubject = CurrentValueSubject<[CardModel], Never>([]) var currentPage = 0 var cancellables = Set<AnyCancellable>() func fetchNextPage() { let url = URL(string: ,→ "https://myserver.com/page/\(currentPage)")! currentPage += 1 URLSession.shared.dataTaskPublisher(for: url) .sink(receiveCompletion: { _ in // handle completion }, receiveValue: { [weak self] value in guard let self = self else { return } let jsonDecoder = JSONDecoder() if let models = try? jsonDecoder.decode([CardModel].self, ,→ from: value.data) { self.dataSubject.value += models } }).store(in: &cancellables) } } Notice how I’m using a CurrentValueSubject to publish newly loaded data. To do this, I have to subscribe to the publisher created in fetchNextPage() and update dataSubject in the receiveValue closure. I’d rather not have to subscribe to anything in fetchNextPage() and connect my data task to the dataSubject directly. One of the ways this can be achieved is with assign(to:) Donny Wals 73 Practical Combine and a minor refactor to dataSubject: class DataProvider { @Published var fetchedModels = [CardModel]() var currentPage = 0 func fetchNextPage() { let url = URL(string: "https://myserver.com/page/\(currentPage)")! ,→ currentPage += 1 URLSession.shared.dataTaskPublisher(for: url) .tryMap({ [weak self] value -> [CardModel] in let jsonDecoder = JSONDecoder() let models = try jsonDecoder.decode([CardModel].self, from: ,→ value.data) let currentModels = self?.fetchedModels ?? [] return currentModels + models }) .replaceError(with: fetchedModels) .assign(to: &$fetchedModels) } } Notice that I renamed dataSubject to fetchedModel and made it a @Published property instead of a CurrentValueSubject. In fetchNextPage(), I still make the same URL request, except instead of subscribing to the request’s result, I use a tryMap to grab the network call’s result and attempt to convert it to JSON. If this succeeds, I return the currently fetched models combined with the newly fetched models. If the network call or JSON decoding fails, I replace the error that’s emitted with the current value of fetchModels, effectively ignoring all errors. And on the last line I call assign(to: &$fetchedModels). This will automatically assign the value that’s returned from my tryMap (or replaceError if something goes wrong) to the fetchedModels property, Donny Wals 74 Practical Combine causing it to emit its newly obtained value to its subscribers. Note that assign(to:) takes its target as an inout parameter which is why we need to prefix it with &. Note that assign(to:) can only be used on publishers that have Never as their Failure type because a @Published property also has Never as its Failure type. In the code above this is achieved by replacing all errors with a default value to ensure that we can’t end up with an error after the replaceError operator. Also note that we don’t have to hold on to an AnyCancellable after calling assign(to:). The assign(to:) subscriber manages its own cancellable and doesn’t return one for us to manage. You can think of the assign(to:) subscriber as somewhat of a glue subscriber. Its lifecycle is tied to the @Published property that it assigns values to. While I’m quite happy with not having to subscribe to anything to make this example work, there is another benefit to the approach I took here and that’s related to SwiftUI. In SwiftUI, you can’t connect your UI to a publisher directly using assign(to:on:) or sink. Instead, everything in SwiftUI is handled through property wrappers that manage state, like the @ObservedObject and @StateObject property wrappers for example. Both of these property wrappers are used to wrap objects that conform to the ObservableObject protocol. The ObservableObject protocol can be applied to classes like the DataFetcher I just showed you and it automatically adds an objectWillChangePublisher property to conforming classes. This property observes all @Published properties contained in the ObservableObject and emits a value through its objectWillChangePublisher property if any @Published property changes. This allows SwiftUI to update the view rendering so it reflects the ObservableObject’s current state. Since you can’t subscribe to publishers directly in SwiftUI, I’m sure you can imagine that being able to conveniently pipe any publisher into a @Published property using assign(to:) is quite nice for apps that make heavy use of SwiftUI. I digress. . . I promised you that I would try to keep the UI framework related content to a minimum. I just thought that this was a neat bit of background information to help you understand the purpose of the assign(to:) subscriber. Donny Wals 75 Practical Combine Creating a simple theming system with Combine In this section, I will show you how to create a simple theming system. We’ll implement a central theme manager which has a publisher for the desired theme. Changing the theme will immediately update the entire app. Because iOS 13 supports dark and light theming out of the box through system settings, the setting I’m talking about in this section would be used to override the system setting. Most apps solve this with two toggles. One to let the user decide if they want the app to follow the system settings, or if the app should apply it’s own dark and light modes. The second toggle would allow a user to choose between light and dark mode. Without diving into the UI for these toggles, you could model a theming manager that supports this as follows: class ThemeManager { var shouldOverrideSystemSetting: Bool { get { UserDefaults.standard.bool(forKey: ,→ "ThemeManager.shouldOverrideSystemSetting") } set { UserDefaults.standard.set(newValue, forKey: ,→ "ThemeManager.shouldOverrideSystemSetting") } } var shouldApplyDarkMode: Bool { get { UserDefaults.standard.bool(forKey: ,→ "ThemeManager.shouldApplyDarkMode") } set { UserDefaults.standard.set(newValue, forKey: ,→ "ThemeManager.shouldApplyDarkMode") } } } Nothing fancy here, right? Just two properties that map to a key in the UserDefaults store. To use the ThemeManager object, you would typically create an instance of the manager in your SceneDelegate, and pass it along to all view controllers that might be interested in using the theme manager to update the user’s preferences, or to update their UI according to the theme manager’s settings. This technique is called dependency injection. Your view Donny Wals 76 Practical Combine controllers depend on the theme manager, and you inject the theme manager into your view controllers directly through their initializers, or you can inject the theme manager by assigning it to a property on a view controller. Alternatively, you can make ThemeManager a singleton if that fits your workflow and codebase better. Singletons are often considered an anti-pattern and I prefer dependency injection as I described earlier, but either technique should work if you want to implement a theme manager of your own. The code in its current form isn’t very reactive. The theme manager lacks a CurrentValueSubject that will tell us whether a view controller should be dark, light, or use the system settings. This means that we need to update the CurrentValueSubject every time shouldApplyDarkMode, or shouldOverrideSystemSetting changes. Let’s see how that looks in an updated implementation: class ThemeManager { enum PreferredUserInterfaceStyle { case dark, light, system } lazy private(set) var themeSubject: ,→ CurrentValueSubject<PreferredUserInterfaceStyle, Never> = { var preferredStyle = PreferredUserInterfaceStyle.system if shouldOverrideSystemSetting { preferredStyle = shouldApplyDarkMode ? .dark : .light } return CurrentValueSubject<PreferredUserInterfaceStyle, ,→ Never>(preferredStyle) }() var shouldOverrideSystemSetting: Bool { get { UserDefaults.standard.bool(forKey: ,→ "ThemeManager.shouldOverrideSystemSetting") } set { UserDefaults.standard.set(newValue, forKey: ,→ "ThemeManager.shouldOverrideSystemSetting") Donny Wals 77 Practical Combine updateThemeSubject() } } var shouldApplyDarkMode: Bool { get { UserDefaults.standard.bool(forKey: ,→ "ThemeManager.shouldApplyDarkMode") } set { UserDefaults.standard.set(newValue, forKey: "ThemeManager.shouldApplyDarkMode") ,→ updateThemeSubject() } } private func updateThemeSubject() { if shouldOverrideSystemSetting { themeSubject.value = shouldApplyDarkMode ? .dark : .light } else { themeSubject.value = .system } } } A lot is going on in this code. Because there are essentially three states for the user’s preferred theme, I created an enum that can be used to represent the preferred theme in the current value subject. The subject itself is a lazy variable because I want it to be initialized only when somebody accesses it. In the initialization closure, I check what the user’s settings are, and based on that the current value subject receives its initial value. The properties from the previous version of ThemeManager are updated. When a new value is assigned to either shouldOverrideSystemSetting, or shouldApplyDarkMode, updateThemeSubject() is called, and themeSubject’s value is updated which, in turn, will send the user’s new preference to all of its subscribers. A theme manager like this is fairly simple, but also extremely powerful. Let’s see how you would use this theme manager in a view controller: Donny Wals 78 Practical Combine override func viewDidLoad() { // other setup code themeManager.themeSubject.sink(receiveValue: { style in switch style { case .system: overrideUserInterfaceStyle = .unspecified case .dark: overrideUserInterfaceStyle = .dark case .light: overrideUserInterfaceStyle = .light } }).store(in: &cancellables) } This code uses a themeManager that I assume was injected into a view controller. By subscribing to the themeManager’s themeSubject, the view controller can update its overrideUserInterfaceStyle property to correspond with the supplied style preferences. By setting overrideUserInterfaceStyle, the view controller will automatically know whether this view controller should be light, dark, or whatever the system setting is. By injecting the same instance of ThemeManager into all view controllers, you will have a very powerful theming system in your hands. I’ve mentioned dependency injection a few times now, and I realize you might not be familiar with it. In its simplest form, you would pass a dependency like the theme manager to a view controller’s initializer. This only works if you don’t use storyboards. The following code shows how you would create and inject the theme manager into a view controller from the scene delegate: func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } Donny Wals 79 Practical Combine let themeManager = ThemeManager() window = UIWindow(windowScene: windowScene) window?.rootViewController = MainViewController(themeManager: themeManager) ,→ window?.makeKeyAndVisible() } The view controller would have the following initializer: class MainViewController: UIViewController { let themeManager: ThemeManager init(themeManager: ThemeManager) { self.themeManager = themeManager super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("Not implemented") } } I hope this shows that dependency injection sounds a lot more complex than it really is. It’s really just the idea of having an object pass dependencies down to the objects it creates or manages. So if we’d go from MainController to a next view controller, it would be MainController’s job to initialize that next view controller and pass the theme manager to it. Tip: Since iOS 13, it’s possible to achieve dependency injection with storyboards. I won’t explain how in this book, but I have a post on my blog that explains how you can do this exactly. Donny Wals 80 Practical Combine In Summary In this chapter, you took some big leaps towards being able to use Combine in your apps. First, you learned about CurrentValueSubject and PassthroughSubject. You learned about their similarities, differences and use cases. You now know that both subjects allow you to send values to subscribers at will, and you know that a current value subject has a sense of state in the form of the last published value. You also briefly learned about @Published, and how it’s similar to a CurrentValueSubject, except it’s not a Subject like CurrentValueSubject is. Next, you learned how you can assign values published by a publisher to a key path using the assign(to:on:) method. This method allows you to easily update key paths on elements, like labels, with new data as soon as it becomes available. This is a very nice and clean way to integrate Combine in your user interface. As a more practical exercise, you learned how you can integrate Combine in your collection views to update its data source, and how you can integrate Combine with your collection view cells to update a cell’s data dynamically. I demonstrated this by showing you how to load images for your collection view cells with a data task publisher. Lastly, I demonstrated how you can build a simple theming system that allows your users to switch your app between light and dark mode independent of the system setting with a relatively small amount of code. There is a way to clean that code up a little bit and make it shorter by changing the type of value that’s published by the theme manager’s theme subject. I’m going to leave that optimization up to you as an exercise. I have also used CurrentValueSubject for all examples. Some of these examples would work really well with @Published too. Can you figure out which ones? And do you think you can refactor these examples to work with @Published? I’m sure you can! Give it a shot and see. In the next chapter, we’re going to explore user interaction and you’ll learn how you can handle user input in Combine. Donny Wals 81 Practical Combine Using Combine to respond to user input In the previous chapter, you learned how Combine can be used to drive your user interface. The data streams I showed you were pretty static, and user interfaces are typically not this static. Users should be able to tap, drag and swipe to their heart’s content in your app, manipulating data and mutating your app’s state all the time. In this chapter, we’re going to kick it up a notch and respond to user input. You will learn how you can accept user input in one place, and update a UI component somewhere else on the screen. Once you understand how this relationship works in UIKit, I’m going to take a brief detour to the world of SwiftUI, where this principle is built-in through the Binding object. You will also learn how you can place certain timing restrictions on processing user input to prevent the user from generating more input than your code is willing to handle. This chapter is divided into the following sections: • Updating the UI based on user input • Limiting the frequency of user input • Combining multiple user inputs into a single publisher By the end of this chapter, you should be able to build a moderately advanced app with Combine that handles user input and responds to changing values in your app. Sounds good, doesn’t it? Updating the UI based on user input In the previous chapter, you saw how you can use Combine’s assign(to:on:) method to assign a publisher’s output directly to a UI element. While this is pretty cool, it’s also somewhat limiting. The assign(to:on:) way of subscribing to a stream assigns data that’s emitted by a publisher to a UI element, but wouldn’t it be far more interesting to somehow respond to user interaction, update a property and then update the UI? If you’ve worked with RxSwift before you started learning Combine, you might know that it comes with several add-ons that provide a tight integration with UIKit components. Unfortunately, this kind of add-on is not natively available for Combine. In Combine, you need to set up and configure custom extensions on UIKit elements to integrate your UI with Combine. Donny Wals 82 Practical Combine Let’s start with a relatively simple example. What if you wanted to build a slider that, when changed, updates a property that is bound to a published property? Here’s what that might look like without Combine: class ViewController: UIViewController { let slider = UISlider() let label = UILabel() override func viewDidLoad() { super.viewDidLoad() // a bunch of setup code updateLabel() slider.addTarget(self, action: #selector(updateLabel), for: .valueChanged) ,→ } @objc func updateLabel() { label.text = "Slider is at \(slider.value)" } } This approach looks fine upon first inspection. But there are a few gotchas here. First, when you set slider.value directly, it doesn’t trigger a valueChanged action on the slider. This means that if you want to update the slider’s value directly, you need to make sure that you also update the label because the action for the slider doesn’t trigger when you assign the value directly. An alternative approach would be to use an intermediate property with a didSet attached to it: class ViewController: UIViewController { let slider = UISlider() let label = UILabel() Donny Wals 83 Practical Combine var sliderValue: Float = 50 { didSet { slider.value = sliderValue label.text = "Slider is at \(sliderValue)" } } override func viewDidLoad() { super.viewDidLoad() // a bunch of setup code slider.addTarget(self, action: #selector(updateLabel), for: .valueChanged) ,→ } @objc func updateLabel() { sliderValue = slider.value } } This is much better, but the didSet isn’t triggered until the first time the slider’s value is updated. So the label won’t have text until the user moves the slider for the first time. Of course, it’s possible to update the label in viewDidLoad just once to make sure it has a value. Instead of fixing and improving the UIKit-only solution, we’re going to solve this problem with some Combine code instead: class ViewController: UIViewController { let slider = UISlider() let label = UILabel() @Published var sliderValue: Float = 50 var cancellables = Set<AnyCancellable>() Donny Wals 84 Practical Combine override func viewDidLoad() { super.viewDidLoad() $sliderValue .map({ value in "Slider is at \(value)" }) .assign(to: \.text, on: label) .store(in: &cancellables) $sliderValue .assign(to: \.value, on: slider) .store(in: &cancellables) slider.addTarget(self, action: #selector(updateLabel), for: .valueChanged) ,→ } @objc func updateLabel() { sliderValue = slider.value } } There is more code involved in this solution than there was in the UIKit solution. However, this code updates the UI perfectly whenever sliderValue changes and both the label and slider are given their initial values directly through the published property. Unfortunately, Combine doesn’t come with good built-in integration for UIKit. This means that there is no obvious or built-in way for developers to bind sliderValue to the slider. What I mean by this binding here is a two-way binding. In a two-way binding, the slider’s current value is updated whenever sliderValue changes, and the opposite happens when the slider is changed by the user. Donny Wals 85 Practical Combine Caution: Apple implemented a publisher(for:) on NSObject. This publisher allows you to subscribe to KVO changes on classes that inherit from NSObject. UISlider inherits from NSObject through its superclass UIControl. This means that you could use slider.publisher(for: \.value) to subscribe to KVO updates for the slider’s value. Unfortunately, UIControl subclasses aren’t KVO-compliant. This means that slider.publisher(for: \.value) doesn’t work for sliders and other controls. Because Combine doesn’t natively have any publishers that work for slider changes, we need to subscribe to the sliderValue and update it separately. On the other hand, SwiftUI has excellent support for bindings like the one we’d like to achieve here. Let’s look at a SwiftUI example of a view that has a label and a slider which are set up similarly to the UIKit code you just saw, with the main difference being that we don’t need to explicitly update the sliderValue: struct ExampleView: View { @State private var sliderValue: Float = 50 var body: some View { VStack { Text("Slider is at \(sliderValue)") Slider(value: $sliderValue, in: (1...100)) } } } In SwiftUI, properties that are marked with @State trigger UI updates when they change. They can also be used to create bindings between a UI element, and your app’s state like this example shows. I don’t know whether @State uses Combine internally, or whether it uses some other mechanism to update the view when the value of the wrapped property changes. What I do know, is that Apple seems to have decided that the kind of binding I just demonstrated is important for SwiftUI and that it’s not as important in UIKit. In Chapter 9 - Building your own Publishers, Subscribers, and Subscriptions I will show you how you can build a custom publisher that adds an extension to UIKit’s UIControl class Donny Wals 86 Practical Combine that will allow you to use Combine to respond to user events. In that chapter, I will also point you to a set of custom publishers that were created by the community to help you subscribe to changing UI components more conveniently. For now, you must understand that you can use the good old addTarget(_:action:for:) method in UIKit to catch the user’s input, and update a property that resembles a value that should be used to represent the current state of the UI. By making the value you’re updating @Published, you gain the ability to easily subscribe to the property that you’re changing, and update your UI. Limiting the frequency of user input Another common situation where you need to respond to user input is when the user provides input that you need to make a network request. For example, in a search field. A good search field is responsive to the user’s input and makes requests to your search API without forcing the user to press a dedicated “search” button. At the same time, if a user is typing quickly, you don’t want to make a new request for every single character that they type. For example, you might want to make sure that the user has typed more than an arbitrary number of characters before you even initiate a request to your search endpoint in the first place. Additionally, a user that is still typing is unlikely to be interested in the search result for every character they have typed. So we can save precious bandwidth by waiting for a little while before we process the user input to make sure that they aren’t still in the middle of typing their query. For instance, we could wait for 300 milliseconds after the last typed character to begin loading data. I chose a threshold of 300 milliseconds because that’s a commonly used threshold that I’ve seen used for similar features. The principle of waiting a little while before processing user input is called debouncing, and with Combine, we can implement debouncing rather easily. Before I show you how exactly, I want to show you the code I’m working with to make sure it’s easy for you to follow along with me: Donny Wals 87 Practical Combine class DebounceViewController: UIViewController { let textField = UITextField() let label = UILabel() @Published var searchQuery: String? var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // a bunch of setup code textField.addTarget(self, action: #selector(textChanged), for: .editingChanged) ,→ $searchQuery .assign(to: \.text, on: label) .store(in: &cancellables) } @objc func textChanged() { searchQuery = textField.text } } The code in this example looks very similar to the code I’ve presented in the previous section. Whenever the text field’s value changes, a @Published property is updated, and the label that I’ve added to this example is immediately updated with the new text. Instead of making a network call, I will update the label that shows the text field’s current value using the same technique that you’d normally use to debounce the input for network calls. Combine comes with a built-in debounce() operator that we can apply to a publisher. This will limit that publisher’s output, by ignoring values that are rapidly followed by another value. Let’s look at a marble diagram that demonstrates this principle: Donny Wals 88 Practical Combine Figure 5: A marble diagram that describes the debounce operator In this diagram, you can see that whenever a publisher emits several values in a short timeframe, these values are never delivered to the subscriber. The subscriber will only receive values that were not followed up by a new value within 300 milliseconds. To integrate this in for the text field, all we need to change is how we subscribe to $searchQuery: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .assign(to: \.text, on: label) .store(in: &cancellables) The changes required to debounce the output of $searchQuery is pretty minimal, but the effect is huge. Now, when a user is typing and changing their search query, the label isn’t immediately updated. Instead, the user will have to stop typing for a moment, and the label is updated. If you would replace .assign(to: \.text, on: label) with a network call, you’re already saving yourself a lot of bandwidth! Remember that I mentioned a requirement where a user had to type at least a couple of Donny Wals 89 Practical Combine characters before we’re interested in processing the search query? We can achieve this by filtering the output of a publisher using the filter operator: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .filter({ ($0 ?? "").count > 2 }) .assign(to: \.text, on: label) .store(in: &cancellables) Filtering output that’s considered too short can save you some extra bandwith if you’re absolutely sure that it doesn’t make sense for a user to input queries that are shorter than a number of characters. Let’s look at a marble diagram that describes Combine’s filter operator. I want to make sure you’re comfortable with marble diagrams because they are quite common in the world of FRP and they do a good job of explaining how an operator works if you’re comfortable reading them: Figure 6: A marble diagram that describes the filter operator The implementation we have currently is already quite nice, but I’d like to give you a little bit more insight into what output our publisher produces at this point. There are a couple of things we know for sure right now: Donny Wals 90 Practical Combine • The label isn’t updated if the text field’s content is too short. • The label isn’t updated if the user is still typing their search query. There is one last case that isn’t covered here. What if the user types a query, pauses, then types some more and quickly deletes the newly typed characters, making the current value equal to the original value? Here’s a marble diagram that visualizes what happens when a user does this: Figure 7: A marble diagram that describes the filter operator with a duplicated output. Because the label’s text in my example doesn’t change, it’s not easy to see that we handle the same value twice. We can see this in Xcode by printing the output of the filter operator. We won’t do this with sink or assign. Instead, we’re going to use the print operator. This operator will print information about the publisher chain to help you debug and examine what’s happening: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .filter({ ($0 ?? "").count > 2 }) .print() Donny Wals 91 Practical Combine .assign(to: \.text, on: label) .store(in: &cancellables) If we go through the same sequence that I’ve just shown in the marble diagram, we’d see the following output: receive subscription: (Filter) request unlimited receive value: (Optional("Hello")) receive value: (Optional("Hello")) Notice that the value Hello is printed twice. This means that the label receives this value twice. When updating a label, this isn’t a big deal. But if we’d make network calls, or perform expensive computation based on the user’s input, this would be quite wasteful. After all, receiving the same input twice would typically result in the same output. We can prevent this from happening with the removeDuplicates operator. Before I show you how it’s used, here’s a marble diagram that describes removeDuplicates: Donny Wals 92 Practical Combine Figure 8: A marble diagram that describes a filter operator followed by a removeDuplicates operator. As you can see, removeDuplicates will keep any duplicate values to itself if they occur directly after each other. If a different value is emitted between the first and second occurrence of a certain value, the values aren’t considered duplicates and they are forwarded to the subscriber. Let’s see what this looks like in code: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .filter({ ($0 ?? "").count > 2 }) .removeDuplicates() .print() .assign(to: \.text, on: label) Donny Wals 93 Practical Combine .store(in: &cancellables) If we’d run through the same sequence of inputs that I’ve presented earlier, you’d see the following output printed: receive subscription: (RemoveDuplicates) request unlimited receive value: (Optional("Hello")) This is much better. We won’t waste any precious resources on duplicate values anymore. Tip: I will revisit this example in Chapter 10 - Debugging your Combine code where we’ll use Instruments and a tool called Timelane to examine the output of a publisher. With just three simple operators, we were able to create a search feature that ensures that no network calls are wasted on queries that are too short or duplicated. We also won’t waste any network calls by responding to the user input too eagerly. Instead, I showed you how to use debounce to make sure the user is done typing, or at least took a short break from typing, before processing their input. Before I wrap this section up, there is one more output limiting feature that I want to show you. It’s called throttle. While debounce resets its cooldown period for every received value, the throttle operator will emit values at a given interval. When you apply the throttle operator, you can choose whether you want it to emit the first value received during the throttled interval or the last. Let’s look at marble diagrams for both: Emit latest: Donny Wals 94 Practical Combine Figure 9: A marble diagram that shows throttle with latest: true Emit first: Figure 10: A marble diagram that shows throttle with latest: false While throttle is somewhat similar to debounce, they serve very different purposes and are Donny Wals 95 Practical Combine used in very different ways. When you apply a throttle, all you’re guaranteeing is that a publisher will not emit values more frequently than specified in your throttle. This means that if the user is still typing their search query, throttle will output intermediate values while the user types. If you choose to emit the first value that was generated during a throttle interval, you might never receive the latest value unless that value was the first value to occur during a throttle interval. Let’s review the marble diagram with fewer values in it: Figure 11: A simplified marble diagram that shows throttle with latest: false Notice how the latest value occurs during the interval. For this reason, it is never emitted to the subscriber. This may or may not be desirable, depending on the feature you’re building. For a search field, you definitely want to receive the latest value which means you would pass true as the value for the throttle’s latest argument. Doing this roughly approaches what happens when you apply a debounce with the main difference being that the user might still be in the middle of typing when values are emitted. I can imagine that throttling is useful for features where you might want to update a value regularly based on user input. Like for instance, the word count of a document might not need updating all the time. You might consider once every couple of seconds to be good enough regardless of whether the user is still in the middle of typing or not. In this case, you could use a publisher that publishes the document’s current text and throttle its output to compute the Donny Wals 96 Practical Combine word count once every couple of seconds. Combining multiple user inputs into a single publisher There are times where you need something other than a one on one mapping between a publisher and a value. Consider a form where a user would input their details, and you display a summary of the information they supplied below the form. The UI might look a bit as follows: Donny Wals 97 Practical Combine Figure 12: Example screenshot of the form we’ll build in this section. Donny Wals 98 Practical Combine When the user inputs information, the summary view underneath the text fields updates dynamically based on the information that the user provides. To achieve this, I want to use as few publishers as possible. This means that somehow I would need to grab the output from several form fields, and then funnel these fields into a single publisher so I can subscribe to a single publisher to update, for example, a user’s full name. Or possibly their entire address. Consider the following code as a starting point: @Published var firstName = "" @Published var lastName = "" @Published var address = "" @Published var zipCode = "" @Published var country = "" I’m sure you can imagine how the text fields from the screenshot map to the publishers in this code. It’s the same technique that I demonstrated in the first section of this chapter, except there are more text fields. The goal that we’re trying to achieve here is to merge the output from the firstName and lastName publishers into a publisher that emits both values at once and to merge the address and zipCode publishers as well. To achieve this, we’ll need one of Combine’s publishers that merges or combines the output from multiple publishers. When you refer to the documentation for Combine.Publishers, you’ll find that there are three categories of merging publishers: • Publishers.Zip • Publishers.Merge • Publishers.CombineLatest Let’s go over all three of these publishers, and their different versions to see which one achieves the outcome that we’re looking for in this example. Combining publishers with Publishers.Zip The first of the three options I want to explore is Publishers.Zip. This publisher takes two publishers and zips them together. In addition to Publishers.Zip, you can also Donny Wals 99 Practical Combine use Publishers.Zip3 and Publishers.Zip4 these alternative zip publishers take three or four publishers to zip depending on the version you use. You might wonder why Publishers.Zip doesn’t just work on an array instead of having a couple of versions that take a different number of arguments. The reason for this complicated, but what it comes down to is that a version of Zip that accepts an array of publishers would need to erase all type information of the publishers that it zips. If you’re interested in learning more about this, I would recommend that you do some research into the term variadic generics which is a feature that will (hopefully) be added to Swift in the future and would allow for more complex and advanced ways to express a list of generic objects. To demonstrate all three merging publishers, I’m going to use NotificationCenter. I will create two publishers that listen for different notifications. I will then use a merging publisher, in this case Zip to combine the two publishers. Then, I will fire the two notifications one by one to see what happens. I will do this twice. This here is the skeleton code for this process: var cancellables = Set<AnyCancellable>() let firstNotificationName = Notification.Name("first") let secondNotificationName = Notification.Name("second") let firstNotification = Notification(name: firstNotificationName) let secondNotification = Notification(name: secondNotificationName) let first = NotificationCenter.default.publisher(for: ,→ firstNotificationName) let second = NotificationCenter.default.publisher(for: ,→ secondNotificationName) // create and subscribe to Zip, Merge and CombineLatest print("send first") NotificationCenter.default.post(firstNotification) print("send second") NotificationCenter.default.post(secondNotification) Donny Wals 100 Practical Combine print("send third") NotificationCenter.default.post(firstNotification) print("send fourth") NotificationCenter.default.post(secondNotification) I will not include the notification related code in my code samples since it will be the same every time. For Zip, example looks as follows: let zipped = Publishers.Zip(first, second).sink(receiveValue: { val in print(val, "zipped") }).store(in: &cancellables) Simple enough, right? Let’s see what the output in the console looks like for Publishers.Zip: send first send second (name = first, object = nil, userInfo = nil, name = second, object = ,→ nil, userInfo = nil) zipped send third send fourth (name = first, object = nil, userInfo = nil, name = second, object = ,→ nil, userInfo = nil) zipped As the example shows, Publishers.Zip is a publisher that takes two publishers, and will output tuples of the values that are published by the publishers it zips. Notice that for new values to be emitted by Publishers.Zip, all publishers that it zips must have output a new value. The values that are emitted by Publishers.Zip are tuples. The number of values in these tuples is equal to the number of publishers you’re zipping. You can extract the values in this tuple through their position. So for the value from the first publisher you’d use val.0, and for the second value you’d use val.1. If only one of the two (or three, or four) Donny Wals 101 Practical Combine zipped publishers emits a new value, we won’t immediately receive this value. This means that if one of the two publishers completes before the other publisher is completed, you won’t get any new values from the uncompleted publisher because the already completed publisher doesn’t emit new values anymore. The following marble diagram describes how Publishers.Zip works: Figure 13: A marble diagram that shows the zip operator. In addition to creating an instance of Publishers.Zip directly, it’s also possible to apply the zip operator to a publisher instead: Donny Wals 102 Practical Combine let zipped = first.zip(with: second).sink(receiveValue: { val in print(val, "zipped") }).store(in: &cancellables) The result of this code is identical to the earlier example. The zip operator has overloads that allow you to pass to pass two publishers to create a Publishers.Zip3, or three publishers to create a Publishers.Zip4. Note that Publishers.Zip will zip the values from its publishers in order. This means that if one of the publishers emits three values before the second publisher emits a value, the first value from the first publisher is matched up with the first value from the second publisher. The following example does a good job of demonstrating this: let left = CurrentValueSubject<Int, Never>(0) let right = CurrentValueSubject<Int, Never>(0) left.zip(right).sink(receiveValue: { value in print(value) }).store(in: &cancellables) left.value = 1 left.value = 2 left.value = 3 right.value = 1 right.value = 2 This code produces the following output: (0, 0) (1, 1) (2, 2) As you can see, left emits three values before right emits its first value. Since the publishers are zipped in order, you can see that Publishers.Zip emits the first and second emitted value from both publishers together. Donny Wals 103 Practical Combine Based on the findings from this experiment, we can conclude that Publishers.Zip is not the publisher we need for the form I showed you at the start of this section. If either the first name or the last name changes I want to get the current value for both values so I can update the full name label. We can’t achieve this by zipping because a user would have to change both fields for a new value to be emitted by Publishers.Zip. Combining publishers with Publishers.Merge The second combining publisher I want to show you is Publisher.Merge. We’re going to use the same code as I used for Publishers.Zip, except that instead of using Publishers.Zip, I will use Publishers.Merge. But before I do, let’s talk about the different versions of Publishers.Merge. Similar to Publishers.Zip, there are flavors that take two, three or four publishers to merge them into one publisher. But Apple didn’t stop there. They went all the way up to Publishers.Merge8 that allows you to merge eight publishers. And as if that’s not enough, Apple also added a Publishers.MergeMany variant that takes an arbitrary number of publishers. All flavors of Publishers.Merge require that the publishers you want to merge have the same Output and Failure types. The only major difference between Publishers.MergeMany and the other versions of Publishers.Merge that I was able to discover is that Publishers.MergeMany allows you to inspect the publishers it’s merging. You can use Publishers.Merge as follows: let merged = Publishers.Merge(first, second).sink(receiveValue: { val ,→ in print(val, "merged") }).store(in: &cancellables) Alternatively, you can use the merge operator, similar to how you were able to use the zip operator earlier: let merged = first.merge(with: second).sink(receiveValue: { val in print(val, "merged") }).store(in: &cancellables) Donny Wals 104 Practical Combine If we run the code that I used for Publishers.Zip for Publishers.Merge, the following output is produced: send first name = first, object = nil, userInfo = nil merged send second name = second, object = nil, userInfo = nil merged send third name = first, object = nil, userInfo = nil merged send fourth name = second, object = nil, userInfo = nil merged As you can see, Publishers.Merge emits a new value every time one of the publishers it merges emits a new value. Note that it only emits a single value. It interleaves all emitted values from the publishers that it merges. The following marble diagram describes this: Figure 14: A marble diagram that explains the merge operator. This is really close to the desired effect. If we’d apply this to the form that I showed you at the Donny Wals 105 Practical Combine start of this section, we’d be able to update the overview as needed, but we’d have to read the value of both properties that we’re merging because we wouldn’t be able to figure out which of the text fields changed since we’d just receive the current value of the property that was just changed. Let’s look at the last combining publisher. Surely that will do what we need. Combining publishers with Publishers.CombineLatest The last publisher I want to show you is Publishers.CombineLatest. This publisher has several versions, just like Publishers.Merge and Publishers.Zip. For Publishers.CombineLatest, Apple has created three versions in total. In addition to Publishers.CombineLatest we have Publishers.CombineLatest3 and Publishers.CombineLatest4. These exist for the same reasons that Publishers.Zip has multiple versions and they work in the exact same way. So without further explanation, let’s look at the code needed to use Publishers.CombineLatest in our little test skeleton: let combined = Publishers.CombineLatest(first, ,→ second).sink(receiveValue: { val in print(val, "combined") }).store(in: &cancellables) Or written with the combineLatest operator: let combined = first.combineLatest(second).sink(receiveValue: { val in print(val, "combined") }).store(in: &cancellables) This code produces the following output when used in the test skeleton: send first send second (name = first, object = nil, userInfo = nil, name = second, object = ,→ nil, userInfo = nil) combined send third Donny Wals 106 Practical Combine (name = first, object = nil, userInfo = nil, name = second, object = ,→ nil, userInfo = nil) combined send fourth (name = first, object = nil, userInfo = nil, name = second, object = ,→ nil, userInfo = nil) combined Once both publishers have emitted an initial value, the Publishers.CombineLatest publisher will publish the last value emitted for each publisher. The following marble diagram describes Publishers.CombineLatest: Figure 15: A marble diagram that explains combineLatest. This combining publisher is pretty much ideal for our purposes. The only caveat is that both Donny Wals 107 Practical Combine @Published properties that we’re merging need to have an initial value, which they do. So let’s use the combineLatest operator to combine the user’s first name and last name, and populate a UILabel called fullNameLabel: $firstName .combineLatest($lastName) .map({ combined in return "\(combined.0) \(combined.1)" }) .assign(to: \.text, on: fullNameLabel) .store(in: &cancellables) Because firstName and lastName are @Published properties with initial values, we’ll imediately begin receiving the user’s input in either of the text fields as soon as one of the @Published properties changes. By using the combineLatest operator, you can build some pretty nifty and complicated features without a lot of code. I won’t give you the code to combine the output from the zip code and address properties. I’m sure you can write that code by yourself using the example I just showed you. In Summary In this chapter you’ve learned how Combine integrates with a UIKit based user interface. You saw that Combine doesn’t have built-in support for responding to UIControl events like changing slider and text field values but you did see how you can assign a slider or text field value to a @Published property and continue from there. I then went on to show you how you can take user input, and manage how often and when you want to process this input using debounce and filter. This is incredibly useful if you’re building a feature where frequent user input might occur, and where the processing of user input is expensive. A textbook example of this is a search feature where a user types a search query that is used to perform a network request. You saw how you can use the print operator to see exactly what is happening with your publishers. I used this operator to show you that Donny Wals 108 Practical Combine only debouncing and filtering is not always enough. Sometimes a publisher outputs the same value multiple times in a row, and I demonstrated that you can ignore duplicate values with the removeDuplicate operator. To wrap this chapter up, I demonstrated an alternative to debounce called throttle. This operator works slightly different from debounce as you discovered through several marble diagrams. Note that this is the first time I explained an operator with just marble diagrams. I love how marble diagrams communicate the use of operators in Combine clearly, and concisely without any noise that could be introduced through code. In the next chapter you will learn more about networking in Combine, and how you can implement some modern networking features from iOS nicely with Combine’s operators. Donny Wals 109 Practical Combine Using Combine for networking Networking is a key component in many modern applications and it’s no surprise that Apple decided to add a special extension to URLSession to integrate networking with Combine nicely and fluently. Networking is a fantastic use case for Combine because it’s an asynchronous task with a certain output, that you’ll probably want to manipulate and transform before using it. In this chapter, I will show you how can take an existing networking implementation and refactor it to make it play nicely with Combine. This chapter is divided into the following topics: • • • • Creating a simple networking layer in Combine Decoding JSON data with Combine User-friendly networking with Combine Building a complex chain of network calls By the end of this chapter, you will have a deep understanding of how you can use Combine for networking in your application, and you will understand how several of Combine’s more complex and advanced operators can be used to compose a complex chain of tasks. Creating a simple networking layer in Combine If you’ve worked on applications for iOS, tvOS, or even macOS you have probably written or worked with networking code at some point during the development cycle. Your code probably looked a bit like this: func fetchURL<T: Decodable>(_ url: URL, completion: @escaping ,→ (Result<T, Error>) -> Void) { URLSession.shared.dataTask(with: myURL) { data, response, error in guard let data = data else { if let error = error { Donny Wals 110 Practical Combine completion(.failure(error)) } assertionFailure("Callback got called with no data and no ,→ error. This should never happen.") return } do { let decoder = JSONDecoder() let decodedResponse = try decoder.decode(T.self, from: data) completion(.success(decodedResponse)) } catch { completion(.failure(error)) } }.resume() } And while this code functions just fine, there are a couple of things that could be improved to reduce the complexity of the code that handles the outcome of the data task. Let’s go over a couple of things that could use some improvements in this code sample one by one. First, the data task’s callback handler isn’t very user-friendly. Its type signature is (Data?, URLResponse?, Error?) -> Void which doesn’t tell us much about how the callback is supposed to work. In theory, all three arguments could be nil. They might just as well all be non-nil. There’s just no telling. This means that in our closure, we need to check whether data is present, and if it isn’t then error should be. I happen to know that they should never both be nil. But that’s not obvious from the closure arguments. Then, in the same closure, we need to decode the JSON. This code could be considered noise because it’s a somewhat specific task and it can generate errors of its own that are different from networking-related error. In the next section, I will show you how you can use Combine to decode JSON from a data task, but for now, we’re just going to accept that JSON decoding is part of the networking code. If you’re not sure what the do {} catch {} part in this code does, you can read my blog post on throwing functions in Swift to get yourself up to speed. Donny Wals 111 Practical Combine Lastly, it’s important to make sure that completion is called on all possible code paths. This is almost impossible because in production code the assertionFailure would never trigger which means that in the case that there is no data and no error, the completion handler is never called. Again, I happen to know that that should never happen so I also know that this code is fine but it still feels a little off to me. If you didn’t examine the code sample closely, I want you to take one more look at the method signature for the function I defined in the example: fetchURL<T: Decodable>(_:completion:). This function has a generic type T which is Decodable. This means that users of the function get to decide what the fetched data is decoded into. This is very convenient because it enables a very useful level of flexibility. The completion closure I defined has a signature of (Result<T, Error>) -> Void. This means that the completion closure is called with a result that contains a decoded model or an error. If you want to learn more about Swift’s Result type, you can read my blog post on the topic. Let’s see what a similar request might look like when we refactor this exact function with Combine: func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> { URLSession.shared.dataTaskPublisher(for: url) .tryMap({ result in let decoder = JSONDecoder() return try decoder.decode(T.self, from: result.data) }) .eraseToAnyPublisher() } This refactored version of fetchURL is much shorter than the version that didn’t use Combine. It’s also much clearer and more focused on the task at hand than the previous version. By calling dataTaskPublisher(for:) on the URLSession, a publisher is created that has a success value of (data: Data, response: URLResponse) and an error type of URLError. This makes a very clear distinction in terms of what’s expected to happen what the request succeeds or fails. If the task fails, the tryMap operator is skipped and the subscriber of the AnyPublisher that is returned by fetchURL(_:) immediately receives the error. If the request succeeds, Donny Wals 112 Practical Combine the response data is extracted from the result and its decoded into the generic model that the caller of this function needs. Because I used a tryMap here, any errors that occur while decoding the data are automatically forwarded to the subscriber of the resulting publisher. Note that even if a data task comes back with data and a response, you shouldn’t assume that the request completed successfully. It just means that your request was sent to the server successfully, and the server responded. This means that you might have received an error response from the server. For example, you might need to authenticate for your request, or you may have requested a resource that doesn’t exist. It’s always a good idea to check the response’s status code to make sure you got an expected response. This is also true for any networking code you write without Combine. In the next section of this chapter, I will show you how you can do this, and how it integrates with Combine. The networking code that I initially showed you immediately made a request and called a completion closure when the request was done. In Chapter 2 - Exploring publishers and subscribers I explained that publishers in Combine don’t perform any work if they don’t have any subscribers, and URLSession.DataTaskPublisher is no different. When you call fetchURL(_:) and don’t subscribe to the returned publisher, the network call isn’t executed. This means that it’s possible to create a publisher that postpones making a network request as follows: let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL) And what’s interesting is that you can subscribe to this publisher multiple times to get its result: var cancellables = Set<AnyCancellable>() let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL) publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { (model: MyModel) in print(model) Donny Wals 113 Practical Combine }).store(in: &cancellables) publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { (model: MyModel) in print(model) }).store(in: &cancellables) If you were to try this code in a Playground, you’d see that the network request is now executed twice. Usually, this is the desired behavior for your publisher. However, imagine that you want to make a single network call that multiple objects subscribe to without making a new network call. In Combine, you can use the share operator to convert a publisher into a publisher that has reference semantics. In other words, it converts the existing publisher into a class instance that republishes its upstream publisher. Let’s see how this operator can be used in an example: let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL) .share() .eraseToAnyPublisher() In this code, I applied the share operator to the publisher that is returned by fetchURL(_:). This means that the resulting AnyPublisher in this example is now a class instance that republishes the values that are published by the original AnyPublisher that was returned by fetchURL(_:). You can use the share operator for more than just networking, it’s available for all publishers and it allows you to make sure that multiple subscribers for a single publisher will all receive the same values without triggering more work than needed. Now that we have the basics of networking down, let’s make the networking code from this section a little bit fancier by using Combine to decode the data task’s response into the requested model. Donny Wals 114 Practical Combine Handling JSON responses from a network call More often than not, the data returned by a data task will be JSON data. To make decoding this data easy for you, Combine has a decode operator that works on publishers that have Data as their output. Let’s see how this operator can be used in the fetchURL(_:) function I showed you in the previous section: func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> { URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .decode(type: T.self, decoder: JSONDecoder()) .eraseToAnyPublisher() } The decode operator works on any publisher that has Data as its output, and it needs to know what type to decode into, and what kind of decoder should be used to do this. This decoder must conform to the TopLevelDecoder protocol, and currently, there are two common objects that conform to this protocol: JSONDecoder and PropertyListDecoder. Because URLSession.DataTaskPublisher emits values of type (data: Data, response: URLResponse), the map operator is needed to extract the data property from the emitted value. This part is simple enough, and it’s fairly straightforward. However, networking is often a bit more complex in the real world. Depending on the response that we get from the network, it might not make sense to decode into the supplied model. Maybe the server returned a JSON body that describes an error, along with a non-2xx HTTP status code. By non-2xx I mean a response with a status code that falls outside of the range of status codes that represent a success response. Let’s start simple, if the server returns a response with a non-2xx status code, we’ll decode the response data into an error model, and we’ll throw an error. If we get a 200 status code, we’ll decode into the requested model. Let’s set the stage first by changing the logic a little bit while still only handling the success case: Donny Wals 115 Practical Combine func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> { URLSession.shared.dataTaskPublisher(for: url) .mapError({ $0 as Error }) .flatMap({ result -> AnyPublisher<T, Error> in guard let urlResponse = result.response as? HTTPURLResponse, ,→ (200...299).contains(urlResponse.statusCode) else { fatalError("We'll handle this later") } return Just(result.data).decode(type: T.self, decoder: ,→ JSONDecoder()).eraseToAnyPublisher() }).eraseToAnyPublisher() } This first thing to note is the mapError that’s applied to the data task publisher. This step is somewhat unfortunate, but it’s required to make the flatMap work properly. The flatMap operator requires that the publisher it operates on has the same error type as the publishers that it emits. And since decode emits a generic Error, we need to transform the data task’s URLError to Error. Inside of the flatMap, I don’t do anything fancy. I check whether the received response has a status code of 200 and if we do, I return a Just publisher that publishes the result’s data which is then decoded into T. If you’re not sure what Just is, I recommend that you go back to Chapter 4 - Updating the User Interface because it’s introduced in that chapter. This code is functionally almost the same as code we hade before I introduced the decode operator. The most important difference is the flatMap operator which just makes everything a little bit more confusing. So why are we using flatMap here? The reason is simple, I want this to be complicated on purpose! The flatMap operator sometimes makes you jump through hoops to get it to work due to its Failure type requirements, and I figured this was a good opportunity to remind you of that. Towards the end of this section, I will simplify the code a little bit by removing the flatMap and replacing it with a tryMap instead. Note that the flatMap emits publishers of type AnyPublisher. The reason for that is that we’ll have a slightly different publisher for the case where we didn’t get a 200 response, and even if we did, I don’t want to type out the full publisher returned by applying decode to Just. Donny Wals 116 Practical Combine Let’s expand the code I just showed you by adding the code that’s supposed to be where I initially wrote a fatalError: struct APIError: Decodable, Error { // fields that model your error } func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> { URLSession.shared.dataTaskPublisher(for: url) .mapError({ $0 as Error }) .flatMap({ result -> AnyPublisher<T, Error> in guard let urlResponse = result.response as? HTTPURLResponse, ,→ (200...299).contains(urlResponse.statusCode) else { return Just(result.data) .decode(type: APIError.self, decoder: JSONDecoder()) .tryMap({ errorModel in throw errorModel }) .eraseToAnyPublisher() } return Just(result.data).decode(type: T.self, decoder: ,→ JSONDecoder()).eraseToAnyPublisher() }).eraseToAnyPublisher() } The code that I added here is slightly more complex than what we had before. When the data task comes back with an error code, I still use a Just publisher to grab the data. I then decode that data into a special APIError struct that models the fields of the JSON error that I expect, and I then use a tryMap to throw the decoded model as an error. This makes the Just publisher fail even though it normally doesn’t. Ultimately, the subscribers of fetchURL(_:) will receive the thrown error in their receiveCompletion block as an Error object that they can extract from the completion object using the following code: Donny Wals 117 Practical Combine var cancellables = Set<AnyCancellable>() fetchURL(myURL) .sink(receiveCompletion: { completion in if case .failure(let error) = completion, let apiError = error as? APIError { print(error) } }, receiveValue: { (model: MyModel) in print(model) }).store(in: &cancellables) I’m quite happy with how fetchURL(_:) is used but in all honesty, I think the implementation of fetchURL(_:) is a bit of a mess. You did get to see some more of flatMap, but I think we can do better. Let’s see what fetchURL(_:) looks like if we don’t flatMap: func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> { URLSession.shared.dataTaskPublisher(for: url) .tryMap({ result in let decoder = JSONDecoder() guard let urlResponse = result.response as? HTTPURLResponse, ,→ (200...299).contains(urlResponse.statusCode) else { let apiError = try decoder.decode(APIError.self, from: ,→ result.data) throw apiError } return try decoder.decode(T.self, from: result.data) }) .eraseToAnyPublisher() } This is much cleaner, and much easier to understand don’t you agree? We don’t have to use mapError, and we don’t have complicated nested publishers that return response.data which is then mapped using the decode operator. This code doesn’t use the decode operator Donny Wals 118 Practical Combine at all. Isn’t that something? We started this section with the decode operator, and we end this section with a tryMap and manual decoding. Pretty much the same thing we did before. The purpose of this whole exercise was to show you that the tool that looks right for the job initially, isn’t always the tool that is the right tool for the job. Let’s explore one last common pattern that I’ve seen in many apps myself, retrying requests after performing a token refresh. When you work with an API that requires authentication through tokens, there’s a good chance that you need to refresh the token every once in a while. In Combine, you can use tryCatch, map and a new operator called switchToLatest to achieve this kind of behavior. Before I demonstrate how this is done exactly, I want to show you what switchToLatest does, and what it’s good for. And to do that, we need to revisit flatMap. In Combine, flatMap takes the output of a publisher, transforms that into a new publisher and all values emitted by that new publisher are relayed to subscribers, making it appear as if it’s a single publisher that emits all of these values. You can limit the number of publishers that a flatMap will keep active at any time using its maxPublishers argument. When you do this, flatMap will only transform the first n values that it receives where n is equal to the value you passed to maxPublishers. Any subsequent values will not be passed to the flatMap until one of the publishers it created completes and flatMap is ready to handle the next value. There are times where this isn’t quite what you want to achieve, and that’s what switchToLatest is good for. You can apply switchToLatest to the output of a map operator that produces publishers. This will automatically emit values produced by the latest publisher that is produced inside of the map. If you’re familiar with RxSwift, you might know this combination of operators as a single operator called flatMapLatest. Tip: I first introduced flatMap in Chapter 3 - Transforming publishers. Go back to that chapter to read up on maxPublishers if you’re not entirely sure how that argument works. Now that you have an idea of what switchToLatest does, let’s look at this operator as a marble diagram: Donny Wals 119 Practical Combine Figure 16: A marble diagram that explains map and switchToLatest As you can see, this operator takes values, transforms them into publishers and it only emits values produces by the latest publisher that was created. The older publisher is discarded. Let’s see this in action in a final version of the fetchURL(_:) function: struct APIError: Decodable, Error { let statusCode: Int } func refreshToken() -> AnyPublisher<Bool, Never> { Donny Wals 120 Practical Combine // normally you'd have your refresh logic here return Just(false).eraseToAnyPublisher() } func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> { URLSession.shared.dataTaskPublisher(for: url) .tryMap({ result in let decoder = JSONDecoder() guard let urlResponse = result.response as? HTTPURLResponse, ,→ (200...299).contains(urlResponse.statusCode) else { let apiError = try decoder.decode(APIError.self, from: ,→ result.data) throw apiError } return try decoder.decode(T.self, from: result.data) }) .tryCatch({ error -> AnyPublisher<T, Error> in guard let apiError = error as? APIError, apiError.statusCode == ,→ 401 else { throw error } return refreshToken() .tryMap({ success -> AnyPublisher<T, Error> in guard success else { throw error } return fetchURL(url) }).switchToLatest().eraseToAnyPublisher() }) .eraseToAnyPublisher() } The interesting bit here is the tryCatch operator. The error thrown in tryMap is an APIError which has a statusCode property. In my implementation, that statusCode would Donny Wals 121 Practical Combine match the status code from the HTTP response. The tryCatch operator is an operator you haven’t seen before but it’s fairly straightforward. It is used to catch any errors that were thrown upstream, so in this case by the data task itself, or by tryMap. When an error is thrown, it is passed to the tryCatch, and you can replace the thrown error with a new publisher. In this case, we only want to replace the error if it’s a 401 error which the HTTP error code for “unauthorized”. If we get a different error, we immediately throw the error that we received so it ends up downstream at the subscriber for this publisher. When we get a 401 error, the refreshToken() function is called which, for demo purposes, returns an AnyPublisher<Bool, Never> that indicates whether the refresh was successful. If the refresh wasn’t successful, the original error is thrown downstream. If the refresh completed successfully, the fetchURL(_:) publisher is returned which will cause the request to be executed again but this time we’d be authenticated. Because we do all of this in a map, we need to call switchToLatest on the map and erase it to AnyPublisher to make sure we return an AnyPublisher<T, Error> from the tryCatch. This whole contraption is quite something, isn’t it? And while we’re doing extremely complicated things here, the code is actually fairly straightforward if you know what all of these operators do. This is both one of Combine’s strengths and a weakness in my opinion. It’s very easy to chain operators together and create extremely complicated flows with very little code. At the same time, the simplicity and terseness of the code can hide complexity in a way that ultimately makes the code somewhat harder to understand and reason about. Even though it’s pretty neat, the token refresh flow I just showed you requires with a word of warning. If your refreshToken function succeeds, but your API still returns a 401 when you retry the initial request, you might infinitely recurse in this function. In general, this would probably indicate that something is wrong on your server, but it’s good to be aware of this caveat. Tip: In Chapter 12 - Driving publishers and flows with subjects I will show you a slighty more advanced way to implement a token refresh flow similar to the one I’ve shown you in this chapter except it uses a PasstroughSubject and flatMap to drive the process. Donny Wals 122 Practical Combine User-friendly networking with Combine Aside from the straightforward network calls you learned about in the previous section, there are certain networking features on iOS that are somewhat important and often overlooked. There are two features that I want to focus on in this section because they are new in iOS 13, are very useful for your users, and they integrate very nicely with Combine. In iOS 13, Apple introduced two new features that allow developers to help their users save precious data. The features I’m talking about are Low Data Mode and the allowsExpensiveNetworkAccess property on URLSessionConfiguration. Low Data mode is a feature that users can enable in the settings app under their networking settings. When Low Data mode is active, the user provides a clear sign to apps that they’re using. The network they are on is slow, congested, they may have a limited data plan, or they may have Low Data mode enabled for a difference. Ultimately the reason doesn’t really matter. The user has a clear preference and as developers, we should respect this preference as much as possible. When Low Data mode is active, your app has the same capabilities as it always had. The feature is opt-in from an application’s point of view. It’s good practice to do this and to consider which parts of your app are non-essential, use a lot of data, or can easily be replaced with a version that uses fewer of the user’s precious megabytes. A good example of a feature that can easily be adapted for Low Data mode is a fancy screen that uses a nice gif or video as a background. When Low Data mode is active, you might choose to load a static image as a background instead of having an animated version. Alternatively, you might load lower resolution images when Low Data mode is active. Before I move on to explain allowsExpensiveNetworkAccess, let’s see how you can opt-in to Low Data mode, and make it play nicely with Combine. The simplest way to opt-in to Low Data mode is to set the allowsConstrainedNetworkAccess property on your URLSessionConfiguration to false: let session = URLSession.shared session.configuration.allowsConstrainedNetworkAccess = false When you configure your URLSession like this, any request made by this URLSession will now error with a URLError that has its networkUnavailableReason set to .con- Donny Wals 123 Practical Combine strained. For that reason, I wouldn’t recommend enabling low data mode on the configuration for URLSession.shared. Instead, you might want to consider two separate URLSession instances for this purpose: let session = URLSession.shared let constrainedConfiguration = URLSessionConfiguration.default constrainedConfiguration.allowsConstrainedNetworkAccess = false let constrainedSession = URLSession(configuration: ,→ constrainedConfiguration) This splits the two different kinds of use cases but an approach like this comes with its own problems. Instances of URLSession can perform certain optimizations for you that aren’t shared between sessions. This means that the two sessions I just created exist individually and we might miss out on some networking performance enhancements. Instead of configuring a URLSessionConfiguration for Low Data mode, it’s also possible to configure individual requests to respect the user’s Low Data mode settings: var constrainedRequest = URLRequest(url: fullImageURL) constrainedRequest.allowsConstrainedNetworkAccess = false let normalRequest = URLRequest(url: smallImageURL) Both of these requests can be executed by the same URLSession. If Low Data mode is active, constrainedRequest will fail with the error I mentioned earlier. The normalRequest would execute just fine because it doesn’t opt-in to Low Data mode. Let’s see how this is used with a URLSession.DataTaskPublisher: URLSession.shared .dataTaskPublisher(for: constrainedRequest) .tryCatch({ error -> URLSession.DataTaskPublisher in guard error.networkUnavailableReason == .constrained else { throw error } Donny Wals 124 Practical Combine return session.dataTaskPublisher(for: normalRequest) }) .sink(receiveCompletion: { completion in // handle completion }, receiveValue: { result in // handle received data }) Based on what you already know about Combine and its operators, this code shouldn’t need much explanation. We subscribe to the data task publisher, use tryCatch to inspect the error and forward the error if it’s not a Low Data mode related error, and if it is, we return a new publisher from tryCatch that will load a lower quality version of the same image. A feature like Low Data mode is something that I’m sure a lot of your users will come to rely on to save data when their data plan is low, or when they use a less than ideal WiFi connection. If you use Combine for networking, I don’t think there’s any reason to not support Low Data mode in your app. Integrating it is fairly straightforward, and you’d do your users a huge favor. Earlier in this section, I mentioned the allowsExpensiveNetworkAccess property that was introduced in iOS 13. This property doesn’t have a toggle in the OS’ settings, but it’s an important setting to understand and use. Lots of users use their devices on networks that are expensive when you use a lot of data. Mobile data plans immediately come to mind, but metered networks or mobile hotspots might also cost a user a lot of money. If you want to prevent your app from ramping up quite the bill, make sure to set allowsExpensiveNetworkAccess to false on your URLSessionConfiguration or URLRequest objects. This feature is especially useful if your app performs large synchronization operations, or if your users can download large files from a server using your app. Both features I mentioned can be used and implemented similarly, so I won’t show how to implement allowsExpensiveNetworkAccess line by line. Instead, I think you should be able to implement this on your own. Donny Wals 125 Practical Combine Building a complex chain of network calls In the previous chapter I showed you how you can use a Publishers.CombineLatest publisher to combine values that are emitted by two publishers into a publisher that emits tuples of the two latest values that were emitted by both publishers. Merging or combining the output of publishers isn’t only useful in the context of UI. It can also be really useful if you want to use Combine to make sure several network calls have succeeded, or if you want to fire off multiple network calls and handle their responses in a single sink. In this section, I will show you how you can use Combine’s Publishers.Zip and Publishers.Merge publishers to create an interesting and complicated homepage building process with very little code. The following image describes the flow of the process that I will show you in this section: Figure 17: The flow for building the homepage Note that the homepage in this image is made up of three tasks. One task fetches the featured Donny Wals 126 Practical Combine content, one fetches a section with curated content and one builds up the user’s favorites section. The featured section and the curated section are both built using a single network call. The favorites section is a little bit more complicated. The app that this homepage would be a part of has some offline capabilities which allow the user to store their favorites locally. This means that the user might have some favorite items on their device, that are not on the server. To retrieve the local and the remote favorites, I will use separate publishers that are then merged into a single publisher using Publishers.Zip. By zipping the remote and local publishers, I can use a single map to combine both responses into a single set of favorites, which is then published to the homepage through the merged publisher. Pretty cool, right? Before I show you the publishers and how they come together in context, I want to show you the models I’m working with: enum SectionType: String, Decodable { case featured, favorites, curated } struct Event: Decodable, Hashable { // event properties } struct HomePageSection { let events: [Event] let sectionType: SectionType static func featured(events: [Event]) -> HomePageSection { return HomePageSection(events: events, sectionType: .featured) } static func favorites(events: [Event]) -> HomePageSection { return HomePageSection(events: events, sectionType: .favorites) } static func curated(events: [Event]) -> HomePageSection { return HomePageSection(events: events, sectionType: .curated) } Donny Wals 127 Practical Combine } The model is relatively simple. Each publisher will ultimately map whatever output it has into a HomePageSection. Even if a fetch operation fails, we should still get an empty HomePageSection that corresponds with the relevant publisher. This might not be the best approach to take in a production environment, but for demonstration purposes, it’s more than good enough. Let’s start with the two simple publishers. The curated content and the featured content publishers: var featuredPublisher = URLSession.shared.dataTaskPublisher(for: ,→ featuredContentURL) .map({ $0.data }) .decode(type: [Event].self, decoder: JSONDecoder()) .replaceError(with: [Event]()) .map({ HomePageSection.featured(events: $0) }) .eraseToAnyPublisher() var curatedPublisher = URLSession.shared.dataTaskPublisher(for: ,→ curatedContentURL) .map({ $0.data }) .decode(type: [Event].self, decoder: JSONDecoder()) .replaceError(with: [Event]()) .map({ HomePageSection.curated(events: $0) }) .eraseToAnyPublisher() Both of these publishers retrieve an array of [Event] from the server, which is used to create the relevant HomePageSection objects. This code uses several mechanisms that you are already familiar with. The replaceError operator is very important for the flow that I’m building in this section. It makes sure that no section ever comes back with an error. In this case, decoding error and networking errors will be caught and replaced with an empty events array. Let’s look at the two publishers that will make up the favoritesPublisher next: Donny Wals 128 Practical Combine class LocalFavorites { static func fetchAll() -> AnyPublisher<[Event], Never> { // retrieve events from a local source } } var localFavoritesPublisher = LocalFavorites.fetchAll() var remoteFavoritesPublisher = URLSession.shared.dataTaskPublisher(for: curatedContentURL) ,→ .map({ $0.data }) .decode(type: [Event].self, decoder: JSONDecoder()) .replaceError(with: [Event]()) .eraseToAnyPublisher() These publishers on their own don’t look very impressive. They are quite simple and similar to the two publishers you saw before. The code so far shouldn’t contain any surprises but we’re about to start with the fancy work. Let’s zip up the two favorites publishers: var favoritesPublisher = Publishers.Zip(localFavoritesPublisher, remoteFavoritesPublisher) ,→ .map({ favorites -> HomePageSection in let uniqueFavorites = Set(favorites.0 + favorites.1) return HomePageSection.favorites(events: Array(uniqueFavorites)) }) .eraseToAnyPublisher() By zipping the two favorites publishers together, it’s guaranteed that both publishers will have retrieved data when the map operator is called. In the previous chapter you learned that Publishers.Zip emits tuples with the values from the zipped publishers. In this case, that means that we have two tuple members and both are [Event] objects. We can add them together and convert them to a Set to remove duplicates, and then convert back to an array to create the favorites HomePageSection. With this favoritesPublisher in place, we’re ready to create the homePagePublisher which will merge the favoritesPublisher, curatedPublisher and Donny Wals 129 Practical Combine featuredPublisher together which will allow us to build the homepage as sections become available rather than waiting for all sections to be fetched and rendering the entire page at once: var homePagePublisher = Publishers.Merge3(featuredPublisher, ,→ curatedPublisher, favoritesPublisher) This code is pretty straightforward. We call Publishers.Merge3 to create a new publisher that will interleave the events from the three publishers it merges which means that subscribers of the homePagePublisher will receive a single stream of values that represent different sections of the homepage: homePagePublisher .sink(receiveValue: { section in switch section.sectionType { case .featured: // render featured section case .curated: // render curated section case .favorites: // render favorites section } }).store(in: &cancellables) None of the code I have shown you in this section is extremely complex or completely new to you. But by cleverly combining what you already know you can come up with complex flows in Combine that read naturally, and are reasonably simple to understand. It’s those moments when you write a bunch of small, isolated pieces of code that can be composed together using built-in operators to create something complex and beautiful when I truly appreciate everything that Combine enables me to do without thinking about all the complex and intricate details that are under the hood of Combine’s operators. Donny Wals 130 Practical Combine In Summary In this chapter, you took your networking knowledge to the next level. I’ve shown you several interesting concepts that apply to networking and Combine. You saw how you can build a very simple and basic networking layer in Combine, and you saw how Combine deals with JSON decoding beautifully using the decode operator. You also learned how you can implement a rather complicated networking flow where you attempt to refresh an authentication token if a network request failed due to an authentication error, and automatically retrying the network request if the token refresh succeeds. After that, I showed you how you can integrate Low Data mode in you app to make your app’s networking logic more user-friendly. It was particularly interesting to see how you can use tryCatch to inspect a networking error and create a new publisher to make a new network request if needed. To wrap this chapter up, I showed you how you can use Combine’s combining publishers to build a complex and interesting flow of network calls that all fetch a section of a homepage. You learned that you can write small isolated bits of code to build simple publishers that can be composed into something far more complex. With all of this newfound knowledge you didn’t just expand your networking skills. All of Combine’s operators and combining publishers can be used in many contexts that might not involve any networking at all. Donny Wals 131 Practical Combine Wrapping existing asynchronous processes with Futures in Combine In this book, I have mostly focussed on showing you built-in Combine publishers, operators, and processes. While this is a fantastic foundation to work with, the reality is that most code bases contain tons of custom asynchronous operations. Several of Apple’s frameworks come with asynchronous jobs that are not (yet) wrapped in Combine publishers. Luckily, Combine comes with the ability to create custom publishers in the form of Subject objects like CurrentValueSubject and PassthroughSubject. These subjects are extremely useful to turn mutable properties into publishers that parts of your app can subscribe to. You can even use them to create a publisher that performs a task and outputs a single result. Oftentimes, you’ll find that creating a CurrentValueSubject or a PassthroughSubject will give you the kind of abstraction you’re looking for. In this chapter, I would like to present an alternative abstraction to you called Future. A Future in Combine is a special kind of publisher that performs work and fulfills a promise to notify subscribers that the work is done. This sounds very similar to how a Publisher works, but a Future in Combine comes with special semantics that makes it look similar to other publishers, while it acts in a completely different way. In this chapter, you will learn everything you need to know about futures. You will learn how they work, how they differ from other publishers, and when they might be useful in your apps. Understanding how Futures work If you’ve worked with abstractions over asynchronous takes before, you may have heard of the term Future, or Promise. The concept of futures is not unique to Combine and there are several libraries in different programming languages on different platforms that provide an implementation of the Future object. A Future in Combine is a publisher that will eventually emit a single value and completes immediately. For most publishers other than Just this rule is somewhat of an unwritten rule because it’s not strictly enforced. If you look Donny Wals 132 Practical Combine at a publisher like URLSession.DataTaskPublisher, nothing about it tells you that it will only emit a single value. You know it won’t emit multiple values, but theoretically, it can. Or at least, it looks like it can. There is no way for us to be sure. This is especially true if you transform the data task publisher to an AnyPublisher. A Future in Combine can’t ever emit more than a single value because it’s enforced in its implementation. Because a Future is a publisher, you can transform it into an AnyPublisher which would hide this detail from users of your Future, but at least you know at the point where you create the Future what you’re working with. It’s a small difference that you might consider to be insignificant, but as you’ll find out throughout this chapter, it matters. Before I explain more about Future and how it compares to other publishers, I want to show you a quick example of how a Future is created and used. var cancellables = Set<AnyCancellable>() func createFuture() -> Future<Int, Never> { return Future { promise in promise(.success(Int.random(in: 0..<Int.max))) } } createFuture() .sink(receiveValue: { value in print(value) }).store(in: &cancellables) In this code, you can already derive a lot of information about how a Future is used. The createFuture() function creates an instance of a Future that has Int as its Output and Never as it’s Failure. This means that we’ll emit a single Int from this Future, and the Future will never fail. The initializer for Future takes a closure. This closure will be passed a Promise closure. This Promise closure must be called by you with an instance of Result to fulfill the Promise, which will make the Future emit its result. The Result that you pass to the Promise must have the same Success and Failure types as the Future’s Output and Failure. Makes sense so far, right? Donny Wals 133 Practical Combine You can perform all kinds of work in the closure that’s passed to the Future’s initializer. In the initial example, I just generate a random Int but we could do far more elaborate work, like making a network request: var cancellables = Set<AnyCancellable>() func fetchURL(_ url: URL) -> Future<(data: Data, response: URLResponse), URLError> { ,→ return Future { promise in URLSession.shared.dataTask(with: url) { data, response, error in if let error = error as? URLError { promise(.failure(error)) } if let data = data, let response = response { promise(.success((data: data, response: response))) } }.resume() } } let publisher = fetchURL(myURL) publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { value in print(value) }).store(in: &cancellables) This code creates a Future that is used to perform a network request. When the request completes, I check whether the request is successful and I fulfill the Promise with the respective success or failure value. If you look closely at how fetchURL(_:) is used in this example, you should see that it looks very similar to the networking code that I’ve shown you before. In the previous chapter you saw the following code: Donny Wals 134 Practical Combine var cancellables = Set<AnyCancellable>() let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL) publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { (model: MyModel) in print(model) }).store(in: &cancellables) publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { (model: MyModel) in print(model) }).store(in: &cancellables) I explained that subscribing to a publisher twice would result in the URL request being executed twice. Once for each subscriber. The reason this happens is that publishers normally don’t emit values or perform work if they don’t have any subscribers. This is not how a Future works in Combine. A Future begins executing its work immediately when it’s created. Try placing the following code in a Playground to see what I mean: import Combine import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true let myURL = URL(string: "https://practicalcombine.com")! func fetchURL(_ url: URL) -> Future<(data: Data, response: ,→ URLResponse), URLError> { return Future { promise in Donny Wals 135 Practical Combine URLSession.shared.dataTask(with: url) { data, response, error in if let error = error as? URLError { promise(.failure(error)) } if let data = data, let response = response { promise(.success((data: data, response: response))) } print("RECEIVED RESPONSE") }.resume() } } let publisher = fetchURL(myURL) You’ll notice that RECEIVED RESPONSE is printed to the console even though you never subscribed to the created Future. Now add the following code after the let publisher = fetchURL(myURL) line: publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { value in print(value.data) }).store(in: &cancellables) publisher .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { value in print(value.data) }).store(in: &cancellables) if you run this code, you’ll find that the following output is printed to the console: Donny Wals 136 Practical Combine 141745 bytes finished 141745 bytes finished RECEIVED RESPONSE This proves that a Future will not only execute regardless of whether you subscribe to it. It also shows that a Future will only run once. Once a Future is completed, it will replay its output to new subscribers without running again until you create a new Future. This is important so I’ll make it extra clear: Caution: A Future executes immediately when it is created instead of waiting for a subscriber like a normal publisher does. This means that even though Future looks and feels like a Publisher it doesn’t play by the same rules. The following code would execute the Future-based network call twice: fetchURL(myURL) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { value in print(value.data) }).store(in: &cancellables) fetchURL(myURL) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { value in print(value.data) }).store(in: &cancellables) The reason the code above executes the network call twice is that every time you call fetchURL(_:) a new Future is created. This Future will begin executing immediately and will emit a single value to its subscribers. Donny Wals 137 Practical Combine Because a Future executes once and begins its execution immediately when its created, it could lead to some interesting and confusing outcomes if you’re not careful. When you call eraseToAnyPublisher() on a Future, it becomes an AnyPublisher. This means that users of your Future will not be able to tell that they’re working with a Future that begins its execution immediately, emits a single value and re-emits the same value to its subscribers if it receives more than a single subscriber. It’s possible to force futures to wait for subscribers before they start their execution. This will make your futures behave more like other publishers, and it gives back some control over when a Future executes its work. You can do this with the Deferred publisher. Let’s look back at the first example of a Future I showed you: func createFuture() -> Future<Int, Never> { return Future { promise in promise(.success(Int.random(in: (1...100)))) } } You know that this Future will run immediately when you call createFuture(). To prevent this using the Deferred publisher, you would have to refactor createFuture() as follows: func createDeferredFuture() -> Deferred<Future<Int, Never>> { return Deferred { return Future { promise in promise(.success(Int.random(in: (1..<Int.max)))) } } } Notice that I renamed the function from createFuture() to createDeferredFuture(). I did this so you can easily tell them apart. The return type changed from Future<Int, Never> to Deferred<Future<Int, Never>>. This informs anybody that calls createDeferredFuture() that they can expect the following statements to be true about the publisher they receive: Donny Wals 138 Practical Combine • The publisher will not perform work until it has subscribers • If you subscribe to the same instance of this publisher more than once, the work will be repeated Note that both of these statements are false for a regular Future. The following code demonstrates this: let plainFuture = createFuture() plainFuture.sink(receiveValue: { value in print("plain1", value) }) plainFuture.sink(receiveValue: { value in print("plain2", value) }) let deferred = createDeferredFuture() deferred.sink(receiveValue: { value in print("deferred1", value) }) deferred.sink(receiveValue: { value in print("deferred2", value) }) Here’s an example of the output produced by the code above: plain1 2015758617746974568 plain2 2015758617746974568 deferred1 1272179517968986878 deferred2 2449688451723054346 Notice how plain1 and plain2 show the same integer value. The reason for this is that the Future executed immediately, and repeats its output. The deferred version prints a different Donny Wals 139 Practical Combine value for each subscription. The reason for this is that the Deferred publisher executes the code that it receives as a closure whenever it receives a new subscriber. In the example where I introduced Deferred, I returned an instance of Future in the Deferred closure. This means that every subscriber of deferred in the example above indirectly subscribes to a new instance of Future rather than subscribing to the same Future instance multiple times. In other words, wrapping a Future in Deferred makes it behave exactly the same as any other Publisher. So when should you use Deferred and when shouldn’t you use Deferred? That’s a tough question in my opinion. The decision is ultimately a semantic and API design decision and the answer will vary for each use case. If you’re certain that performing work immediately and only once is the way to go, a plain Future is probably fine. An example of this could be loading an app’s configuration file if that file never changes throughout the course of your application. Wrapping a Future in Deferred makes it behave like a normal Publisher. This could be a good fit if you don’t want a Future to execute immediately, or if you want to be able to subscribe multiple times to the same instance of Deferred without getting the same result every time. In my opinion, it’s a good idea to evaluate the behavior you need on a case by case basis while keeping in mind how your code is used. Note that if you’re working on an API that is meant to be used by others and you feel like you can’t make the choice between a plain Future or one that’s wrapped in a Deferred publisher, you can let the user of your API decide for themselves. They can wrap a call to a function that creates a Future in Deferred themselves if needed: let manuallyDeferredPublisher = Deferred { return createFuture() } The Future created by createFuture() is not executed until manuallyDeferredPublisher receives a subscriber. If you do decide that you want to return a deferred Future from a method, I would recommend type erasing your publisher to AnyPublisher. This will make it clear to users of Donny Wals 140 Practical Combine your code that the publisher they receive will behave like any other Publisher in Combine without being aware of it being a deferred Future. This is an implementation detail that can be hidden, allowing you to change the underlying publisher without having to change the return type. All of these small details about futures and deferred futures are extremely important to understand, and I would like to advise you to use futures with care, and avoid hiding them in your codebase. If you’re going to use a Future, it needs to be clear and obvious that you are doing so to avoid confusion down the line. That said, there are interesting applications for futures in Combine. Let’s look at a couple of fun examples. Using Combine to ask for push permissions I think futures are a great way to wrap asynchronous work that you would normally kick off immediately and don’t need future updates for. In other words, in my opinion, a Future is a good abstraction for asynchronous work that I want to write as if I’m not using Combine while in reality, I am. By using futures for this kind of work I get all of the benefits of Combine without the downsides of imperative asynchronous code. Consider a scenario where you want to check for a user’s current push notification permissions, and take action based on the result of that call. You might write the following code to get notification permissions and take action based on the current permission status if you’re not using Combine: UNUserNotificationCenter.current().getNotificationSettings { settings ,→ in switch settings.authorizationStatus { case .denied: DispatchQueue.main.async { // update UI to point user to settings } case .notDetermined: UNUserNotificationCenter.current().requestAuthorization(options: ,→ [.alert, .badge, .sound]) { result, error in Donny Wals 141 Practical Combine if result == true && error == nil { // We have notification permissions } else { DispatchQueue.main.async { // Something went wrong / we don't have permission. // update UI to point user to settings } } } default: // assume permissions are fine and proceed break } } A lot is going on in this example, and you might consider this code somewhat hard to read and follow. If you examine the code closely, there are two parts to what’s happening here. First, we check for the user’s current notification settings. This is an asynchronous task. In the completion handler for this task, we examine the current notification settings and based on that we take action. If the current authorizationStatus is .notDetermined, the user has never been asked for push permissions before so we need to ask them for permission. This is another asynchronous task with a completion handler. Depending on the permission status, we take the same actions that we would if we didn’t ask for permission first. The current implementation for this code is very explicit and somewhat wordy. It’s also less than ideal that two paths lead to the same outcome of this code which is to present the user’s notification permissions in the UI. There are two asynchronous tasks in this code: 1. Get the user’s current notification settings. 2. Ask the user for permission to send notifications if we haven’t already. Both of these tasks can be written as a Future: Donny Wals 142 Practical Combine extension UNUserNotificationCenter { func getNotificationSettings() -> Future<UNNotificationSettings, Never> { ,→ return Future { promise in self.getNotificationSettings { settings in promise(.success(settings)) } } } func requestAuthorization(options: UNAuthorizationOptions) -> Future<Bool, Error> { ,→ return Future { promise in self.requestAuthorization(options: options) { result, error in if let error = error { promise(.failure(error)) } else { promise(.success(result)) } } } } } I can’t stress it enough but futures in Combine are meant for one-off, immediately executing tasks. This means that as soon as requestAuthorization(options:) is called, the closure passed to the Future immediately executes even if we don’t subscribe to the created Future yet, and its result is broadcast to all future subscribers. For some operations this is good. In the case of requestAuthorization(options:) we wouldn’t want to trigger the Future more than once even if we’d subscribe to the Future multiple times. The two methods I added to UNUserNotificationCenter in the extension I just presented fit well within the example that I want to show you, and in my personal opinion that fit well for the concept I want to demonstrate. Whether or not a Future truly is the correct choice depends on your app, your requirements, and your goal. What matters to me most is that Donny Wals 143 Practical Combine you understand the implications and details of using a Future over a Subject or other Publisher. The two methods that I created can be used to rewrite the imperative code I showed earlier as follows: var cancellables = Set<AnyCancellable>() UNUserNotificationCenter.current().getNotificationSettings() .flatMap({ settings -> AnyPublisher<Bool, Never> in switch settings.authorizationStatus { case .notDetermined: return UNUserNotificationCen,→ ter.current().requestAuthorization(options: [.alert, ,→ .sound, .badge]) .replaceError(with: false) .eraseToAnyPublisher() case .denied: return Just(false).eraseToAnyPublisher() default: return Just(true).eraseToAnyPublisher() } }) .receive(on: DispatchQueue.main) .sink(receiveValue: { hasPermissions in if hasPermissions == false { // point user to settings } else { // we have permission } }).store(in: &cancellables) The new version of the code is not necessarily shorter than the initial version. It is, however, cleaner and less repetitive. There is now a single place where the UI is updated and by applying a flatMap, we can ask the user for notification permissions within our chain of operators quite neatly. Donny Wals 144 Practical Combine Notice that I used a new operator called receive in this example. The receive operator allows you to specify where you want to receive values emitted by a publisher. In this case, we want to work with the UI when we receive the user’s notification permissions, and there’s a good chance that we won’t receive the user’s notification permissions on the main thread if we don’t explicitly call receive. I will explain receive in-depth in Chapter 8 - Understanding Combine’s Schedulers. Let’s look at another cool example of how a Future can be used to wrap Core Data fetch requests. Using Futures to fetch data from Core Data If you’ve worked with Core Data before, you probably know how finicky threading can be in a Core Data environment. In my experience, you often end up writing fetch requests as follows: extension NSPersistentContainer { func fetchUsers(using moc: NSManagedObjectContext, _ completion: @escaping ([User]) -> Void) { ,→ moc.perform { let request: NSFetchRequest<User> = User.fetchRequest() let users = try? moc.fetch(request) completion(users ?? []) } } } struct UsersFetcher { let persistentContainer: NSPersistentContainer func fetchUsers(_ completion: @escaping ([User]) -> Void) { persistentContainer.fetchUsers(using: ,→ persistentContainer.viewContext, completion: completion) Donny Wals 145 Practical Combine } } The code in this example isn’t very special and if you’re familiar with Core Data this should look somewhat familiar. We can refactor this code to work with a Combine Future and remove all completion closures fairly easily: func fetchUsers(using moc: NSManagedObjectContext) -> Future<[User], Never> { ,→ return Future { promise in moc.perform { let request: NSFetchRequest<User> = User.fetchRequest() let users = try? moc.fetch(request) promise(.success(users ?? [])) } } } struct UsersFetcher { let persistentContainer: NSPersistentContainer func fetchUsers() -> Future<[User], Never> { return persistentContainer.fetchUsers(using: ,→ persistentContainer.viewContext) } } Notice how little code we had to change to get a Future based implementation. By wrapping the entire moc.perform block in a Future, we end up with a Combine-friendly version of fetchUsers(_:). Keep in mind that even though Combine hides a lot of complexity, you still need to keep Core Data’s threading confinements in mind if you abstract your fetch requests into futures. In this case, it would be good enough to use the UsersFetcher like this to avoid any problems: Donny Wals 146 Practical Combine usersFetcher.fetchUsers() .receive(on: DispatchQueue.main) .sink(receiveValue: { users in // handle users }) This might not always be the case so thread as careful as you normally would if you decide to add Future based abstractions to Core Data. In Summary In this chapter, I have shown you how Combine’s Future works. You learned that a Future is a Publisher but that it doesn’t behave like publishers you have seen so far. Futures in Combine mainly differ from publishers because a Future begins executing its work immediately when it’s created, and it replays its results to all subscribers that subscribe to the same Future. This is not always true for a Publisher. Furthermore, a Future is guaranteed to always produce a single value or error. This can’t be said for all objects that conform to Publisher. You saw how this behavior makes Future a good fit for abstracting existing asynchronous work that uses completion handlers in your app. I demonstrated this with two examples. In one example I showed you how you could wrap two methods from UNUserNotificationCenter to get a user’s current notification permissions and to ask for permission if needed. You also saw how you could abstract a Core Data fetch request using a Future. There are far more applications of futures that are feasible than I can reasonably cover in a single chapter. That’s why I chose two examples that do a good job of showing you how to refactor existing asynchronous code to work with a Future. It’s more important to focus on the refactoring process I’ve attempted to show you than it is to focus on the exact application of Future because, as I said, there are many, many more applications of Future that you could come up with. Donny Wals 147 Practical Combine Understanding Combine’s Schedulers A topic I haven’t touched upon in-depth so far is the topic of Combine’s schedulers. In this chapter, I aim to change this. I will explain what Combine’s schedulers are, what they do and more importantly how and when you will run into them when you work with Combine. Because this book is all about providing you with useful knowledge that you can apply in the real world. I won’t get down to the finest of details about this topic. Schedulers in Combine are heavily tied to dispatch queues, threads, and the runloop and I’m pretty sure I won’t be able to do all the elaborate details of those topics justice in a single chapter. By the end of this chapter, you will have a good understanding of what Combine’s Scheduler protocol is and what Combine’s receive(on:) and subscribe(on:) operators mean and do. Exploring the Scheduler protocol Because Combine deals with tons of asynchronous work, Combine must have a good way to schedule and execute work without blocking any threads. To help with this, Combine defines a protocol called Scheduler that defines all kinds of requirements that an object capable of performing work on behalf of Combine’s publishers. By default, several objects that you might be familiar with already conform to the Scheduler protocol: • RunLoop • DispatchQueue • OperationQueue Some of the possibly lesser-known objects that conform to Scheduler include OS_dispatch_queue_main and OS_dispatch_queue_global. These last two objects are not created by you directly. Instead, you typically gain access to them through DispatchQueue.main and DispatchQueue.global() respectively. Combine also provides a scheduler of its own called ImmediateScheduler which is a scheduler that always performs work that it’s asked to perform immediately. Any attempts to schedule a delayed task on this scheduler will result in immediate execution rather than delayed execution. Donny Wals 148 Practical Combine In Chapter 5 - Using Combine to respond to user input, you encountered schedulers for the first time when I showed you this code: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .assign(to: \.text, on: label) .store(in: &cancellables) In this example, Combine needs a scheduler to set up some timed work to make the debouncing work. Because timers and delayed tasks are inherently tied to schedulers, there is no way we could schedule our debouncer without passing a scheduler to the debounce operator. Note that in this case, DispatchQueue.main is likely to be an okay choice, but there is a potential problem. Timers are paused when the main queue is busy. For example, if you’re running a normal timer and begin scrolling through a table view, the timer will be paused until the scroll ends. In this case, it’s better to schedule the timer on a different queue than the main queue. The same appears to apply to schedulers in Combine. So in this case, we could use the following code if we want to make sure the main thread doesn’t interfere with the debouncing logic: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.global()) .assign(to: \.text, on: label) .store(in: &cancellables) Because Scheduler is a protocol, it’s possible to swap out DispatchQueue.global() for an instance of OperationQueue or a RunLoop. Ultimately they should provide the same functionality. Choosing one is a matter of context and choice in some cases. For example, RunLoop.main and DispatchQueue.main are both schedulers that operate on the main thread. Using an instance of OperationQueue as a scheduler is a bit trickier. In Combine, it’s expected that all schedulers operate as a serial queue. This means that every scheduler handles tasks one by one rather than in parallel. This is an important detail because Combine has no way of enforcing this and you might run into some interesting problems if you end up Donny Wals 149 Practical Combine getting this wrong. There is a thread on the Swift forums that you can go through for more information. Overall, I think it’s important that you understand that Combine uses schedulers internally but there is typically no need to get down to the details unless you have to. Now that you have an idea of what Combine’s Scheduler protocol is, let’s take a closure look at a common use case. Understanding receive(on:) and subscribe(on:) In Combine, there are two main operators that you can apply to your publishers that have a huge impact on the behavior of your code. These operators are receive and subscribe. Both take a Scheduler as their argument. While these operators look somewhat similar, their uses are very different from each other. In the previous chapter, I’ve shown you the receive(on:) publisher using the following code: var cancellables = Set<AnyCancellable>() UNUserNotificationCenter.current().getNotificationSettings() .flatMap({ settings -> AnyPublisher<Bool, Never> in switch settings.authorizationStatus { case .notDetermined: return UNUserNotificationCen,→ ter.current().requestAuthorization(options: [.alert, ,→ .sound, .badge]) .replaceError(with: false) .eraseToAnyPublisher() case .denied: return Just(false).eraseToAnyPublisher() default: return Just(true).eraseToAnyPublisher() } Donny Wals 150 Practical Combine }) .receive(on: DispatchQueue.main) .sink(receiveValue: { hasPermissions in if hasPermissions == false { // point user to settings } else { // we have permission } }).store(in: &cancellables) I used receive(on:) here because I wanted to make sure that my subscriber would receive all of its values on the main queue so I could safely update the UI. By default, Combine applies a default scheduler to the work you do. The default scheduler will emit values downstream on the thread that they were generated on. Let’s explore this behavior through an example: var cancellables = Set<AnyCancellable>() let intSubject = PassthroughSubject<Int, Never>() intSubject.sink(receiveValue: { value in print(value) print(Thread.current) }).store(in: &cancellables) intSubject.send(1) DispatchQueue.global().async { intSubject.send(2) } let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 queue.underlyingQueue = DispatchQueue(label: "com.donnywals.queue") Donny Wals 151 Practical Combine queue.addOperation { intSubject.send(3) } Examine the code in this snippet closely to see how I’m using three different origins to send value over the PassthroughSubject. If you would run this code in a Playground, you would see the following output: 1 <NSThread: 0x6000019da180>{number = 1, name = main} 2 <NSThread: 0x6000019d2500>{number = 7, name = (null)} 3 <NSThread: 0x6000019c6e80>{number = 3, name = (null)} It’s clear that the sink’s receiveValue is called on a different thread every time. This is a problem if we want to use the PassthroughSubject to drive UI. In Chapter 4 - Updating the User Interface I didn’t give this detail too much attention because I thought it would distract from the main idea of that chapter. Now that you’re more comfortable with Combine, it’s time to start paying more attention to the finer details which is why I’ve saved this topic for now. When you apply receive(on:) to a publisher it will make sure that all events are delivered downstream on the scheduler you provide. For example, you could modify the subscription from the previous example to always receive values on the main queue as follows: intSubject .receive(on: DispatchQueue.main) .sink(receiveValue: { value in print(value) print(Thread.current) }).store(in: &cancellables) I would recommend that you only apply the receive(on:) operator right before the sink or if you absolutely need something to happen on a specific thread. Donny Wals 152 Practical Combine The other operator I mentioned earlier, is the subscribe(on:) operator. This operator works it’s way upstream through the publisher stream and modifies the scheduler that is used for the publisher’s subscribe, request and cancel operations. Note that this does not mean that you can set the scheduler that upstream publishers output their values on. Just the thread that their subscribe method is called on. To give you an idea, let’s look at an interesting example of how this works exactly with a publisher chain that’s similar to the one you’ve used before: let intSubject = PassthroughSubject<Int, Never>() intSubject .subscribe(on: DispatchQueue.global()) .sink(receiveValue: { value in print(value) print(Thread.current) }).store(in: &cancellables) intSubject.send(1) intSubject.send(2) intSubject.send(3) If you run this code in a Playground (don’t forget to set PlaygroundPage.current.needsIndefiniteE = true), you will find that sometimes all three values are printed, but usually only the second and third value are printed. The reason for this is that the subscription to intSubject is now performed on the global dispatch queue which means it’s performed asynchronously on a different thread than the one we’re sending values from. This means that the first and second values may have already been sent before the subscription is set up. Try replacing DispatchQueue.global() in this example with ImmediateScheduler.shared. You’ll see that the subscription is now executed immediately. For a subject like PassthroughSubject, it’s unlikely that you’ll ever want to apply subscribe(on:). But there are cases where it’s extremely useful: func generateInt() -> Future<Int, Never> { return Future { promise in promise(.success(Int.random(in: 1...10))) Donny Wals 153 Practical Combine } } generateInt() .map({ value in sleep(5) return value }) .sink(receiveValue: { value in print(value) }).store(in: &cancellables) print("hello!") In this example, a random integer is generated using a Future. For the sake of testing, I use the map operator to do something expensive which is faked by calling sleep to pause execution for five seconds. If you run this code, you’ll find that nothing happens for five seconds, then the generated integer is printed, and then "hello" is printed. This situation is less than ideal. We can do much better by applying a subscribe(on:) operator: generateInt() .map({ value in sleep(5) return value }) .subscribe(on: DispatchQueue.global()) .sink(receiveValue: { value in print(value) }).store(in: &cancellables) If you apply the subscribe(on:) operator after the map, you’ll find that "hello" is printed immediately. This is much better than the old situation. Try moving the subscribe(on) around to see what happens: Donny Wals 154 Practical Combine generateInt() .subscribe(on: DispatchQueue.global()) .map({ value in sleep(5) return value }) .sink(receiveValue: { value in print(value) }).store(in: &cancellables) If you’d run the code like this, the effect is the same. The reason for this is that subscribe(on:) affects how objects subscribe to a publisher which means that it affects the entire chain of publishers and operators. Note that in this case, the future’s work is never affected by subscribe(on:). The reason for this is that a Future performs work before it has subscribers which means that the work it does can’t be affected by subscribe(on:). It’s possible to mix subscribe(on:) and receive(on:) to make sure your subscriptions are done off the main thread but values are received on the main thread: generateInt() .subscribe(on: DispatchQueue.global()) .map({ value in sleep(5) return value }) .receive(on: DispatchQueue.main) .sink(receiveValue: { value in print(value) }).store(in: &cancellables) Even though Combine’s operators to manipulate threading and queueing behavior of your publishers are simple, it doesn’t magically make threading easy. Use these operators with care and only if you need them. It’s far too easy to make bad assumptions about threading and cause problems in your app. Regardless, it’s good to be aware of these operators because Donny Wals 155 Practical Combine they can really help you clean up your code, and they allow you to take control over what happens where if needed. One last thing I want you to take note of is that publishers get to make their own decisions about the queue they use to emit values on. A lot of publishers will emit values on the queue they received their subscriber on, but this is not always the case. For example, try the following code to see what happens: URLSession.shared.dataTaskPublisher(for: URL(string: "https://practicalcombine.com")!) ,→ .subscribe(on: DispatchQueue.main) .map({ result in print(Thread.current.isMainThread) }).sink(receiveCompletion: { _ in }, receiveValue: { value in print(Thread.current.isMainThread) }) Even though we subscribe to the data task on the main thread, both the sink and map aren’t called on the main thread. The reason for this is that the DataTaskPublisher is set up to publish its values off the main thread at all times. I can only assume that this is done because URL requests are always performed off the main thread to avoid blocking the UI for a long time. In Summary In this chapter, I explained what the Scheduler protocol Combine is used for, and I gave you a rough overview of what a scheduler is. I explained that several built-in objects like DispatchQueue and Runloop conform to Scheduler and that you can use them to tell Combine where certain code should be executed. Then I went on to show you how you can use Combine’s receive(on:) and subscribe(on:) operators using several examples. You learned that receive(on:) allows you to specify on what scheduler values should be delivered downstream from your call to receive(on:). This is especially useful to make sure that your sink receives values on the main queue so you can safely update your UI. Donny Wals 156 Practical Combine After explaining receive(on:), I showed you subscribe(on:). This operator manipulates the scheduler that is used to create subscriptions on. You saw that this means that values can get lost in the ether when you use DispatchQueue.global() to subscribe to a PassthroughSubject that is expected to output values immediately. After that less than useful example, I showed you how you can use subscribe(on:) to move work that’s done in a map to a background thread to prevent blocking the main thread. Note that this isn’t guaranteed to always work because publishers can decide for themselves whether they want to emit values on the same thread that they received that subscriber on. Donny Wals 157 Practical Combine Building your own Publishers, Subscribers, and Subscriptions You’ve come a long way since you started learning Combine in Chapter 1 - Introducing Functional Reactive Programming. This chapter is the last chapter where I’ll show you how to use Combine, and it’s going to be a good one. You know how you can work with Combine in your apps now. You know exactly how Combine’s operators work, and how you can chain them together to build extremely powerful features with very little noise and boilerplate in your code. While Combine comes with a ton of built-in publishers and features, and Apple even actively discourages developers from building their own custom publishers, there are times where a custom publisher will scratch that itch that prevents you from writing the code you want to write. And even if you end up staying away from custom publishers in your app, I think it’s good that you have some insight into how publishers and subscribers work in Combine, and how they are tied together. In this chapter, I will show you how you can implement your own publishers. I will start by examining a built-in publisher and the sink subscriber. By examining the behavior of this pair, I will implement custom versions of this. Note that I will show you my interpretation and not the official implementation. My interpretation will work as advertised, but I know it’s not identical to Apple’s code. By implementing a custom publisher, we’ll run into some new terminology in Combine that I have been avoiding up until now. After reading this chapter, I truly believe that you will unlock Combine’s full potential because you will have a thorough understanding of Combine’s core principles. and more importantly, you’ll understand how you can use Combine effectively in your apps. Donny Wals 158 Practical Combine Understanding what happens when you subscribe to a publisher Throughout this book, you have seen countless code samples where I used sink to subscribe to a publisher. Every time the publisher emitted a new value, the sink’s receiveValue or receiveCompletion closures would be called and you know exactly what each of these closures does. What I haven’t looked into in-depth, is what happens when you call sink on a publisher. If you examine the documentation for sink, you’ll find that it’s a method defined on the Publisher protocol. This is why you can call sink on any publisher in the framework. If you do some more digging, you’ll find that Combine contains a Subscribers enum that contains a Sink object. Based on this, I think it’s safe to say that sink creates one of these Sink subscribers and subscribes it to the publisher. With this information, we can define a custom version of sink that looks as follows: extension Publisher { func customSink(receiveCompletion: @escaping (Subscribers.Completion<Self.Failure>) -> Void, ,→ receiveValue: @escaping (Self.Output) -> Void) -> AnyCancellable { ,→ let sink = Subscribers.Sink(receiveCompletion: receiveCompletion, ,→ receiveValue: receiveValue) self.subscribe(sink) return AnyCancellable(sink) } } I cannot stress enough that the code I present in this chapter is my interpretation that will usually work as intended. This code is not tested thoroughly enough to call it perfect, nor is it how sink is implemented in Combine. Notice that I call self.subscribe(sink) in this code snippet. The subscribe method Donny Wals 159 Practical Combine from Publisher is where the magic is. It’s where Combine connects a subscriber and publisher to each other to set up a subscription stream. Inside of a publisher’s subscribe method is where a publisher will start its work, and where the stream will get going. Let’s examine subscribe a bit more. There are multiple implementations of subscribe define on Publisher. One is meant to attach subjects like PassThroughSubject to a publisher. Doing this will automatically forward any events emitted by a publisher to the subscribing subject. Let’s look at a brief example: var cancellables = Set<AnyCancellable>() let publisher = [1, 2, 3].publisher let subject = PassthroughSubject<Int, Never>() subject .sink(receiveValue: { receivedInt in print("subject", receivedInt) }).store(in: &cancellables) publisher .subscribe(subject) .store(in: &cancellables) Because subject in this code is directly subscribed to publisher, all values emitted by publisher are automatically republished by subject. While this is pretty cool and could be useful in certain cases, this isn’t the subscribe method that is used in the custom sink I showed you earlier. In that method, I use the other version of subscribe which takes a Subscriber as its argument. The documentation for subscribe isn’t particularly helpful at the time of writing this chapter. But if you examine the documentation for Publisher, you’ll find some interesting information about subscribe. The documentation mentions a method called receive(subscriber:). This method is a required method on the Publisher protocol, and it’s called whenever any version of subscribe is called on a publisher. The documentation also mentions that a publisher Donny Wals 160 Practical Combine uses its receive(subscriber:) method to receive new subscribers and to call several methods on the subscriber to interact with it: • receive(subscription:) is called on the subscriber to pass it a subscription. The subscriber can use this subscription to demand new items and to cancel the subscription. I will explain this in-depth later in this chapter. • receive(_:) is called on a subscriber to pass it a new value. • receive(completion:) is called on a subscriber to pass it a completion event. These three methods happen to be methods that are required by the Subscriber protocol. Implementing a custom Subscriber Because I started this section off with a custom implementation of the sink operator I think it’s fitting to implement a custom version of Subscriber.Sink to see the three receive methods I just listed in action. extension Subscribers { class CustomSink<Input, Failure: Error>: Subscriber { let receiveValue: (Input) -> Void let receiveCompletion: (Subscribers.Completion<Failure>) -> Void var subscription: Subscription? init(receiveCompletion: @escaping ,→ (Subscribers.Completion<Failure>) -> Void, receiveValue: @escaping (Input) -> Void) { self.receiveCompletion = receiveCompletion self.receiveValue = receiveValue } func receive(subscription: Subscription) { self.subscription = subscription Donny Wals 161 Practical Combine subscription.request(.unlimited) } func receive(_ input: Input) -> Subscribers.Demand { receiveValue(input) return .none } func receive(completion: Subscribers.Completion<Failure>) { receiveCompletion(completion) } } } This code defines an extension on Subscribers and adds a new class called CustomSink to the Subscribers enum. I do this to make my custom subscriber fit in with Combine’s other subscriber objects nicely. The CustomSink is generic over an Input and a Failure. The Input will match up with the Output of a Publisher, and the Failure will match up with the Failure type of the Publisher we’re subscribing to. Just like the regular Subscribers.Sink, the custom version will take two closures. One for incoming values, and one for incoming completion events. Note that my custom sink’s receive(_:) and receive(completion:) call these closures directly. This will allow the custom sink’s behavior to match that of the built-in one. In receive(_:) I return a value of .none. I will explain that in a moment, let’s look at receive(subscription:) first. In this method, I store the received Subscription object. The reason for this is so we can call cancel on the subscription, or cancel it if needed. Note that I also call subscription.request(.unlimited) here. This matches up with the definition of Subscribers.Sink which states the following: A simple subscriber that requests an unlimited number of values upon subscription. You’ve seen the concept of requesting or demanding values a couple of times now, and you’ve also seen an object called Subscribers.Demand which is the return type of receive(_:). This concept is one of Combine’s driving forces, and it’s called backpressure or backpressure management. In the next subsection, I will explore this topic with you. But Donny Wals 162 Practical Combine before we do, I want to wrap up the custom sink that I just created because it’s not quite ready. Remember that the following line from the customSink method I implemented earlier? return AnyCancellable(sink) To wrap a Subscriber in an AnyCancellable, it needs to conform to Cancellable. We can make Subscribers.CustomSink conform to Cancellable quite easily: extension Subscribers.CustomSink: Cancellable { func cancel() { subscription?.cancel() subscription = nil } } All we do here is cancel the subscription, and set it to nil to prevent memory leaks. The CustomSink can now be used as follows: extension Publisher { func customSink(receiveCompletion: @escaping (Subscribers.Completion<Self.Failure>) -> Void, ,→ receiveValue: @escaping (Self.Output) -> Void) -> AnyCancellable { ,→ let sink = Subscribers.CustomSink(receiveCompletion: ,→ receiveCompletion, receiveValue: receiveValue) self.subscribe(sink) return AnyCancellable(sink) } } There’s hardly a difference with the initial version of customSink. The only thing that I’ve changed is that sink is now an instance of Subscribers.CustomSink which is exactly Donny Wals 163 Practical Combine what I was going for. Now that the custom subscriber is finished, let’s explore the concept of backpressure before we move on to creating a custom publisher. Understanding backpressure Backpressure is a feature in Combine that is extremely important to make everything work the way it does, but it’s also a feature that’s hidden from users of the framework relatively well. You can get pretty far with Combine without realizing that backpressure even exists, but if you want to understand how Combine works, and possibly create custom subscribers or even publishers, you must understand backpressure. In Combine, subscribers are in charge of the number of items they receive, if any at all. By requesting, or demanding, values from a subscription object, a subscriber can communicate to a subscription whether it’s prepared to receive values. This can help you prevent your system from backing up completely if a subscription is sending a subscriber more values than it can handle. The way it works is that a subscriber will use its receive(subscription:) method to request an initial number of items. In the case of Subscribers.Sink, this is an unlimited number of items. This means that Subscribers.Sink will receive all values published by a publisher, no matter how rapidly those values are emitted, or how many values are generated, until the subscription is canceled. This kind of unlimited demand is represented through the Subscribers.Demand.unlimited object. Alternatively, we could request an initial demand of .none which means no values at all or .max(Int) where Int represents a maximum number of values equal to the value that’s passed to it. To communicate an initial demand of a single value, we could write the following: func receive(subscription: Subscription) { self.subscription = subscription subscription.request(.max(1)) } This code would result in the subscriber receiving a single value, and then no further values until the subscriber calls subscription.request(_:) again with a new demand, or if it returns a new demand larger than .none from its receive(_:) method. Donny Wals 164 Practical Combine In my custom sink I implemented receive(_:) as follows: func receive(_ input: Input) -> Subscribers.Demand { receiveValue(input) return .none } This is perfectly fine because I already requested an unlimited number of values from the subscription. Returning .none from receive(_:) does not negatively affect the initial demand from the subscriber. Demands in Combine can only be increased. You can never decrease a subscription’s demand. This means that you can’t lower the number of items you want to receive by requesting something like .max(-1). If you have an initial demand that isn’t .unlimited, but for example, you requested .max(1), you can return a new demand from receive(_:) as follows: func receive(_ input: Input) -> Subscribers.Demand { receiveValue(input) return .max(1) } This code will request one more value from the subscriber every time it received a value. A subscription will only send values to a subscriber if demand is high enough. This means that if you’d request an initial number of .max(10) values, and always return .none from receive(_:), your subscription will stop receiving new values after the tenth item is delivered to your subscriber. It’s important to note that this means that you will also not receive any completion events from the publisher you’ve subscribed to if the subscriber’s demand doesn’t allow this. A subscriber can call request(_:) on a subscription at any time to communicate that it’s ready to receive new values. This contract of how and when a subscriber will or will not receive new values due to backpressure is extremely important and it’s a huge part of why Apple recommends that you don’t create custom publishers. Apple’s publishers are supposed to honor all of the implicit and explicit details of how publishers send values to their subscribers, and the corresponding subscription objects are perfectly tuned to work together with their publishers as needed. Donny Wals 165 Practical Combine Getting backpressure management right in a custom publisher is no easy task but we’re going to explore it regardless because I want to show you the basics of how publishers can be implemented so you can make your own choices when the time comes to decide whether you should create a custom publisher. Understanding how you can create a custom publisher and subscription Creating a custom publisher is an interesting task because Apple doesn’t provide many details on how to do it. All we know is that an object that conforms to the Publisher protocol has to implement a receive(_:) method, and that we’re expected to connect a subscription and a subscriber in this method. What’s interesting is that we can derive a little bit of behavior or information from the way things work in Combine. Remember that I used URLSession.DataTaskPublisher to explain and demonstrate that a publisher doesn’t begin working until it receives a subscriber? And that multiple subscriptions to the same URLSession.DataTaskPublisher result in multiple network calls being made? This tells us something interesting about publishers. A single publisher does not necessarily generate a single stream of values. In other words, a publisher might not be responsible for generating its own values. This is an interesting thought that we can explore a little bit deeper. Consider the following code: var cancellables = Set<AnyCancellable>() let ints = [1, 2, 3].publisher ints .sink(receiveValue: { print($0 )}) .store(in: &cancellables) ints .sink(receiveValue: { print($0 )}) .store(in: &cancellables) Donny Wals 166 Practical Combine The full array of integers will be presented both times you subscribe to ints. Based on this, it’s safe to say that a publisher’s completion state isn’t directly tied to the publisher itself. The subscription that’s created by the publisher is ultimately responsible for initiating and emitting a stream of values. A publisher is just an object that acts as some kind of placeholder, or facade for whatever happens internally. This is why Future is such an interesting publisher because it doesn’t follow the same narrative. When you subscribe to a Future, it executes immediately, and it executes only once. To see how this publisher of integers might work, let’s implement a custom version of it! extension Publishers { struct IntPublisher: Publisher { typealias Output = Int typealias Failure = Never let numberOfValues: Int func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = ,→ Subscriptions.IntSubscription(numberOfValues: ,→ numberOfValues, subscriber: subscriber) ,→ subscriber.receive(subscription: subscription) } } } This publisher isn’t terribly exciting, is it? The publisher I’ve defined publishes integers and never fails. The most interesting bit is that in the receive(subscriber:) method I create a new subscription object, and pass it to the subscriber. Let’s look at the implementation of my Subscriptions.IntSubscription to see if it’s any more exciting: Donny Wals 167 Practical Combine extension Subscriptions { class IntSubscription<S: Subscriber>: Subscription where S.Input == Int, S.Failure == Never { ,→ let numberOfValues: Int var currentValue = 0 var subscriber: S? var openDemand = Subscribers.Demand.none init(numberOfValues: Int, subscriber: S) { self.numberOfValues = numberOfValues self.subscriber = subscriber } func request(_ demand: Subscribers.Demand) { openDemand += demand while openDemand > 0 && currentValue < numberOfValues { if let newDemand = subscriber?.receive(currentValue) { openDemand += newDemand } currentValue += 1 openDemand -= 1 } if currentValue == numberOfValues { subscriber?.receive(completion: .finished) cancel() } } func cancel() { subscriber = nil Donny Wals 168 Practical Combine // we don't have anything to clean up } } } It certainly is a lot more code, and the interesting bit is the request(_:) method’s implementation. In it, I store the new value for the demand that we need to fulfill. This will be equal to the current demand, plus the newly requested demand. Because demands are always additive and we can use the + operator on two demands, it’s easy to add the new demand to the existing demand. Because this is a simple published and all work can be done synchronously, I used a while loop to send values to the subscriber that was passed to this subscription’s initializer. This loop will run until we have no open demand anymore, or until we’ve published all of the values we needed to publish. When we call subscriber?.receive, the subscriber can update its demand, so we need to add this new demand to the open demand. We then increase the current value by one, and the open demand is decreased by one because we just sent a value to the subscriber. After the loop, I check whether all values were sent to the subscriber. If this is the case, the subscription is completed and the subscriber is informed. If not, we’ll have to hold off on sending new values to the subscriber until it requests them from this subscription. This example of creating a custom subscription and publisher is purely intended to give you an idea of how they work. They are not intended to be used as a definitive reference of how publishers and subscribers work exactly, and they are not intended to be used as a definitive reference on how to perfectly manage backpressure in a subscription. Let’s look at a final code snippet that uses all of the custom objects I’ve created in this section: let customPublisher = Publishers.IntPublisher(numberOfValues: 10) customPublisher .customSink(receiveCompletion: { completion in Donny Wals 169 Practical Combine print(completion) }, receiveValue: { int in print(int) }) .store(in: &cancellables) Try playing around with this code and the custom sink to see how the custom publisher adapts to different kinds of demand strategies. It works pretty well, especially if you consider it for what it is. It’s just a silly example to help you grasp the concept of backpressure management and to give you an idea of how publishers are created. Even though we’re not supposed to create custom publishers, and backpressure management is super complicated if you want to get it right all the time, I want to show you an example of a custom publisher that I think you’ll like. It’s a publisher that will allow you to subscribe to UIControl events with a very nice API which will allow you to improve some of the code I’ve shown you in Chapter 4 - Updating the User Interface. Extending UIControl with a custom publisher The idea of extending UIControl with a Combine publisher is no way unique. There is a GitHub repository with all kinds of extensions for UIKit components that define custom publishers that you can use in your projects. The code that I’m about to show you in this section is heavily inspired by this project, except it’s somewhat simplified. The final code I want to end up with is the following: slider .publisher(for: .valueChanged) .sink(receiveValue: { control in guard let slider = control as? UISlider else { return } Donny Wals 170 Practical Combine print(slider.value) }).store(in: &cancellables) Looks good, doesn’t it? To achieve this, we’ll need to extend UIControl with a method that allows us to call publisher(for:) with a UIControl.Event that represents the event we want to create a publisher for. In the case of a text field, you are most likely interested in .valueChanged but we should be able to pass any other UIControl.Event to publisher(for:) to achieve maximum flexibility. The publisher(for:) method will create a custom publisher object. I’ll show you the publisher and its corresponding subscription first before I define publisher(for:). Before you read on though, think about what the code you’re about to read should look like for a moment. You’ve already seen all of the core components of what we’re about to do, I’m just going to change a couple of details. If you’re feeling particularly confident and adventurous, I highly recommend to stop reading now and try to implement this entire publisher and subscription pair yourself. See how far you get with it. I’m sure you’re going to do much better than you might think! The code for the publisher is pretty much identical to the publisher I’ve shown you before: extension UIControl { struct EventPublisher: Publisher { typealias Output = UIControl typealias Failure = Never let control: UIControl let controlEvent: UIControl.Event func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { ,→ let subscription = EventSubscription(control: control, event: ,→ controlEvent, subscriber: subscriber) subscriber.receive(subscription: subscription) } Donny Wals 171 Practical Combine } } The publisher is initialized with an instance of UIControl and the UIControl.Event that this publisher is expected to send values for. Note that I’ve used the UIControl itself as the publisher’s Output. This makes it easy for subscribers to know what the origin of the value was, and they can use the provided UIControl instance to read new values from. Let’s look at the far more exciting EventSubscription object next: extension UIControl { class EventSubscription<S: Subscriber>: Subscription where S.Input == UIControl, S.Failure == Never { let control: UIControl let event: UIControl.Event var subscriber: S? var currentDemand = Subscribers.Demand.none init(control: UIControl, event: UIControl.Event, subscriber: S) { self.control = control self.event = event self.subscriber = subscriber control.addTarget(self, action: #selector(eventOccured), for: event) } func request(_ demand: Subscribers.Demand) { currentDemand += demand } func cancel() { subscriber = nil Donny Wals 172 Practical Combine control.removeTarget(self, action: #selector(eventOccured), for: event) } @objc func eventOccured() { if currentDemand > 0 { currentDemand += subscriber?.receive(control) ?? .none currentDemand -= 1 } } } } In the initializer for this subscription object, I immediately call addTarget(_:action:for:) on the control that’s passed to the initializer. This means that this subscription will immediately begin receiving updates for this UIControl even if the subscriber isn’t interested in any events just yet. In this subscription’s request(_:) method I only increase the currentDemand. We don’t send any values when the subscriber requests them because the purpose of this subscriber is to send values when the specified UIControl.Event occurs. The cancel method on this subscription is pretty straightforward. I set the subscriber to nil, and remove the subscription from the control’s targets. The most interesting bit of this custom subscription is in eventOccured. In this method, the subscriber’s receive method is called if it sent sufficient demand, and the current demand is increased with the subscriber’s new demand and decreased by one because we just sent a value to the subscriber. Even though this custom publisher and subscription don’t contain a ton of code, it’s actually a pretty advanced and complicated component that I’ve just shown you. Let’s look at the publisher(for:) method now so you can see how this publisher is used: extension UIControl { func publisher(for event: UIControl.Event) -> ,→ UIControl.EventPublisher { Donny Wals 173 Practical Combine return UIControl.EventPublisher(control: self, controlEvent: ,→ event) } } It’s that simple! Crazy, right? And you can now use publisher(for:) in the exact way I mentioned at the start of this section. You can even apply all of Combine’s operators on this custom publisher. For example, you might want to use debounce to prevent this publisher from firing new values all the time while the user is dragging a slider: slider .publisher(for: .valueChanged) .debounce(for: 0.2, scheduler: DispatchQueue.main) .sink(receiveValue: { control in guard let slider = control as? UISlider else { return } print(slider.value) }).store(in: &cancellables) Creating custom publishers can be really powerful, and a lot of fun. Regardless, I think it’s good to take Apple’s advice and only create your own publishers when there is no other way to reasonably achieve the functionality you need. This might sound very conservative, but publishers and subscriptions can be complex beasts and backpressure management can be hard to get right, especially for more complex asynchronous tasks. In Summary What a chapter this was. Everything you’ve learned about Combine came together. I hope I was able to make the final pieces of the puzzle fit in your mind by giving you an idea of how Combine works behind the curtain. In this chapter, you have learned how subscribers, publishers, and subscriptions work. You learned why publishers don’t start doing work until they have a subscriber that requests Donny Wals 174 Practical Combine sufficient demand, and you’ve learned what demand is and how it works. You learned about one of Combine’s hidden cornerstones which is its backpressure mechanism. I’ve shown you how you can mimic Combine’s built-in sink method, and then I went on to show you how you can create an extension on UIControl to create a custom publisher that allows you to write beautiful and convenient code. In the next chapter, you will learn one last valuable skill. You will learn how you can debug and profile your Combine publishers through the built-in print operator, and a tool called Timelane. Donny Wals 175 Practical Combine Debugging your Combine code Being able to write reactive code is a fantastic skill. An equally fantastic skill is being able to debug your reactive code. Because Combine’s publishers are inherently asynchronous, it’s not always easy to debug Combine code. In earlier chapters, I have shown you that you can use Combine’s print operator to help you understand what your code does. In this chapter, I will explain print a little bit more in-depth and I will show you how you can use a tool called Timelane to help you understand your Combine code on a deeper level. By the end of this chapter, you should be able to use print and Timelane with confidence to help you gain insight into your code. I will use examples from earlier chapters to demonstrate print and Combine. That way you should be familiar with the concepts I present while gaining new insights that you may not have had otherwise. Using print to debug your code One of the simplest tools for debugging in any developer’s toolbox is the print statement. Pretty much every language or SDK you can develop with has some way of printing values to a console where you can inspect them to see whether your program produces the expected outputs at the expected times. Combine provides its own print operator which prints information about a publisher, it’s subscriptions and the values that the publisher emits to Xcode’s console. In Chapter 3 - Transforming publishers I showed you the following code: [1, 2, 3].publisher .print() .flatMap(maxPublishers: .max(1), { int in return Array(repeating: int, count: 2).publisher }) .sink(receiveValue: { value in // handle value }) This code produced the following output: Donny Wals 176 Practical Combine receive subscription: ([1, 2, 3]) request max: (1) receive value: (1) request max: (1) receive value: (2) request max: (1) receive value: (3) request max: (1) receive finished In the previous chapter you learned a lot about subscriptions so this output should be much more meaningful than the first time I showed it to you. You can see when our subscription is created, how many values the subscriber requests and what outputs are produced. Note that this output is somewhat complicated because it involves a flatMap with a maxPublishers of one. This means that the flatMap now acts as a subscriber that requests a single value from its upstream publisher initially. It also requests a single new value every time the publisher that’s created within the flatMap completes. While this is somewhat complex, it all makes sense when you know what’s happening. If you have no idea how flatMap works, this output might not be extremely useful. Moreover, in many real applications, you will have multiple publishers running at the same time and your code might log a lot more data than we did in this simple example. Luckily, there’s a tool available that can help us debug and understand Combine code in a much more convenient way. Using Timelane to understand and debug your code While printing is a convenient way to do some quick and dirty debugging, there are often better tools to understand and validate your code than printing tons of information to your console. Timelane is a convenient open-source tool that was developed by Marin Todorov to help developers understand and debug their Combine subscriptions in-depth. In this section, Donny Wals 177 Practical Combine you will learn how you can install and use this super valuable tool. I will guide you through the installation process (which is fairly straightforward) and we’ll look at some neat examples to see Timelane in action. Preparing to use Timelane To use Timelane, there are two things you need to do. First, you need to install the Instruments template that is used to visualize your data streams. Second, you need to add the TimelaneCombine dependency to your project. Note that there is also a RxTimelane framework available that allows you to use Timelane to profile RxSwift code. In this post, I will focus on Combine but the RxSwift version works in the same manner as the Combine version. To install the Timelane Instruments template, go to the Timelane releases page on Github and download the Timelane app zip file. Open the downloaded application and follow the installation instructions shown in the app: Donny Wals 178 Practical Combine Figure 18: Screenshot of the Timelane installer app After installing the Instruments template, you can go ahead and open Xcode. The easiest way to integrate Timelane is through the Swift Package Manager. Open the project you want to use Timelane in and navigate to File -> Swift Packages -> Add Package Dependency. Donny Wals 179 Practical Combine Figure 19: Screenshot of the Xcode menu to access Swift Package Manager In the pop-up that appears, enter the TimelaneCombine Github URL which is: https://github.com/icanzilb/TimelaneCombine Donny Wals 180 Practical Combine Figure 20: Screenshot of the Add Package screen with the TimelaneCombine URL prefilled Adding this package to your project will automatically pull down and install the TimelaneCombine package in your project. If you’re using Cocoapods or Carthage to manage your dependencies you can add the TimelaneCombine dependency to your Podfile or Cartfile as needed. Using Timelane to debug a simple sequence Once you have installed the Timelane Instruments template and added the TimelaneCombine dependency to your project, you can start using Timelane immediately using the lane operator it provides. Let’s look at a simple example first. In Chapter 5 - Using Combine to respond to user input, I introduced the debounce operator by showing you the following code. $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) Donny Wals 181 Practical Combine .assign(to: \.text, on: label) .store(in: &cancellables) This code is used to prevent the $searchQuery publisher from emitting too many values in a short time. However, it had a problem that we discovered using the print operator. Let’s use the lane operator this time to discover the same problem: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .lane("Search query") .assign(to: \.text, on: label) .store(in: &cancellables) By applying the lane operator after debounce, you can inspect every event that is emitted by the debounce operator. This means that we’ll see all the debounced values. The string that is passed to the lane operator is used to label the lane that’s used to display the output of debounce. To use Timelane for debugging, you need to run your app through Instruments. You can do this by pressing cmd+i or through Xcode’s menu by going to Product -> Profile. When Instrument launches, make sure to select the Timelane template: Donny Wals 182 Practical Combine Figure 21: A screenshot of the Instruments template selector with Timelane selected When you run your app with Instruments, your Combine code is profiled and visualized in realtime. This means that you can inspect your subscriptions in great detail. If you run the above code and you type something, remove a character and retype that character, the Instruments log might look as follows: Donny Wals 183 Practical Combine Figure 22: A screenshot of a profiled Timelane session First, notice the big green line in the top Instruments lane. This line resembles our subscription to the $searchQuery publisher. The line starts when the subscription is created (which is immediately) and it runs until the subscription is completed. I stopped the recording session after I captured a couple of values so the line ends. In reality, this line would run until the point where the subscription is destroyed because this specific never completes on its own. In the second lane, you can see values over time. This lane represents the values that are emitted. In the bottom section of the Instruments window, you can see more details about the emitted events. Notice that the output Timelane is duplicated. This is because I typed Timelane, waited a moment, removed a letter and then added the removed letter back. I can demonstrate this by adding an extra lane operator: Donny Wals 184 Practical Combine $searchQuery .lane("Raw search query") .debounce(for: 0.3, scheduler: DispatchQueue.main) .lane("Search query") .assign(to: \.text, on: label) .store(in: &cancellables) Performing the same actions results in the following Instruments output: Figure 23: A screenshot of a Timelane session with two lanes Notice that there are two lanes now. One for each time the lane operator is used. You can see that the Raw search query lane shows all the characters I typed without debouncing them. The Search query lane is debounced and shows fewer values because it only displays values that are emitted by the debounce operator. A lane operator only works on the publisher Donny Wals 185 Practical Combine that it is applied to. This is very convenient because it allows you to gain really good insights into the values that travel through your publisher chain. If you’re working with many different lanes, you might want to limit the data you track in Instruments. In the case of $searchQuery, you might not be interested in seeing its subscription lifecycle. If this is the case for you, you can apply a filter to the data that is logged by Timelane. This means that you can control whether you want a certain lane to show up under Subscriptions only, Events over time only or both. You can apply a filter as follows: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .lane("Search query", filter: [.event]) .assign(to: \.text, on: label) .store(in: &cancellables) By passing [.event] to the lane operator, I can tell it to only log values that are emitted and ignore the subscription lifecycle. If you’d want this to work the other way around and only see the subscription lifecycle, you can pass [.subscription] to the lane operator instead. Let’s look at an example session for the code I just showed you: Donny Wals 186 Practical Combine Figure 24: A screenshot of a Timelane session that only tracks events for the created lane Notice how there are events logged, but the top lane of the Instruments session is empty. This is extremely useful when you’re working with lots of lanes and want to minimize the noise in your Instruments sessions. Speaking of noise, because we’re sending optional values to Instruments, they get logged as Optional("Timelane"). This isn’t ideal and we can improve this using a third argument for the lane operator: $searchQuery .debounce(for: 0.3, scheduler: DispatchQueue.main) .lane("Search query", filter: [.event], transformValue: { value in return value ?? "" }) .assign(to: \.text, on: label) Donny Wals 187 Practical Combine .store(in: &cancellables) In this example, I use the transformValue argument and a closure that unwraps the optional value into the value or an empty string to get rid of the Optional part of the output. You can use the transformValue argument to perform all kinds of transformations on the data your logging. Imagine that you’re working with some complex values like user models. You can use transformValue to extract and log a specific property of the user instead of logging the entire object: aPublisherThatEmitsUsers .lane("Users", filter: [.event], transformValue: { user in return "User with id: \(user.id)" }) The ability to transform values is extremely useful and it can help you produce a nice and clean Instruments log that only contains the output you need. In addition to using Timelane through the lane operator, it also has a dedicated property wrapper that allows you to debug @Published properties. Whenever you have a property that is defined as follows: @Published var searchQuery: String = "" You can replace it with Timelane’s @PublishedOnLane property as follows: @PublishedOnLane("Search query") var searchQuery: String = "" This will log every value that is emitted by the $searchQuery to a lane that’s labeled as Search query. Since @Published publishers often live for a long time and you might not be interested in visualizing their subscription lifecycle, you can apply a filter to them as follows: @PublishedOnLane("Search query", filter: [.event]) var searchQuery: ,→ String = "" Donny Wals 188 Practical Combine Using Timelane to debug a sequence of publishers Now that you understand how Timelane displays information in Instruments, and how you can filter the data that is logged, let’s look at a more complex example of an Instruments session that traces the complex networking sequence from Chapter 6 - Using Combine for networking. Before I show you the relevant Instruments session I want to refresh your mind and show you the diagram of the work that was done: Figure 25: The sequence of complex tasks from Chapter 6 I added a call to the lane operator at the end of every publisher from the section in Chapter 6 - Using Combine for networking where we built this complex chain of network calls. I will not repeat all of the code in this chapter so if you want to refer to the code you can look at Chapter 6 - Using Combine for networking, or you can have look at the sample code for this chapter in the book’s code bundle. When you run Timelane to visualize the complex flow of subscriptions in this example, you get the following output: Donny Wals 189 Practical Combine Figure 26: A screenshot of a Timelane session for the complex network chain from Chapter 6 Notice how all publishers create their subscriptions at the same time. They all finish as soon as their work is done, and the Homepage publisher lane doesn’t complete until the Curated publisher completed because that’s the last publisher to complete. Once the last publisher completes, the home page publisher can merge the output from the curated, featured and favorites publishers. Note that the Favorites publisher lane completes together with the Remote favorites publisher because it depends on the local and remote favorites to emit values. While this is really cool, let’s see what happens if one of the steps in this complex process fails: Donny Wals 190 Practical Combine Figure 27: A screenshot of a Timelane session for the complex network chain from Chapter 6 where one task fails This output is extremely interesting. The Featured publisher failed before the Curated publisher could complete. This causes the Curated publisher to be canceled, and the Homepage publisher completes with an error. The curated publisher is canceled because it doesn’t make sense for that publisher to do any work once one of the publishers that the homepage publisher is supposed to merge failed. When one of the three publishers that the homepage publisher depends on fails, it will emit that error immediately, and the stream is completed. The fact that the curated publisher is canceled because it was still running when the featured publisher failed is something you probably would not have discovered without Timelane and its powerful visualizations. Donny Wals 191 Practical Combine In Summary In this chapter, you learned a lot about debugging with Combine. You saw how the print operator provides quick insights into what a publisher and subscription do exactly. You also saw how you can gain more structured, deep insights into your code through Timelane. By analyzing and visualizing your code, you can gain valuable insights that can help track down and fix problems. I like Timelane a lot for debugging and I wouldn’t be surprised if Apple adds native support for the kind of debugging that it provides in the near feature. Now that you know how to debug your Combine code, there is just one more thing I want to show you in this book. Being able to write unit tests is an important skill for developers so in the next chapter you will learn how to write unit tests for your Combine code. Donny Wals 192 Practical Combine Testing code that uses Combine An important skill to have as a software developer is the ability to write good unit tests. A good test suite helps you verify that your code works as expected and ideally your test suite also ensures that your code can handle edge cases without trouble. Writing a good test suite is not an easy task though. You can’t just take any codebase and write a good test suite for it. You need to carefully craft your codebase to be testable by creating the right abstractions in the right places. And even if you get this balancing act right, it’s not always easy to write tests. Especially if the code you’re writing tests for runs asynchronously. Combine code runs asynchronously by its very nature, which makes it non-trivial to test. If you’re familiar with unit testing, it’s only logical that you may have been wondering about tests throughout reading this book. In this chapter, I will show you how you can test your Combine code. You will learn to do this through the following topics: • Writing tests for code that uses Combine • Optimizing Combine code for testability • Creating helpers to make testing Combine code easier By the end of this chapter you will have a good understanding of what it means to test Combine code. You will not just learn how to write tests, but you will also have an understanding of what you should and what you should not test when you’re using Combine. The last thing you want in a test suite is to have unit tests that don’t test the logic you think you’re testing because it’s testing the wrong thing. Note: In this chapter I will assume that you have basic knowledge of unit testing for iOS. I will briefly explain some basics, but if you’ve never written a unit test before there’s a good chance you will feel lost while going through this chapter. In that case, I would like to recommend that you take a look at the testing section on my blog. Specifically I would like to recommend Getting started with unit testing on iOS – part 1 and Getting started with unit testing on iOS – part 2. Donny Wals 193 Practical Combine Writing tests for code that uses Combine When you’re writing code that you plan to write tests for, it’s important that you make sure that you can mock, fake or stub the parts of your code that are not essential to your test. This means that you should at least make sure that you don’t rely on a network connection, databases, the file system or any other external source that you don’t explicitly control during your unit tests. Relying on external sources will make your test suite unreliable and prone to mistakes. After all, you don’t want your unit test to fail because another test manipulated the database in a way that your current test did not expect. If that happens your unit test would make it look like your code is flawed while in reality your code didn’t receive the input it expected. Or possibly worse, what if your code is flawed but it’s hidden because the test that happens to run before the test that tests your flawed code happens to set up the environment a way that favors your faulty code. Your test would pass as long as the tests run in the correct order and your bug could go unnoticed for much longer than you should be comfortable with. So how does testing work with Combine? To figure that out, it’s important to have a baseline understanding of what we should, and should not test. Let’s look at the Car model I introduced in Chapter 4 - Updating the User Interface: class Car { @Published var kwhInBattery = 50.0 let kwhPerKilometer = 0.14 } Imagine that we wanted to write a test for this model. Take a moment to examine the model and ask yourself what you’d like to test here. Or in other words, what guarantees would you like to be able to make about this code. A good test I can think of is to make sure that no mutations or side-effects are applied when a new value is assigned to kwhInBattery. Additionally, I’d want to make sure that every new value I assign to kwhInBattery is sent to subscribers of the $kwhInBattery publisher. Note that this last test sits right on the border of what we should, and should not test. The reason it’s on the border is that we should not be testing whether Combine does its job. Since Donny Wals 194 Practical Combine kwhInBattery is marked as @Published, we should be able to assume that everything works as expected. However, I still want to write the test I just mentioned because it also serves as a form of documentation and we could remove the @Published annotation at any time if we don’t verify that we can subscribe to $kwhInBattery. By writing a test that subscribes to $kwhInBattery and validates its output, we formalize that kwhInBattery is @Published rather than testing whether @Published works as expected. Let’s see how you could set up a test that validates that no side-effects are applied to kwhInBattery when we assign it, and a test that validates that we get the expected values in our sink. The following code sets up the skeleton for the test: class CarTest: XCTestCase { var car: Car! var cancellables: Set<AnyCancellable>! override func setUp() { car = Car() cancellables = [] } func testKwhBatteryIsPublisher() { let newValue: Double = 10.0 var expectedValues = [car.kwhInBattery, newValue] let receivedAllValues = expectation(description: "all values ,→ received") car.$kwhInBattery.sink(receiveValue: { value in // we'll write the assertion logic here }).store(in: &cancellables) car.kwhInBattery = newValue Donny Wals 195 Practical Combine waitForExpectations(timeout: 1, handler: nil) } } This test contains two properties, one to hold the Car instance that’s being tested and another to store the AnyCancellable objects that are created during the test. In the test’s setUp method these properties are initialized with fresh instances. This ensures that every test we write has a fresh Car and set of AnyCancellable to work with. In the testKwhBatteryIsPublisher test method, I create an array of expected values. These are the values that I expect to receive in my sink. If everything works as expected, I should receive the default kwhInBattery value first, and my newValue second. I’ve added these values to the expectedValues array in the order that I expect the values to be emitted in. I also create an XCTestExpectation using XCTestCase’s expectation(description:) method. This method creates an expectation object that must be fulfilled to consider the test completed. Notice the last line in the test method where I call waitForExpectations(timeout:handler:). Calling that method will pause execution of the test until all XCTestExpectation instances I have created are fulfilled. In this case, there’s only one and we’ll fulfill it in the sink I used to subscribe to $kwhInBattery. In the sink’s receiveValue closure I will ensure that the received value is equal to the fist item in the expectedValues array. If it is, then the publisher emitted the value that I expected. Once I’ve established this, I will update the expectedValues array by removing the first element from the expectedValues array using its dropFirst() method. By doing that the first item is removed from the array, moving each element up one slot. So the next time the receiveValue closure is called, the first element in the expectedValues array should match the value that’s emitted. I will keep updating the expectedValues array and comparing the emitted element to the first item in expectedValues until it’s empty. If the test receives a value once the expectedValues array is empty, the publisher emitted more values than expected. Let’s fill in the sink method and write some actual test code: car.$kwhInBattery.sink(receiveValue: { value in guard Donny Wals let expectedValue = expectedValues.first else { 196 Practical Combine XCTFail("The publisher emitted more values than expected.") return } guard expectedValue == value else { XCTFail("Expected received value \(value) to match first expected ,→ value \(expectedValue)") return } // This creates a new array with all elements from the original ,→ except the first element. expectedValues = Array(expectedValues.dropFirst()) if expectedValues.isEmpty { receivedAllValues.fulfill() } }).store(in: &cancellables) This code follows the steps I described earleir. First, I make sure that I have an expected value to compare the received value with. If I don’t have any expected values left, the sink received more values than expected. Next, I check whether the expected value matches the received value. If it does, I update expectedValues by removing the first item from the array of expected values. If I removed the last value from that array, the receivedAllValues expectation can be fulfilled and the test is considered completed. Because the expectedValues array contains two items, I expect the sink to be called twice before considering the test completed. Apart from some details, this code really isn’t all that different from code you would write to test other asynchronous code. It ultimately comes down to verifying that a certain closure is called with the expected arguments a certain number of times before considering the test to be completed. What’s important to take away here is that I don’t test whether I can map over the published values or transform them otherwise. If you write a map in your test, it’s likely that you are Donny Wals 197 Practical Combine accidentally testing whether Combine’s map works rather than testing that your code does what it should. When you write a test for your Combine code you want to make sure that your publishers emit the values that they should emit. Not whether you can multiply that value by two using a map, and apply a filter to remove any values larger than some arbitrary number in your test. A test like that wouldn’t verify that your code is correct at all. It would just verify that Combine can do its job. Luckily, I have an example of a scenario like that. Remember the CarViewModel from Chapter 4 - Updating the User Interface? It’s okay if you don’t. I included the code for that view model below: struct CarViewModel { var car: Car lazy var batterySubject: AnyPublisher<String?, Never> = { return car.$kwhInBattery.map({ newCharge in return "The car now has \(newCharge)kwh in its battery" }).eraseToAnyPublisher() }() mutating func drive(kilometers: Double) { let kwhNeeded = kilometers * car.kwhPerKilometer assert(kwhNeeded <= car.kwhInBattery, "Can't make trip, not ,→ enough charge in battery") car.kwhInBattery -= kwhNeeded } } Notice how batterySubject creates a publisher that converts the Double values published by $kwhInBattery to String. If we want to test this logic, we should not write a test that applies map to $kwhInBattery. Instead, we should test batterySubject directly and test whether the strings emitted by batterySubject are the strings we need. Here’s what that test might look like: Donny Wals 198 Practical Combine class CarViewModelTest: XCTestCase { var car: Car! var carViewModel: CarViewModel! var cancellables: Set<AnyCancellable>! override func setUp() { car = Car() carViewModel = CarViewModel(car: car) cancellables = [] } func testCarViewModelEmitsCorrectStrings() { let newValue: Double = car.kwhInBattery - car.kwhPerKilometer * ,→ 10 var expectedValues = [car.kwhInBattery, newValue].map { ,→ doubleValue in return "The car now has \(doubleValue)kwh in its battery" } let receivedAllValues = expectation(description: "all values ,→ received") carViewModel.batterySubject.sink(receiveValue: { value in //we'll write the assertion logic here }).store(in: &cancellables) carViewModel.drive(kilometers: 10) waitForExpectations(timeout: 1, handler: nil) } } Notice how similar this test looks to the previous test. A big difference is that I’ve changed my newValue to be car.kwhInBattery car.kwhPerKilometer * 10. The reason for this is that I want to call drive on the Donny Wals 199 Practical Combine carViewModel and verify that after calling drive, my batterySubject emits an updated status string. Similar to the earlier example, I create an array of expected output. This time, I map over the Double values I expect to be generated by the car and transforming them into strings. Note that this doesn’t violate any principles I mentioned before. I trust that Swift’s map works on arrays and I use it to generate my expected test output. That’s different than writing a test that validates that applying map to an array produces the expected output. The most important bit of this test is written inside carViewModel.batterySubject.sink. Everything else is basically the same as the previous test. I update my expected output, check whether the emitted value matches my expected value and I fulfill the expectation if all values are received. Let’s look at the implementation of the assertion logic in carViewModel.batterySubject.sink. If you want, you can try to finish this test yourself before looking at my code. The implementation should look almost identical to the assertion logic from the earlier test. carViewModel.batterySubject.sink(receiveValue: { value in guard let value = value else { XCTFail("Expected value to be non-nil") return } guard let expectedValue = expectedValues.first else { XCTFail("The publisher emitted more values than expected.") return } guard expectedValue == value else { XCTFail("Expected received value \(value) to match first expected ,→ value \(expectedValue)") return } expectedValues = Array(expectedValues.dropFirst()) Donny Wals 200 Practical Combine if expectedValues.isEmpty { receivedAllValues.fulfill() } }).store(in: &cancellables) The only difference with the code from before is that I added an extra guard to make sure the emitted value is not nil. Notice that even though we’re making sure that the CarViewModel maps the Double values emitted by $kwhInBattery to String, we don’t explicitly rely on map. We don’t even check whether map is used. Or whether the String values are derived from the Car’s $kwhInBattery publisher. All we’re interested in is making sure that batterySubject emits the expected strings. To achieve this I used the same techniques that I would use to test any other asynchronous code. Optimizing Combine code for testability Writing a good test suite requires you to write testable code. If your code was not designed with testability in mind you’ll likely end up with a poorly written test suite that produces questionable results. I have already mentioned that you should design your code in a way that allows you to replace, or mock certain objects. This wasn’t needed for the tests that I showed you earlier because the code was fairly trivial and didn’t rely on any external resources. But how can you optimize Combine code that does rely on external resources to be tested properly? The answer isn’t any more exciting than it would be for other code. Optimizing your Combine code to be testable means that you abstract external resources, or dependencies, behind protocols so you can inject special versions of these dependencies in your tests. In this section, I will demonstrate this using two examples. First, I will show you how you can make code that relies on NotificationCenter notifications testable. The second example will be slightly more complicated because it involves writing a protocol to abstract (and test) an object that relies on the network. Donny Wals 201 Practical Combine Architecting code that uses a NotificationCenter publisher In Chapter 2 - Exploring publishers and subscribers I first showed you how you can subscribe to Notification Center events in Combine using a NotificationCenter.Publisher. Listening to Notification Center events can be useful for a plethora of reasons. In your UI, you might want to listen for notifications that tell you whether the keyboard is shown or hidden. In an application that uses Core Data, you might want to listen for notifications that signal that a managed object context was updated. Or maybe your app uses an in-memory cache for data that is expensive to calculate and you’re using NotificationCenter to listen for memory warnings so you can purge your cache when the system tells you it’s low on memory. With Combine, it’s easy to subscribe to events that are published through NotificationCenter because Apple has implemented a convenient publisher for us. But that’s not what I want to explain in this section. Instead, I want you to examine the following code and think about how you would test it: class ImageLoader { var images = [URL: UIImage]() var cancellables = Set<AnyCancellable>() init() { let notificationCenter = NotificationCenter.default let notification = UIApplication.didReceiveMemoryWarningNotification ,→ notificationCenter.publisher(for: notification) .sink(receiveValue: { [weak self] _ in self?.images = [URL: UIImage]() }) .store(in: &cancellables) } } The code itself doesn’t do much other than listening for memory warnings and clearing the images dictionary when the system sends a memory warning. Donny Wals 202 Practical Combine If you’ve given some thought to how you would test this code you may have come up with an idea similar to this: • • • • Set up an instance of ImageLoader Add a couple of images to the images dictionary Send a memory warning notification through NotificationCenter.default Check that the cached image is no longer cached after sending a memory warning If you would write this test there’s a good chance it’ll work just fine. But there’s also a good chance that at some point down the line you’re going to introduce some unexpected behavior. After all, every object in your app has access to NotificationCenter.default. And by that I don’t mean every object in your code. It also means that every object from UIKit, Combine and any third-party dependency you use has access to NotificationCenter.default. So while sending a notification through the default Notification Center might work fine for a while it’s not unlikely that other parts of your code will react to the memory warning that’s sent by your test and it’s possible that this introduces a bug in your test. Or worse, maybe the system actually sends a memory warning in the middle of you setting up your test. What would happen if you receive a memory warning before you’ve set up everything you wanted to test. And what happens when you fire your memory warning after the system has issued its memory warning? You don’t want to worry about these things. Instead of trying to make NotificationCenter.default work, I would refactor ImageLoader as follows: class ImageLoader { var images = [URL: UIImage]() var cancellables = Set<AnyCancellable>() init(_ notificationCenter: NotificationCenter = NotificationCenter.default) { ,→ let notification = ,→ UIApplication.didReceiveMemoryWarningNotification notificationCenter.publisher(for: notification) Donny Wals 203 Practical Combine .sink(receiveValue: { [weak self] _ in self?.images = [URL: UIImage]() }).store(in: &cancellables) } } I haven’t changed much. All I did was allow users of ImageLoader to inject a specific NotificationCenter instance if they desire to do so. I use NotificationCenter.default as the default value for this argument so I can still create instances of ImageLoader without passing a Notification Center explicitly because I really only want to inject a different Notification Center in my tests. By using a special Notification Center in my tests I have fine-grained control over when I send a memory warning, and more importantly, I know that no external code can listen for or trigger memory warnings on that specific Notification Center instance. Try to write a test for this object yourself before peeking at my version: import XCTest import Combine import UIKit @testable import Chapter11 class ImageLoaderTests: XCTestCase { func testImageLoaderClearsImagesOnMemoryWarning() { // setup let notificationCenter = NotificationCenter() let imageLoader = ImageLoader(notificationCenter) // store a dummy image let image = UIImage(systemName: "house") let url = URL(string: "https://fake.url/house")! imageLoader.images[url] = image XCTAssertNotNil(imageLoader.images[url]) // send memory warning Donny Wals 204 Practical Combine let memoryWarning = ,→ UIApplication.didReceiveMemoryWarningNotification notificationCenter.post(Notification(name: memoryWarning)) // verify that the images are now gone XCTAssertNil(imageLoader.images[url]) } } The test is fairly straightforward. I create an ImageLoader and NotificationCenter object, store a UIImage in it using a made-up URL and check whether the image is actually available in the ImageLoader’s images dictionary. It’s okay that the URL is made-up and that the UIImage that I store is just an SF Symbol image. I’m not testing whether I can load images from arbitrary locations. I’m only testing whether ImageLoader clears any images that it stored when a memory warning is received. After performing all the required setup I post a memory warning to my custom Notification Center object and lastly I verify that the images dictionary no longer contains an entry for the URL I created earlier. I hope that at this point you are wondering where the Combine is in this test. We don’t use Combine in this test at all because we don’t have to. We’re using Combine in the implementation of ImageLoader but that doesn’t mean that we need to use Combine in the test that verifies whether ImageLoader works as expected. Keep in mind that we’re not testing Combine. That’s Apple’s job. Our job is to test that our code is written properly and works as expected. Ultimately, the fact that Combine is used to listen for memory warnings is an implementation detail. That doesn’t mean we need to use Combine to verify that the implementation is correct. The next example builds upon this ImageLoader and adds some testable networking capabilities to it. Donny Wals 205 Practical Combine Architecting code that requires a networking layer So far the tests you have written are fairly trivial. In this section, I want to show you a somewhat more complicated setup that, while more complex, is built upon the exact same rules and foundation as the code you wrote in the previous subsection. Since the ImageLoader is fairly useless without the ability to load images from some kind of external source, I would like to add some networking capabilities to it. The simplest way to achieve this would be to add the following method to ImageLoader: func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> { if let image = images[url] { return Just(image).setFailureType(to: Error.self).eraseToAnyPublisher() ,→ } return URLSession.shared.dataTaskPublisher(for: url) .mapError({ $0 as Error }) .tryMap({ response in guard let image = UIImage(data: response.data) else { throw ImageLoaderError.invalidData } return image }) .eraseToAnyPublisher() } The way I wrote this code makes it virtually impossible to reliably test it. This code is tightly coupled to URLSession.shared. This means that for loadImage(at:) to load an image we need a network connection. And the server that we would load images from in the test has to be up and running. And not just that, the server must have an image at the location we want to test. If any of these three preconditions are missing, any test that we write for loadImage(at:) would fail, even if our logic in loadImage(at:) is perfectly fine. It would be a shame if a unit test fails for reasons that are outside of our control. Unit tests are Donny Wals 206 Practical Combine supposed to tell you when your code is faulty, not that a server somewhere is down. Before you can write a test for loadImage(at:) we’ll need to do some refactoring. Usually in an application you’ll want to abstract all network access behind a networking layer. This means that the ImageLoader should not use URLSession.shared.dataTaskPublisher(for directly. Instead, it should access a method on an object that implements networking operations that are useful in your app. To make it possible to create a fake networking object for testing purposes, it’s common to write a protocol that your networking implementation conforms to. This will allow you to use the protocol in your code rather than a concrete implementation of a networking object. For the ImageLoader, I came up with the following networking protocol: protocol ImageNetworking { func loadURL(_ url: URL) -> AnyPublisher<Data, Error> } This protocol is very basic but it’s also rather important. Notice that the loadURL(_:) function in this protocol returns AnyPublisher<Data, Error>. A URLSession.DataTaskPublisher has (data: Data, response: URLResponse) as it’s Output and URLError as its Failure. By not copying this Output and Failure in my protocol I have completely decoupled my networking protocol from URLSession. Now that we have this networking protocol, let’s define an object that ImageLoader can use to load image when we’re not using it in a test suite: class ImageNetworkProvider: ImageNetworking { func loadURL(_ url: URL) -> AnyPublisher<Data, Error> { return URLSession.shared.dataTaskPublisher(for: url) .mapError({$0 as Error}) .map(\.data) .eraseToAnyPublisher() } } It’s now possible to refactor ImageLoader to require an ImageNetworking object in its initializer, similar to how it requires a NotificationCenter. The only difference is that Donny Wals 207 Practical Combine we also need an instance variable on ImageLoader to hold on to the ImageNetworking object: class ImageLoader { var images = [URL: UIImage]() var cancellables = Set<AnyCancellable>() let network: ImageNetworking init(_ notificationCenter: NotificationCenter = NotificationCenter.default, ,→ network: ImageNetworking = ImageNetworkProvider()) { self.network = network // Notification center related code } func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> { // This will be updated soon } } Okay, at this point the ImageLoader depends on an object that implements the ImageNetworking protocol. By default we’ll use an instance of ImageNetworkProvider but we can use a different object when testing the ImageLoader. Let’s update loadImage(at:) so it uses this new ImageNetworking object: func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> { if let image = images[url] { return Just(image).setFailureType(to: ,→ Error.self).eraseToAnyPublisher() } return network.loadURL(url) Donny Wals 208 Practical Combine .tryMap({ data in guard let image = UIImage(data: data) else { throw ImageLoaderError.invalidData } return image }) .eraseToAnyPublisher() } The code looks fairly similar to what we had before but it’s far more testable. Now that ImageLoader is no longer tied to URLSession, we can use any object we’d like in our test suite to act as the networking layer as long as it implements the ImageNetworking protocol. There are three things I would like to test regarding the ImageLoader and networking. First, I want to make sure that the ImageLoader uses the networking object that we pass it to make network requests for new images. Second, I want to test that the ImageLoader caches loaded images in its images dictionary. And third, I want to test that the ImageLoader uses its cached images instead of the network if a cached image is available. To do this, I will write two tests. One will verify that the networking object is used and images are written to the cache. The second test will verify that cached images are used when available. Before I show you any tests, I want to show you the mock ImageNetworking object that’s used in the test suite. If you write mock objects in your own test suite, make sure to add them to your test target. They have no business in your application target. class MockImageNetworkProvider: ImageNetworking { var wasLoadURLCalled = false func loadURL(_ url: URL) -> AnyPublisher<Data, Error> { wasLoadURLCalled = true let data = UIImage(systemName: "house")!.pngData()! Donny Wals 209 Practical Combine return Just(data) .setFailureType(to: Error.self) .eraseToAnyPublisher() } } This mock network object is very simple. It keeps track of whether loadURL(_:) has been called, and when it’s called I create a simple publisher that returns the data for a UIImage instance. It doesn’t matter that this example uses an SF Symbol image instead of a more useful image. After all, we just want to test whether ImageLoader works as expected. Not that our mock image loader provides the correct images to ImageLoader. With this helper in place, add the following property and setUp method to ImageLoaderTests: var cancellables = Set<AnyCancellable>() override func setUp() { cancellables = Set<AnyCancellable>() } I will use some Combine in the tests I’m about to show you so each test will need a fresh set of AnyCancellable objects. The first test I want to show you is the test that verifies that ImageLoader uses the network object we give it and that it caches images that it loads. If you’re feeling adventurous, try implementing this test on your own. You have seen all the bits and pieces needed to complete this task. If you’re not sure how you would tackle this kind of test, no worries. Here’s what I came up with: func testImageLoaderLoadsImageFromNetwork() { // setup let mockNetwork = MockImageNetworkProvider() let notificationCenter = NotificationCenter() let imageLoader = ImageLoader(notificationCenter, network: ,→ mockNetwork) Donny Wals 210 Practical Combine let url = URL(string: "https://fake.url/house")! // expectations let loadCompleted = expectation(description: "expected image load to complete") ,→ let imageReceived = expectation(description: "expected to receive an image") ,→ // load the image imageLoader.loadImage(at: url).sink(receiveCompletion: { completion ,→ in guard case .finished = completion else { XCTFail("Expected load to complete succesfully") return } // verify our assumptions XCTAssertEqual(mockNetwork.wasLoadURLCalled, true) XCTAssertNotNil(imageLoader.images[url]) loadCompleted.fulfill() }, receiveValue: { image in // acknowledge that we received an image imageReceived.fulfill() }).store(in: &cancellables) // wait for expectations waitForExpectations(timeout: 1.0, handler: nil) } This test follows a similar pattern to what you’ve seen before. It’s just a bit bigger. First, I create all objects needed to perform my test. A special networking object, an isolated Notification Center and the ImageLoader itself. I also set up some test expectations. One to validate that we eventually recieve an image, and one to validate that the AnyPublisher returned by ImageLoader eventually completes successfully. Donny Wals 211 Practical Combine Next, the actual test is performed. I call loadImage(at:) on the ImageLoader. At that point I expect the ImageLoader to do its work. When I receive an image, I just fulfill the imageReceived expectation. I don’t care about the exact UIImage I received. All I care about is that I received something. In receiveCompletion, I check that the task completed with success. I also check that the ImageLoader used my mock network object and that the ImageLoader has stored an image in its images dictionary using the URL I requested as a key. Lastly, I fulfill the loadCompleted expectation. If you’re following along, go ahead and run this test. What’s that? The test failed? Good! We used unit testing to find a bug! The ImageLoader does not handle caching of images yet. Its loadImage(at:) method needs some tweaking to work properly. Currently, loadImage(at:) uses the ImageNetworking object to load an image and then applies a tryMap to convert the fetched data to an image if possible. You might be tempted to add a simple line of code to this tryMap: self.images[url] = image But doing so would violate the rules of tryMap. We’re supposed to take the input passed to tryMap, attempt to transform it and return the transformed value or throw an error. We shouldn’t apply any side-effects in a tryMap. Luckily, Apple realized that sometimes you need to apply side-effects. And to help us keep our code proper, Combine has a special operator that’s perfect for applying side-effects. This operator is called handleEvents. When you apply the handleEvents operator to a publisher, you can hook into virtually every stage of a publisher’s lifecycle. You can react to the publisher receiving a subscriber, you can respond to errors or handle any values that are emitted by the publisher. The handleEvent operator has many optional arguments like receiveSubscription, receiveRequest, receiveOutput and more. To see them all, type handleEvents after a publisher in Xcode and let it autocomplete the operator for you. Donny Wals 212 Practical Combine In this case, we want to respond to the values that are emitted by tryMap so we can cache the UIImage instances that are created by tryMap. To do this, we use handleEvents and pass it a receiveOutput closure: .handleEvents(receiveOutput: { [weak self] image in self?.images[url] = image }) Here’s the handleEvents operator in the full context of loadImage(at:): func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> { if let image = images[url] { return Just(image).setFailureType(to: Error.self).eraseToAnyPublisher() ,→ } return network.loadURL(url) .tryMap({ data in guard let image = UIImage(data: data) else { throw ImageLoaderError.invalidData } return image }) .handleEvents(receiveOutput: { [weak self] image in self?.images[url] = image }) .eraseToAnyPublisher() } The handleEvents operator is perfect for cases like this where you want to sit in between the publisher you created and its subscriber. By using handleEvents, the ImageLoader can now handle every UIImage that’s emitted alongside the code that subscribes to the publisher that subscribed to the AnyPublisher returned by loadImage(at:). Note that handleEvents does not count as a subscriber. This means that a publisher will not start Donny Wals 213 Practical Combine performing work when you call handleEvents on it. You still need to explicitly subscribe to the publisher to set up a proper subscription. After updating loadImage(at:), the test we wrote earlier should pass. There’s one more test I want to write. This test will make sure that ImageLoader uses its cached image when possible: func testImageLoaderUsesCachedImageIfAvailable() { // setup let mockNetwork = MockImageNetworkProvider() let notificationCenter = NotificationCenter() let imageLoader = ImageLoader(notificationCenter, network: mockNetwork) ,→ let url = URL(string: "https://fake.url/house")! // populate the cache imageLoader.images[url] = UIImage(systemName: "house") // expectations let loadCompleted = expectation(description: "expected image load to complete") ,→ let imageReceived = expectation(description: "expected to receive an image") ,→ // load the image imageLoader.loadImage(at: url).sink(receiveCompletion: { completion ,→ in guard case .finished = completion else { XCTFail("Expected load to complete succesfully") return } // verify our assumptions XCTAssertEqual(mockNetwork.wasLoadURLCalled, false) XCTAssertNotNil(imageLoader.images[url]) Donny Wals 214 Practical Combine loadCompleted.fulfill() }, receiveValue: { image in // acknowledge that we received an image imageReceived.fulfill() }).store(in: &cancellables) // wait for expectations waitForExpectations(timeout: 1.0, handler: nil) } This test is almost identical to the previous test. The only difference is that I populate the image cache with an image using the URL I’m testing with as a key. Then in the receiveCompletion closure I make sure that mockNetwork.wasLoadURLCalled is false. In other words, I make sure that the ImageLoader did not attempt to load the image from the network because it had the image in its cache. With the tests I showed you in this section, you should now have an idea of how you can abstract code in a way that allows you to substitute external dependencies like NotificationCenter or the network using objects that you can control in your test. Sometimes this means you simply create an isolated instance of an object. Other times it’s a little bit more involved and you can only obtain the required level of control by hiding the external dependency behind a protocol. The ultimate goal here is to create a controlled environment that you can set up and manipulate in your test suite so you can be sure that you’re only testing the code and logic that you want to test. And that any test successes or failures are the result of your code, and not the result of events that occurred outside of your control. In the next section, I will show you some helpers that I think can be useful when you’re testing Combine code by making your life somewhat easier. Donny Wals 215 Practical Combine Creating helpers to make testing Combine code easier By now you should have an idea of you can test Combine code. If you’re new to testing I understand that you probably have many questions right now. If that’s the case, I would like to point you to the testing section on my blog. There you can brush up on your testing basics in a Combine-less context. After that, you can come back to this chapter and give it another read. I’m sure it will make more sense by then. Testing is one of those things that you could write stacks of books about so don’t feel discouraged if you don’t feel like a testing expert just yet. It takes time and practice to get better at testing. In this section, I would like to revisit some of the tests I have shown you in the previous section, and show you two neat helper methods that you could implement to improve the test suite. First up is the CarViewModelTest: func testCarViewModelEmitsCorrectStrings() { let newValue: Double = car.kwhInBattery - car.kwhPerKilometer * 10 var expectedValues = [car.kwhInBattery, newValue].map { doubleValue in ,→ return "The car now has \(doubleValue)kwh in its battery" } let receivedAllValues = expectation(description: "all values received") ,→ carViewModel.batterySubject.sink(receiveValue: { value in guard let value = value else { XCTFail("Expected value to be non-nil") return } guard let expectedValue = expectedValues.first else { XCTFail("The publisher emitted more values than expected.") return } Donny Wals 216 Practical Combine guard expectedValue == value else { XCTFail("Expected received value \(value) to match first ,→ expected value \(expectedValue)") return } expectedValues = Array(expectedValues.dropFirst()) if expectedValues.isEmpty { receivedAllValues.fulfill() } }).store(in: &cancellables) carViewModel.drive(kilometers: 10) waitForExpectations(timeout: 1, handler: nil) } There is a whole bunch of boilerplate in this test. You can tell because the test you wrote in CarTest closely resembles the CarViewModel test. It would be really nice to capture this boilerplate somehow so the tests themselves can be much cleaner. To do this, you need to think about what it is that these tests do and have in common. Ultimately both tests have a set of expected values that should be emitted by a publisher. Next, I subscribe to the publisher that should emit these values, compare the emitted value with an expected value and ultimately the test is considered completed if all expected values are received. When I refactor code to be less repetitive I usually try to imagine how I would like to use my code after it has been refactored. For this example I came up with the following design for CarViewModelTest: func testCarViewModelEmitsCorrectStrings() { let newValue: Double = car.kwhInBattery - car.kwhPerKilometer * 10 var expectedValues = [car.kwhInBattery, newValue].map { doubleValue ,→ in Donny Wals 217 Practical Combine return "The car now has \(doubleValue)kwh in its battery" } let receivedAllValues = expectation(description: "all values ,→ received") carViewModel.batterySubject .assertOutput(matches: expectedValues, expectation: receivedAllValues) ,→ .store(in: &cancellables) carViewModel.drive(kilometers: 10) waitForExpectations(timeout: 1, handler: nil) } This code is much shorter than before, and it also communicates our intent much clearer. We want to assert that the values published by carViewModel.batterySubject match the values in expectedValues. This code is concise, expressive and if you ask me, it looks kind of beautiful. The implementation of the assertOutput operator looks as follows: extension Publisher where Output: Equatable { func assertOutput(matches: [Output], expectation: ,→ XCTestExpectation) -> AnyCancellable { var expectedValues = matches return self.sink(receiveCompletion: { _ in // we don't handle completion }, receiveValue: { value in guard let expectedValue = expectedValues.first else { XCTFail("The publisher emitted more values than expected.") return } guard expectedValue == value else { Donny Wals 218 Practical Combine XCTFail("Expected received value \(value) to match first ,→ expected value \(expectedValue)") return } expectedValues = Array(expectedValues.dropFirst()) if expectedValues.isEmpty { expectation.fulfill() } }) } } I added assertOutput as an extension on Publisher so it can be used as an operator. It also returns an AnyCancellable so the caller of assertOutput can keep the subscription alive for as long as needed. The implementation of assertOutput is fairly straightforward. I create a mutable copy of the matches array by assigning it to a variable and I subscribe to the publisher assertOutput is called on. This is self. I then compare all emitted values using the same logic I used before and eventually I fulfill the expectation, just like the code did when it was still part of CarViewModelTest. But by making this logic available as an operator on Publisher it’s now possible to quickly apply this logic where needed. Try refactoring CarTest to use this operator. You should be able to do this by looking at the updated test I just showed you. You can use assertOutput to compare any publisher’s output with an array of expected values as long as the publisher’s output is Equatable. Note that this helper does not concern itself with a completion event. The reason is that this helper can be used on publishers that typically don’t ever complete like @Published or the NotificationCenter.Publisher. The second test I would like to refactor with you is the ImageLoader test. In this test the receiveCompletion closure was used to perform a couple of assertions. In other words, we weren’t interested in validating the publisher’s output as much as we were interested in validating that the publisher did the right thing. Let’s look at one of the tests I showed you in Donny Wals 219 Practical Combine the previous section once more: func testImageLoaderLoadsImageFromNetwork() { // setup let mockNetwork = MockImageNetworkProvider() let notificationCenter = NotificationCenter() let imageLoader = ImageLoader(notificationCenter, network: mockNetwork) ,→ let url = URL(string: "https://fake.url/house")! // expectations let loadCompleted = expectation(description: "expected image load to complete") ,→ let imageReceived = expectation(description: "expected to receive an image") ,→ // load the image imageLoader.loadImage(at: url).sink(receiveCompletion: { completion ,→ in guard case .finished = completion else { XCTFail("Expected load to complete succesfully") return } // verify our assumptions XCTAssertEqual(mockNetwork.wasLoadURLCalled, true) XCTAssertNotNil(imageLoader.images[url]) loadCompleted.fulfill() }, receiveValue: { image in // acknowledge that we received an image imageReceived.fulfill() }).store(in: &cancellables) // wait for expectations waitForExpectations(timeout: 1.0, handler: nil) Donny Wals 220 Practical Combine } If you recall I showed you two virtually identical tests in the previous section. One to verify that the ImageLoader attempted to load an image from the network and cached the result and one to verify that the ImageLoader would use a cached image instead of attempting to load one from the network if a cached image is available. This code could be improved if we could somehow tell the loader to load an image, get a Result<Success, Failure> object back, and then perform any assertions that we need to do. It’s probably easier to understand what I mean if I show you: func testImageLoaderLoadsImageFromNetwork() { // setup let mockNetwork = MockImageNetworkProvider() let notificationCenter = NotificationCenter() let imageLoader = ImageLoader(notificationCenter, network: ,→ mockNetwork) let url = URL(string: "https://fake.url/house")! let result = awaitCompletion(for: imageLoader.loadImage(at: url)) XCTAssertNoThrow(try result.get()) XCTAssertEqual(mockNetwork.wasLoadURLCalled, true) XCTAssertNotNil(imageLoader.images[url]) } That looks clean, doesn’t it? We can assert that the image load was successful using XCTAssertNoThrow(try result.get()) and check whether we accessed the network and if the image is now cached without subscribing to anything or even using any XCTestExpectation. So how does this magic work? Before I show you I want to give a quick shout out to John Sundell and Cassius Pacheco for giving me some pointers and sharing their ideas that led me to create the following helper: Donny Wals 221 Practical Combine extension XCTestCase { func awaitCompletion<P: Publisher>(for publisher: P) -> Result<[P.Output], P.Failure> { ,→ let finishedExpectation = expectation(description: "completion expectation") ,→ var output = [P.Output]() var result: Result<[P.Output], P.Failure>! _ = publisher.sink(receiveCompletion: { completion in if case .failure(let error) = completion { result = .failure(error) } else { result = .success(output) } finishedExpectation.fulfill() }, receiveValue: { value in output.append(value) }) waitForExpectations(timeout: 1.0, handler: nil) return result } } This helper is defined on XCTestCase and it uses the XCTestExpectation feature to synchronously return a Result object. The awaitCompletion(for:) method is generic over P which should be a Publisher. It returns a Result<[P.Output], P.Failure>. In other words, it returns a result object that either holds an array of emitted values or an error. To obtain the result object I subscribe to the passed publisher and collect its output in the output array. When the publisher’s receiveCompletion is called I create the Result object and call fullfil on the finishedExpectation. At that point, the code will continue running past the line where I call waitForExpectations and the Result object Donny Wals 222 Practical Combine is returned. Remember that waitForExpectations halts the execution of your code. This means that you can use it to wait for a publisher to complete before returning from awaitCompletion. Pretty nifty I’d say. Note that this helper is only useful for publishers that you expect to complete. If you don’t expect your publisher to complete you’re better off using the assertOutput operator that I showed you earlier in this section. Try refactoring the other ImageLoader test that you wrote in the previous section using waitForExpectations on your own if you’re coding along. I’m sure you can do it. Summary In this chapter, I have shown you everything you need to know to begin testing your Combine code. You know that you should not test whether Combine’s map does what it should but instead that you should test whether a publisher emits the number of elements you expect it to emit given a certain input. You learned that this fits really well in Combine’s Functional Programming roots where you expect every chain of function calls to produce a certain output for a given input. You also learned how you can use XCTest’s expectation API to write asynchronous tests that help you validate whether a publisher has emitted all expected elements and/or errors. I also showed you a convenient helper that’s inspired by code from John Sundell and Cassius Pacheco that you can use to wait for a publisher to complete before asserting that all produced elements are correct. Donny Wals 223 Practical Combine Driving publishers and flows with subjects Up until this point, I have shown you relatively simple publishers and examples. They sometimes used complicated operators, or chained operators together in surprising ways but overall they were relatively straightforward to build and understand. That doesn’t mean they weren’t complex when you first saw them, but as you write more and more Combine code, you’ll realize that the principles you’ve seen up to this point aren’t that scary. The problems we’ve solved so far weren’t very new or necessarily complex either. That’ll change in this chapter. In this chapter, I want to take a closer look at CurrentValueSubject and PassthroughSubject and show you how you can use these subjects together with a strategically placed flatMap to build really cool and complicated interactions and flows in your applications. By the end of this chapter you will have an idea of how you can work with paginated network calls, fetch partial data, and retrieve data recursively. I will also present an alternative implementation for the token refresh flow that I showed you in Chapter 6 - Using Combine for networking. Retrieving partial data in Combine When working with large amounts of data it might be beneficial to load parts of your data instead of loading it all at once. This is common in social media applications that load more and more data as you scroll down an endless feed of pictures, videos, and text. Batched loading is also used when processing data or reading a large number of files. It wouldn’t make sense to load a heap of files into memory if you can only process one file at a time. Regardless of the exact data your loading, the principle of retrieving partial data is almost always the same. You request a section of data, process it, and at a later time you request the next section of data. The trigger for this request could be user interaction, or anything else. An object that’s used to retrieve such partial data that doesn’t use Combine might look a bit like this: Donny Wals 224 Practical Combine class DataFetcher { private var currentPage = 0 func loadNextPage(_ completion: @escaping (Result<Data, Error>) -> ,→ Void) { // perform load // increment currentPage // call completion handler } } This example is super simple and basic and you might have implemented the same feature in a completely different way. That’s okay. The point isn’t to make this the perfect fetcher for you. It’s to show you how you can convert a loader like this to work with Combine. To do that, we need to think of a nice API. We could make loadNextPage return an AnyPublisher<Data, Error>, and that would work. However, that means that we need to subscribe to the publisher that’s created by loadNextPage every time we want to load new data. But what if we’d want to trigger loadNextPage in a place where it’s not convenient for us to handle its result. In an infinitely scrolling list for example. Instead, what we can do is use a PassthroughSubject<Data, Error> to publish newly fetched data, and we could make loadNextPage return nothing. If this sounds familiar, you’re probably thinking of the DataProvider that I showed you in Chapter 4 - Updating the User Interface. In this chapter, I’m using a slightly different, more generic object called DataFetcher but it’s built on the same building blocks as the data fetcher you’ve seen before. class DataFetcher { private var currentPage = 0 var dataPublisher = PassthroughSubject<Data, URLError>() var cancellables = Set<AnyCancellable>() Donny Wals 225 Practical Combine func loadNextPage() { let url = URL(string: ,→ "https://practicalcombine.com?page=\(currentPage)")! URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] loadedData in self?.dataPublisher.send(loadedData) self?.currentPage += 1 }) .store(in: &cancellables) } } Looks good? Not really. As you might recall from Chapter 4 - Updating the User Interface, I don’t like subscribing to a publisher in loadNextPage(). You already saw one way to get rid of the sink in loadNextPage() using assign(to:) and a @Published property. But what if I don’t want to use @Published and keep my PassthroughSubject? In Chapter 3 - Transforming publishers you learned about flatMap. You saw that flatMap takes values that are emitted by a publisher and converts these emitted values into new publishers, where all values emitted by the publishers created in the flatMap are emitted to a single subscriber. To achieve my goal of not having to subscribe to anything in loadNextPage, I need a flatMap. Why? You ask. I’m glad you asked. In your application, hardly anything happens without an external force doing something. Usually, this external force is the user. The user taps buttons, scrolls in lists, locks their device, visits locations in the real world that might trigger updates in your app, and performs many other actions with your app and their device. If you think of these actions as streams of values, where a value could be the act of scrolling Donny Wals 226 Practical Combine or needing a different page, you’re essentially thinking of a publisher that emits actions as values. When you have a publisher that emits values, you can flatMap over these values to create new publishers that do something. So in the case of our infinitely scrolling list, requesting a page could be an event that’s emitted by a publisher. And the publisher that loads the next page could be the result of applying flatMap to the page requesting publisher. When we slap a map on the publisher created in the flatMap (which emits URLSession.DataTaskPublisher.Output) we can extract data. This results in a publisher that emits <Data, URLError> which happens to line up with the Output and Failure of my dataPublisher property. Let’s refactor DataFetcher so it implements the flow I described above: class DataFetcher { private var currentPage = 0 lazy var dataPublisher: AnyPublisher<Data, URLError> = { loadRequestPublisher .flatMap({ [weak self] _ -> URLSession.DataTaskPublisher in let url = URL(string: ,→ "https://practicalcombine.com?page=\(self?.currentPage ?? ,→ 0)")! return URLSession.shared.dataTaskPublisher(for: url) }) .map(\.data) .eraseToAnyPublisher() }() private var loadRequestPublisher = PassthroughSubject<Void, ,→ Never>() func loadNextPage() { loadRequestPublisher.send(()) } } Donny Wals 227 Practical Combine In the code above you can see how I defined a PassthroughSubject that has Void as its Output. This subject’s only purpose is to emit values that can be flatMapped over. Every time loadNextPage is called a new Void value is emitted by loadRequestPublisher. I have updated dataPublisher to be a lazy property that’s initialized with a closure. This publisher is now the result of taking loadRequestsPublisher, applying flatMap on it to create a new data fetch task, and mapping over the output of the flatMap to extract the data property from the URLSession.DataTaskPublisher.Output. The resulting publisher is erased to AnyPublisher. The result of the code above is that once dataPublisher has a subscriber, every value emitted by loadRequestPublisher triggers a new data fetch. The best part is that there are no subscriptions being set up within DataFetcher and its loadNextPage() method. There is just one problem with this code though. The currentPage property is never updated. I could refactor the map that currently only extracts data, make it a closure where I update self.currentPage and return the data from the URLSession.DataTaskPublisher.Output that’s passed to the map but that violates the idea that map shouldn’t operate on any external resources. It should be pure. Instead, we can use the handleEvents operator. This operator allows us to hook into the lifecycle of a subscription, without subscribing to it. It’s ideal for applying side-effects. If you need a refresher on handleEvent please refer back to Chapter 11 - Testing code that uses Combine. The following code shows an updated version of the loadRequestPublisher that uses handleEvents to update the currentPage property: lazy var dataPublisher: AnyPublisher<Data, URLError> = { loadRequestPublisher .flatMap({ [weak self] _ -> URLSession.DataTaskPublisher in let url = URL(string: ,→ "https://practicalcombine.com?page=\(self?.currentPage ?? ,→ 0)")! return URLSession.shared.dataTaskPublisher(for: url) }) .map(\.data) .handleEvents(receiveOutput: { [weak self] _ in self?.currentPage += 1 Donny Wals 228 Practical Combine }) .eraseToAnyPublisher() }() A setup like the one I just showed you is pretty advanced, and it might take a little while before the concept of using subjects and flatMap like this sinks in (pun intended) and you feel confident enough to design your own flows that use a similar pattern. That said, let’s push this train forward and see how we can refactor this paginated DataFetcher and make it load all pages recursively until there are no more pages without having to call loadNextPage every time. Recursively loading data with subjects and flatMap We can take the ideas from the previous section and apply them in a different way to build a function that will recursively load pages from a remote endpoint until there are no more pages to load. Paginated responses, or paginated APIs, come in many shapes. In this section I’m going to assume that a server responds with a very simple object that has the following shape: struct ApiResponse { let hasMorePages: Bool let items: [String] } A real response would of course be far more complex than this, but this should be enough to build our recursive data loader. I’m also going to assume that we’re using a networking object that takes care of loading and returning pages. I’m not going to make real network calls, so my mock networking object uses a Just publisher to return pages when needed. Here’s what the mock network object looks like: Donny Wals 229 Practical Combine class NetworkObject { func loadPage(_ page: Int) -> Just<ApiResponse> { if page < 5 { return Just(ApiResponse(hasMorePages: true, items: ["Item", "Item"])) } else { return Just(ApiResponse(hasMorePages: false, items: ["Item", "Item"])) } } } This object returns an ApiResponse with hasMorePages set to true if we request a page lower than page five. If we request page 5 or up, we get a response that has its hasMorePages property set to false. The object that fetches data is called RecursiveFetcher and I’m going to work off the following skeleton implementation: struct RecursiveFetcher { let network: NetworkObject func loadAllPages() -> AnyPublisher<[String], Never> { } } All of my work will happen in loadAllPages(). When this method is called, I will kick off a page load. Then when I get a response from this page load, I will inspect the response to see if there are any more pages and then load another page. This is very similar to what you saw in the previous section, except instead of having the user ask for more pages the code should automatically “ask” for the next page when the previous page is loaded and there are more pages to load. The code should also collect all responses from all page loads, so we can emit a single array Donny Wals 230 Practical Combine of items (in this case [String]) to subscribers. To do this, we’ll use Combine’s reduce operator. When you apply reduce to a publisher in Combine, all events emitted by that publisher are collected and reduced into a single value. Similar to how reduce works on Array in Swift. When the upstream publisher completes, reduce will emit the single value that was reduced from all values emitted by the upstream publisher. Let’s implement loadAllPages to see how we can automatically trigger new page loads, and learn more about reduce. Examine the code and try to figure out what it does before skipping to the explanation: func loadAllPages() -> AnyPublisher<[String], Never> { let pageIndexPublisher = CurrentValueSubject<Int, Never>(0) return pageIndexPublisher .flatMap({ pageIndex in return network.loadPage(pageIndex) }) .handleEvents(receiveOutput: { response in if response.hasMorePages { pageIndexPublisher.value += 1 } else { pageIndexPublisher.send(completion: .finished) } }) .reduce([String](), { collectedStrings, response in return response.items + collectedStrings }) .eraseToAnyPublisher() } Were you able to figure out what this code does exactly? It can be really confusing at first but I’m sure you got pretty far. In the previous section, I used a PassthroughSubject to publish load requests that were represented by a Void value. This time I’m using a CurrentValueSubject that has the Donny Wals 231 Practical Combine current page as its current value. This is really neat because a CurrentValueSubject emits its current value when it receives a subscriber. In other words, when we subscribe to the publisher returned by loadAllPages, the pageIndexPublisher will receive a subscriber and emit its initial value. This value is then flatMapped over to create a new publisher. This new publisher is created by calling network.loadPage(_:) and passing the emitted pageIndex to it. At this point, we have a publisher that emits ApiResponse objects since that’s what the publisher created in network.loadPage(_:) emits. In the next operator, I use handleEvents to inspect the emitted response to see if the response’s hasMorePages property is true. If it is, I increment pageIndexPublisher.value by one. This will make it emit a new value, which will be sent through my flatMap, and kick off a new page load. If there are no more pages to load I complete the pageIndexPublisher by calling send(completion: .finished) on it. After handleEvents, we enter the reduce operator. This operator will capture all emitted ApiResponse values and combine them into a single array of String objects. The reduce operator takes an initial value (an empty array of String objects) and a closure that is called every time a value is emitted by the upstream publisher. In this closure, I return the combined value of the existing array of strings and the newly fetched strings. When the pageIndexPublisher completes, my String array is emitted by reduce and the subscriber of loadAllPages() will receive a single array with all strings that were loaded. Let’s see how this RecursiveFetcher is used: let fetcher = RecursiveFetcher(network: NetworkObject()) fetcher.loadAllPages() .sink(receiveValue: { strings in print(strings) }) .store(in: &cancellables) The output of this code is: Donny Wals 232 Practical Combine ["Item", "Item", "Item", "Item", "Item", "Item", "Item", "Item", ,→ "Item", "Item", "Item", "Item"] Pretty cool, right? We were able to drive a recursive network call using a CurrentValueSubject combined with a flatMap, handleEvents and a reduce due to the fact that a CurrentValueSubject emits a value when it receives a subscriber, which is sent to flatMap and kicking off a network call. The handleEvents operator then checks whether the CurrentValueSubject should complete or emit another value, and finally the reduce collects and combines all emitted values into a single value that’s emitted when the CurrentValueSubject completes. This is pretty advanced stuff, but I love how we can build complicated features using small, simple building blocks as long as we find and follow the appropriate patterns. Let’s look at one more cool example of using a Subject together with flatMap to drive a complex flow. Automatically retrying a network call after a token refresh In Chapter 6 - Using Combine for networking, I showed you a simple version of a token refresh flow that would retry a network request after attempting a token refresh. In this section, I will show a more sophisticated approach that, in my opinion, looks nicer than the simpler version. Neither is inherently better in terms of functionality, I just think the approach in this chapter is a bit cleaner. Since you already know how to do networking in Combine, I will jump right in and show you the starting point of this section’s feature: Donny Wals 233 Practical Combine struct Token { let isValid: Bool } class Authenticator { private var currentToken = Token(isValid: false) func refreshToken<S: Subject>(using subject: S) where S.Output == ,→ Token { self.currentToken = Token(isValid: true) subject.send(currentToken) } func tokenSubject() -> CurrentValueSubject<Token, Never> { return CurrentValueSubject(currentToken) } } struct UserApi { let authenticator: Authenticator func getProfile() -> AnyPublisher<Data, URLError> { } } The Token object is a simple object that I defined to easily fake an expired token. The Authenticator object is responsible for providing tokens and refreshing them. The UserApi is the object that will make authenticated network requests. The goal here is to obtain an inital token and make a network request. If the request comes back with a 403 status code this means that the token we received initially should be refreshed and another attempt at performing the network request should be made. While the use case is vastly different from the previous section, the pattern is remarkably similar. Donny Wals 234 Practical Combine In both cases we want to call a method that returns a publisher. When we subscribe to that publisher, a network call should be kicked off. Depending on the response we should kick off another network call or emit a response. The only difference is that we don’t want to reduce anything this time. We just want to emit a single value, as long as the network request we kicked off succeeded. If the request failed, the faulty response should be hidden from subscribers so the token can be refresh and a new request is made. Since the full pipeline for this feature is rather long and complex, I will go through it bit by bit until the pipeline is finished. First, we’ll need a subject that drives the pipeline by emitting an initial token. The CurrentValueSubject created by Authenticator.tokenSubject is a perfect fit for this: func getProfile() -> AnyPublisher<Data, URLError> { let tokenSubject = authenticator.tokenSubject() return tokenSubject .eraseToAnyPublisher() } This code doesn’t quite compile but we’ll get there eventually. Just like before, we’ll want to flatMap over the CurrentValueSubject to kick off a network call whenever we receive a token: func getProfile() -> AnyPublisher<Data, URLError> { let tokenSubject = authenticator.tokenSubject() return tokenSubject .flatMap({ token -> AnyPublisher<Data, URLError> in let url: URL = URL(string: "https://www.donnywals.com")! return URLSession.shared.dataTaskPublisher(for: url) .eraseToAnyPublisher() }) Donny Wals 235 Practical Combine .eraseToAnyPublisher() } Here’s where it gets interesting. If the result of the data task that’s created in this flatMap has a 403 status code, we do not want to forward this value to subscribers. Instead, we want to pretend we never received this value and kick off a token refresh and subsequently retry the network request. If the status code is anything other than 403, the data should be extracted from the URLSession.DataTaskPublisher.Output and forwarded down the pipeline so it’s received by subscribers. We can achieve this by using a flatMap and two special publishers. The first is Just. You already know this one. It emits a single value and completes. The other is Empty. This publisher emits no values and completes immediately. To use these publishers, I need to flatMap over the data task publisher. This allows me to inspect the data task publisher’s result and return a Just or Empty publisher depending on the status code. While we’re at it, we can also kick off a token refresh if the status code is 403: func getProfile() -> AnyPublisher<Data, URLError> { let tokenSubject = authenticator.tokenSubject() return tokenSubject .flatMap({ token -> AnyPublisher<Data, URLError> in let url: URL = URL(string: "https://www.donnywals.com")! return URLSession.shared.dataTaskPublisher(for: url) .flatMap({ result -> AnyPublisher<Data, URLError> in if let httpResponse = result.response as? HTTPURLResponse, httpResponse.statusCode == 403 { self.authenticator.refreshToken(using: tokenSubject) return Empty().eraseToAnyPublisher() } Donny Wals 236 Practical Combine return Just(result.data) .setFailureType(to: URLError.self) .eraseToAnyPublisher() }) .eraseToAnyPublisher() }) .eraseToAnyPublisher() } The new flatMap on its own is nothing special. We take the output from the data task and replace that output with an AnyPublisher<Data, Never>. If the status code is 403, I call self.authenticator.refreshToken(using: tokenSubject) to kick off a token refresh and pass the tokenSubject to the authenticator so it can send a new token over the tokenSubject which will kick off the initial network request again. I also return an Empty publisher that must be erased to AnyPublisher. This publisher completes immediately without emitting values. In other words, it pretends we never received any values from the data task publisher and completes. If the status code is good, we return a Just publisher that emits the extracted Data and completes. We need to set its failure type to URLError to make it compatible with the rest of the pipeline. At this point, you have already implemented a token refresh flow but there’s one thing that bothers me. The initial and subsequent requests are driven by a CurrentValueSubject that’s obtained from Authenticator which is great. Every time tokenSubject emits a new token we perform the network request so recursion is handled automatically, just like it was in the previous section. However, the tokenSubject never completes which means that the publisher returned by getProfile() also never completes. Let’s fix that by making one more change to getProfile(): func getProfile() -> AnyPublisher<Data, URLError> { let tokenSubject = authenticator.tokenSubject() Donny Wals 237 Practical Combine return tokenSubject .flatMap({ token -> AnyPublisher<Data, URLError> in let url: URL = URL(string: "https://www.donnywals.com")! return URLSession.shared.dataTaskPublisher(for: url) .flatMap({ result -> AnyPublisher<Data, URLError> in if let httpResponse = result.response as? HTTPURLResponse, httpResponse.statusCode == 403 { self.authenticator.refreshToken(using: tokenSubject) return Empty().eraseToAnyPublisher() } return Just(result.data) .setFailureType(to: URLError.self) .eraseToAnyPublisher() }) .eraseToAnyPublisher() }) .handleEvents(receiveOutput: { _ in tokenSubject.send(completion: .finished) }) .eraseToAnyPublisher() } When the outermost flatMap emits a value I want to complete the tokenSubject immediately. Since any faulty values are replaced with Empty, data tasks created in the flatMap that’s applied to tokenSubject will only ever emit a value when the status code we received is good, and we replace the data task publisher with a Just publisher. And that’s all there is to it! Donny Wals 238 Practical Combine In Summary I saved this chapter until the end of the book because quite frankly, these kinds of pipelines can be mind-boggling if you try to tackle and understand them too soon in your Combine journey. In this chapter, you have seen three fairly advanced uses of Combine that all followed very similar patterns. Once you understand patterns like the once I’ve shown in this chapter, you’ll find that a lot of seemingly complex uses of Functional Reactive Programming actually follow the same sets of rules and patterns. But it takes practice to recognize and understand these patterns. In this chapter, I have hopefully helped you gain a deeper understanding of how you can model actions, requests or even access tokens in a way that allows you to drive entire pipelines by making good use of Combine’s Subjects, flatMap and other operators that you saw in this chapter. In the next and final chapter of this book, I won’t be teaching you anything new. I think you’re ready to go out and explore Combine in the real world. You have all the knowledge you might need, you understand all of the terminology and you’ve seen several examples of how you might integrate Combine in a real app. I hope you’ve enjoyed reading this book as much as I’ve enjoyed writing it, and I hope I’ve been able to translate everything I’ve learned about Combine into a format that’s useful and understandable for you. Thanks for reading this book and trusting me to teach you Combine! Donny Wals 239 Practical Combine Where to go from here? You’ve read this book and you already know a ton about Combine. However, I have only helped you scratch the surface of Combine and what you can do with it. In this short chapter, I would like to recommend some useful tools and resources to you that you might want to pick up or begin following to help you on your journey to fully master and implement Combine. First and foremost, I would like to recommend the amazing free website/book built by Joseph Heck. He put together an amazing online Combine reference that I have definitely used more than a couple of times while I was exploring Combine myself. You can find his website here: https://heckj.github.io/swiftui-notes/. If you enjoy this resource, make sure to grab the epub version on Gumroad: http://gumroad.com/l/usingcombine. It helps Josepth to maintain and improve his Combine content and he simply deserves all the support he can get. A very good resource written by an extremely active member of the Combine community is https://www.apeth.com/UnderstandingCombine/ which is created by Matt Neuburg. This website contains a ton of information on advanced and complicated Combine concepts. Moreover, if you ever ask a question on the Swift forums or stackoverflow, there’s a reasonable chance that Matt will jump in and help you! Normally I would link to Apple’s documentation on a topic I wrote about to help you explore more but unfortunately, Apple’s Combine documentation isn’t fantastic. It’s worth taking a look if you’re interested but I don’t think you’ll find anything that’s amazingly insightful or helpful there. I know I’ve been underwhelmed while writing this book a couple of times. That said, I hope Apple improves their documentation soon so I can update this paragraph with some kind words and a reference to Apple’s docs. If the final chapter of this book left you a little bit overwhelmed and you want to learn more about what I tend to call “The Combine Triad”, you should take a look at the talk I gave at dotSwift in February of 2020: https://www.dotconferences.com/2020/02/donny-walsthe-combine-triad. Reactive and functional programming are interesting topics and I think it’s absolutely worth it for you to take deep dives into these topics if you enjoyed this book. Daniel Steinberg has produced a lot of interesting content that involves functional programming over the years and I absolutely recommend that you look him up on YouTube and in particular his talk Donny Wals 240 Practical Combine Understanding Combine from Pragma conf 2019. All in all, you’re ready to start refactoring your codebase to integrate Combine. Try to start with small sections of your app, or maybe a part of your code that is nice and isolated from the rest like a networking layer. Work your way from there towards integrating Combine with your UI. This approach should work regardless of the UI framework you’re using. The best way to learn is to do, and with all the information from this book and the resources I’ve shared in this chapter, nothing is stopping you from achieving great results with Combine. Sure, you’ll get stuck sometimes, you’ll be confused and lost but I’m sure you’ll come out of it with more knowledge every single time. I’d like to take this moment to thank you for getting and reading my book one last time, I truly appreciate it and it means a lot to me. Don’t hesitate to send me any questions or feedback you have via an email to feedback@donnywals.com. I love to hear what you thought of this book, and I’d love to know how you’re integrating Combine in the projects you’re working on. Donny Wals 241