Swift Combine Cheat Sheet
- Swift Combine Cheat Sheet
- Publisher
- Subscriber
- Publisher Lifecycle
- Subject
- Operators
- Transform Publisher Output to New Publisher
- Scheduler
- Use Cases
- CollectionView via Combine
- Assigning Publisher Output to @Published Property
- Simple Theming System
- Updating UI based on User Input
- Search Query
- Combine multiple publishers
- Handle Network Call with Token
- UI based on Multiple Network Requests
- Ask for Push Permissions
- Observe UIControl Event with Publisher
Publisher
- emits value over time
- can optionally complete
- can complete with an error
Subject
- a mutable publisher
- CurrentValueSubject stores the current value
- PassthroughSubject doesn’t have a current value
Subscriber
- receives values from publishers
- built-in general purpose subscribers:
- sink(receiveValue:), sink(receiveCompletion:receiveValue)
- assign(to:on:)
Operator
- transforms values that are sent by publishers
Scheduler
- mechanism that defines the context for where and when the work is performed
- subscribe(on:) and receive(on:)
Publisher
Declares that a type can transmit a sequence of values over time.
- Publishers in Combine emits values over time, objects that receive these values are subscribers - a subscriber subscribes to the output of a publisher
Publishers
is an enum, representing a namespace for publisher types, e.g.Publishers.Sequence
is a publisher that publishes a given sequence of elements, e.g.
let publisher = ["cat", "dog", "monkey"].publisher
- type of above
publisher
isPublishers.Sequence<[String], Never>
, every publisher has an Output (e.g.[String]
) and a Failure (e.g.Never
)
Apple has created a bunch of publishers that are defined as extensions of objects.
data task publisher:
URLSession.shared.dataTaskPublisher(for: url)
Output
of the publisher is(data: Data, response: URLResponse)
and itsFailure
isURLError
- the publisher completes as soon as the request has either succeeded or failed because a request only executes once
default notification center publisher:
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotificaiton)
Output
of the publisher isNotification
,Failure
isNever
NotificationCenter.Publisher
emits values for the notification that is subscribed to, as long as app is alive
Future
A publisher that eventually produces a single value and then finishes or fails.
func generateAsyncRandomNumberFromFuture() -> Future <Int, Never> {
return Future() { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
promise(Result.success(number))
}
}
}
cancellable = generateAsyncRandomNumberFromFuture()
.sink { number in print("Got random number \(number).") }
- a
Future
begins executing its work immediately when it’s created, unlike other publishers that normally don’t emit values or perform work if they don’t have any subscribers - a
Future
will only run once, once aFuture
is completed, it will replay its output to new subscribers without running again until a newFuture
is created - wrapping a
Future
inDeferred
makes it behave exactly the same as any other Publisher
func createFuture() -> Future<Int, Never> {
Future { promise in
promise(.success(Int.random(in: (1...100))))
}
}
func createDeferredFuture() -> Deferred<Future<Int, Never>> {
return Deferred {
Future { promise in
promise(.success(Int.random(in: (1...100))))
}
}
}
Subscriber
A protocol that declares a type that can receive input from a publisher.
Built-in general-purpose subscribers
sink
:
Attaches a subscriber with closure-based behavior.
["apple", "orange", "banana"].publisher.sink(receiveCompletion: { completion in
print("completed: \(completion)")
}, receiveValue: { value in
print("value received: \(value)")
})
sink
is defined as an extension onPublisher
, it can be used to subscribe to every publishers in Combinesink
takes two closures, one for every emitted value and the other is called after the publisher has emitted its last value- the type of completion parameter sent to
receiveCompletion
isSubscribers.Completion<Self.Failure>
- there’s a version of
sink
for publishers that haveNever
asFailure
type, that thereceiveCompletion
closure is not needed, e.g.
[1, 2, 3].publisher.sink {
print("received value \($0)")
}
assign
:
Assigns each element from a publisher to a property on an object.
var player = Player()
["something nice"]
.publisher
.assign(to: \.introduction, on: player)
assign
is defined on Publisher, used to assign publisher values directly to a property on an objectassign
requires the key path of the object that the value is assigned to, has to be aReferenceWriteableKeyPath
, basically the key path must belong to class
Publisher Lifecycle
- a publisher will not emit any value until it has a subscriber
sink
andassign
create subscribers that are always willing to receive values- subscriber-driven emission has a terminology called backpressure management
- both
sink
andassign
return anAnyCancellable
, when it’s deallocated, the subscription associated with the object is destroyed AnyCancellable
has astore(in:)
method, takes aninout
parameter, can be used to append theAnyCancellable
to a set- Combine comes with a number of operators that can be used to transform the values received from publisher, e.g.
[10, 14, 23]
.publisher
.map { "The area of the square with size \($0) is \($0 * $0)" }
.sink { string in
label.text = string
}
["2", "birds", "with", "1", "stone"]
.map { Int($0) }
.replaceNil(with: 0)
.sink { print($0) }
// 2
// 0
// 0
// 1
// 0
Subject
A subject is a publisher that you can use to ”inject” values into a stream, by calling its send(_:) method. This can be useful for adapting existing imperative code to the Combine model.
PassthroughSubject
: used to send a stream of values from their origin to its subscribers through the publisher. It provides a convenient way to adapt existing imperative code to the Combine model. The origin of the values does not hold a state soPassthroughSubject
does not hold onto the values that it has sent. To keep a state of value stream, useCurrentValueSubject
class Hero {
var mana = CurrentValueSubject<Int, Never>(10)
let manaCostPerPowerLevel = 1
func castMagic(powerLevel: Int) {
let manaNeeded = powerLevel * manaCostPerPowerLevel
mana.value -= manaNeeded
}
}
var cancellables = Set<AnyCancellable>()
let hero = Hero()
hero
.mana
.sink {
print("Current mana: \($0)")
}
.store(in: &cancellables)
CurrentValueSubject
: a subject that wraps a single value and publishes a new element whenever the value changes, it maintains a buffer of the most recently published element- changing the
value
property ofCurrentValueSubject
automatically sends the new value to its subscribers CurrentValueSubject
immediately sends its current value to any new subscribers,CurrentValueSubject
always has initial value to send to its subscribers because it must be initialized with an initial value
@Published property wrapper
Wrapping properties with the @Published
property wrapper will turn them into publishers.
class Hero {
@Published var mana = 10 // 1
let manaCostPerPowerLevel = 1
func castMagic(powerLevel: Int) {
let manaNeeded = powerLevel * manaCostPerPowerLevel
mana -= manaNeeded // 2
}
}
var cancellables = Set<AnyCancellable>()
let hero = Hero()
hero
.$mana // 3
.sink {
print("Current mana: \($0)")
}
.store(in: &cancellables)
- replaces
var mana = CurrentValueSubject<Int, Never>(10)
with@Published var mana = 10
- the value of the property can be accessed and modified directly
- to subscribe to a
@Published
property, we need to use$
prefix on the property name
This is a special convention for property wrapper that allows you to access the wrapper itself, aka “projected value”, rather than the value that is wrapped by the property. In the case where the wrapper’s projected value is a publisher, which can be subscribed to using $mana
.
@Published
property updates its underlying value after emitting the value to its subscribers;CurrentValueSubject
updates its value before emitting the value to its subscribers@Published
can be used only on properties of classes whileCurrentValueSubject
can be used for both structs and classes- it’s not possible to call
send(_:)
on a@Published
property because it’s not aSubject
- assigning a new value to
@Published
property automatically emits the new value to subscriber, equivalent to callingsend(_:)
with a new value
class Counter {
@Published var publishedValue = 1
var subjectValue = CurrentValueSubject<Int, Never>(1)
}
let c = Counter()
c.$publishedValue.sink { print("published", $0 == c.publishedValue) }
c.subjectValue.sink { print("subject", $0 == c.subjectValue.value) }
c.publishedValue = 2
c.subjectValue.value = 2
Output:
published true
subject true
published false
subject true
assign(to:on:)
assign(to:on:)
creates a new subscriber, returns anAnyCancellable
- assigning to keypath on self causes a retain cycle, this seems to be fixed in iOS 14
Assign Output of a Publisher with assign(to:on:)
class Hero {
@Published var mana = 10
let manaCostPerPowerLevel = 1
}
struct HeroViewModel {
var hero: Hero
lazy var statusSubject: AnyPublisher<String?, Never> = {
hero.$mana.map {
"Hero now has \($0) mana left"
}.eraseToAnyPublisher()
}
mutating func castMagic(powerLevel: Int) {
let manaNeeded = powerLevel * manaCostPerPowerLevel
hero.mana -= manaNeeded
}
}
class HeroView {
let statusLabel = UILabel()
let castButton = UIButton()
var viewModel: HeroViewModel
var cancellables = Set<AnyCancellable>()
init(viewModel: HeroViewModel) {
self.viewModel = viewModel
}
func setupLabel() {
viewModel.statusSubject
.assign(to: \.text, on: statusLabel)
.store(in: &cancellables)
}
func didTapCastButton() {
viewModel.castMagic(powerLevel: 2)
}
}
Operators
debounce
Built-in operator that limits publisher’s output by ignoring values that are rapidly followed by another value.
Use the debounce(for:scheduler:options:)
operator to control the number of values and time between delivery of values from the upstream publisher. This operator is useful to process bursty or high-volume event streams where you need to reduce the number of values delivered to the downstream to a rate you specify.
removeDuplicates
Publishes only elements that don’t match the previous element.
Use removeDuplicates()
to remove repeating elements from an upstream publisher. This operator has a two-element memory: the operator uses the current and previously published elements as the basis for its comparison.
filter
Republishes all elements that match a provided closure.
Combine’s filter(_:) operator performs an operation similar to that of filter
in the Swift Standard Library: it uses a closure to test each element to determine whether to republish the element to the downstream subscriber.
throttle
Publishes either the most-recent or first element published by the upstream publisher in the specified time interval.
Use throttle(for:scheduler:latest:)
to selectively republish elements from an upstream publisher during an interval you specify. Other elements received from the upstream in the throttling interval aren’t republished.
Transform Publisher Output to New Publisher
Sometimes you need to transform the output of a publisher into a different publisher:
var cancellables = Set<AnyCancellable>()
[1, 2, 3]
.publisher
.print()
.flatMap { value in
Array(repeating: value, count: 2).publisher
}
.sink { print("Got value: \($0)") }
.store(in: &cancellables)
Output:
receive subscription: ([1, 2, 3])
request unlimited
receive value: (1)
Got value: 1
Got value: 1
receive value: (2)
Got value: 2
Got value: 2
receive value: (3)
Got value: 3
Got value: 3
receive finished
flatMap
is used here because usingmap
would produce threeSequence<Array<Int>, Never>
which won’t be one flattend sequence of publishers- to limit the number of active publishers produced by
flatMap
, pass themaxPublishers
toflatMap
, e.g.:
var cancellables = Set<AnyCancellable>()
[1, 2, 3]
.publisher
.print()
.flatMap(maxPublishers: .max(1)) { value in
Array(repeating: value, count: 2).publisher
}
.sink { print("Got value: \($0)") }
.store(in: &cancellables)
Output:
receive subscription: ([1, 2, 3])
request max: (1)
receive value: (1)
Got value: 1
Got value: 1
request max: (1)
receive value: (2)
Got value: 2
Got value: 2
request max: (1)
receive value: (3)
Got value: 3
Got value: 3
request max: (1)
receive finished
flatMap
requires the type of new publisher must match that of the source publisher, unless on iOS 14 and source publisher’sFailure
isNever
. Below iOS 14 we need to make sure that the publisher weflatMap
over has the same failure type as the publisher that create in theflatMap
:
let baseURL = URL(string: "https://www.test.com")!
var cancellables = Set<AnyCancellable>()
["/", "/content", "/pages", "/articles"]
.publisher
.setFailureType(to: URLError.self)
.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)
Apply Operators that might Throw Error
Operators with a try prefix can throw error:
enum MyError: Error {
case underEighteen
}
var cancellables = Set<AnyCancellable>()
[18, 17, 22, 35]
.publisher
.tryMap { age in
guard age >= 18 else {
throw MyError.underEighteen
}
return "Another player of age \(age) entered the game."
}
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
Output:
Another player of age 18 entered the game.
failure(__lldb_expr_13.MyError.underEighteen)
- publishers can only complete or emit an error once, after an error is thrown, the publisher can’t emit new values
Define Custom Operators
extension Publisher where Output == String, Failure == Never {
func toURLSessionDataTask(baseURL: URL) -> AnyPublisher<URLSession.DataTaskPublisher.Output, URLError> {
if #available(iOS 14, *) {
return flatMap {
let url = baseURL.appendingPathComponent($0)
return URLSession.shared.dataTaskPublisher(for: url)
}
.eraseToAnyPublisher()
} else {
return setFailureType(to: URLError.self)
.flatMap {
let url = baseURL.appendingPathComponent($0)
return URLSession.shared.dataTaskPublisher(for: url)
}
.eraseToAnyPublisher()
}
}
}
["path1", "path2"]
.publisher
.toURLSessionDataTask(baseURL: URL(string: "https://myweb.com")!)
.sink { print($0) }
eraseToAnyPublisher()
removes all type information from the publisher and wraps it in anAnyPublisher
- before:
Publisher.FlatMap<P, Publishers.SetFailureType<Self, URLError>>
- after:
AnyPublisher<URLSession.DataTaskPublisher.Output, URLError>
- before:
Scheduler
Scheduler is the synchronization mechanism of the Combine framework, which defines the context for where and when the work is performed.
Combine does not work directly with threads. Instead, it allows Publishers to operate on specific Schedulers.
receive(on:) and subscribe(on:)
By default, Combine applies a default scheduler to the work, and the default scheduler will emit values downstream on the thread they were generated on.
subscribe(on:)
affects how objects subscribe to a publisher, including the entire chain of publishers and operators; It’s used for controlling the upstream opeartions: subscribe, cancel, and request inputreceive(on:)
sets the scheduler where the downstream receives output on, applyreceive(on:)
to a publisher will make sure that all events are delivered on the provided scheduler- it’s recommended to apply
receive(on:)
just beforesink
extension Publishers {
struct IsMainThread: Publisher {
typealias Output = String
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber,
Never == S.Failure, String == S.Input {
debugPrint("IsMainThread: \(Thread.isMainThread)")
subscriber.receive(subscription: Subscriptions.empty)
DispatchQueue.main.async {
_ = subscriber.receive("test")
}
}
}
}
Without subscribe(on:)
:
Publishers.IsMainThread()
.sink { _ in
print("Sink: \(Thread.isMainThread)")
}
.store(in: &cancellables)
// output:
// "IsMainThread: true"
// Sink: true
With subscribe(on: DispatchQueue.global())
:
Publishers.IsMainThread()
.subscribe(on: DispatchQueue.global())
.sink { _ in
print("Sink: \(Thread.isMainThread)")
}
.store(in: &cancellables)
// output:
// "IsMainThread: false"
// Sink: true
Switching receive(on:)
:
Publishers.IsMainThread() // 1
.receive(on: DispatchQueue.global()) // 2
.map { value -> String in // 3
debugPrint("Map: \(Thread.isMainThread)")
return value
}
.receive(on: DispatchQueue.main) // 4
.sink { _ in // 5
print("Sink: \(Thread.isMainThread)")
}
.store(in: &cancellables)
// output:
// "IsMainThread: true"
// "Map: false"
// Sink: true
- the subscription is created on the current queue - the main queue
- switch the executuion to a new global queue
map()
executes- switch to main queue
- sink executes
Mix and Match
In a multi-threaded environment it’s likely that you need a chain of operations including fetching data, processing data and updates UI, receive(on:)
and subscribe(on:)
can be mixed and matched:
[Resource Intensive Publisher]
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
[Resource Intensive Processing of Output]
.receive(on: DispatchQueue.main)
[Update App UI on the Main Queue]
Use Cases
CollectionView via Combine
Use combine to drive collection view, when the underlying data changes, update the collection view so it displays newest data.
struct Player: Hashable, Decodable {
let firstName: String
let lastName: String
let photoURL: URL
}
class DataProvider {
let dataSubject = CurrentValueSubject<[Player], Never>([])
func fetch() {
let mockPlayers = [
Player(firstName: "Lei",
lastName: "Li",
photoURL: URL(string: "http://player.photo/lei")!),
Player(firstName: "Meimei",
lastName: "Han",
photoURL: URL(string: "http://player.photo/meimei")!)
]
dataSubject.value = mockPlayers
}
func fetchImage(for player: Player) -> AnyPublisher<UIImage?, URLError> {
URLSession.shared.dataTaskPublisher(for: player.photoURL)
.map { UIImage(data: $0.data) }
.eraseToAnyPublisher()
}
}
final class PlayerCell: UICollectionViewCell {
var cancellable: Cancellable?
let imageView = UIImageView()
// setup code omitted
func configure(with player: Player) {
// configure the cell
}
}
final class PlayersViewController: UIViewController {
private let dataProvider = DataProvider()
private var cancellables = Set<AnyCancellable>()
private lazy var dataSource: UICollectionViewDiffableDataSource<Int, Player> = {
.init(collectionView: collectionView) { collectionView, indexPath, player in
let cell = // dequeue PlayerCell
cell.configure(with: player)
cell.cancellable = self.dataProvider.fetchImage(for: player)
.receive(on: DispatchQueue.main)
.sink { cell.imageView.image = $0 }
// .assign(to: \.image, on: cell.imageView)
return cell
}
}()
override func viewDidLoad() {
super.viewDidLoad()
dataProvider.fetch()
dataProvider.dataSubject
.sink { self.applySnapshots($0) }
.store(in: &cancellables)
}
private func applySnapshots(_ players: [Player]) {
var snapshot = NSDiffableDataSourceSnapshot<Int, Player>()
snapshot.appendSections([0])
snapshot.appendItems(players)
dataSource.apply(snapshot)
}
}
Assigning Publisher Output to @Published Property
Building an object that exposes a @Published
property that represents some kind of state that can be updated due to external factors.
class DataProvider {
@Published var fetchedPlayers = [Player]()
private var currentPlayerID = 0
func fetchNextPlayer() {
let url = URL(string: "https://mygameserver.com/player/\(currentPlayerID)")!
currentPlayerID += 1
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { [weak self] value -> [Player] in
let players = try JSONDecoder().decode([Player].self, from: value)
return (self?.fetchedPlayers ?? []) + players
}
.replaceError(with: fetchedPlayers)
.assign(to: &$fetchedPlayers)
}
}
assign(to:)
can only be used on publishers that haveNever
as theirFailure
type because a@Published
property also hasNever
as itsFailure
type. In above code this is achieved by replacing all errors with a default value- there’s no need to hold onto an
AnyCancellable
after callingassign(to:)
, it manages its ownCancellable
and no one is returned
Simple Theming System
class ColorManager {
enum PreferredUserInterfaceStyle: String {
case lowEyeStrain, highContrast, system
}
lazy private(set) var themeSubject: CurrentValueSubject<PreferredUserInterfaceStyle, Never> = {
var preferredStyle = PreferredUserInterfaceStyle.system
return CurrentValueSubject<PreferredUserInterfaceStyle, Never>(currentInterfaceStyle)
}()
private var currentInterfaceStyle: PreferredUserInterfaceStyle {
get {
guard let styleValue = UserDefaults.standard.string(forKey: "preferredUserInterfaceStyle") else {
return .system
}
return PreferredUserInterfaceStyle(rawValue: styleValue) ?? .system
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "preferredUserInterfaceStyle")
}
}
}
class SomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
themeManager.themeSubject.sink { style in
updateUI(for: style)
}
}
private func updateUI(for: style) {
// …
}
}
Updating UI based on User Input
class ViewController: UIViewController {
let slider = UISlider()
let label = UILabel()
@Published var sliderValue: Float = 40
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
$sliderValue
.map { "Slider is at \($0)" }
.assign(to: \.text, on: label)
.store(in: &cancellables)
$sliderValue
.assign(to: \.value, on: slider)
.store(in: &cancellables)
slider.addTarget(self, #selector(updateLabel), for: .valueChanged)
}
@objc
func updateLabel() {
sliderValue = slider.value
}
}
Search Query
Perform search as user types:
final class ResultViewController: UIViewController {
let textField = UITextField()
let label = UILabel()
@Published var searchQuery: String?
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(textChagned), for: .editingChanged)
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.global()) // 1
.filter { ($0 && "").count > 2 } // 2
.removeDuplicates() // 3
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
@objc
func textChanged() {
searchQuery = textField.text
}
}
- ignore values that are rapidly followed by another value; the timer is scheduled on global queue rather than the main queue, in order to not pause the timer when main queue is busy. For example, you might be scrolling through a table view while the timer is running in which case the timer will be paused until the scroll ends
- ignore values that are less than or equal to 2 characters
- ignore values that are duplicates of the previous values
Combine multiple publishers
Publishers.Zip
A publisher created by applying the zip function to two upstream publishers.
Use Publishers.Zip to combine the latest elements from two publishers and emit a tuple to the downstream. The returned publisher waits until both publishers have emitted an event, then delivers the oldest unconsumed event from each publisher together as a tuple to the subscriber.
If either upstream publisher finishes successfully or fails with an error, so too does the zipped publisher.
In addition to Publishers.Zip
, there are Publishers.Zip3
and Publishers.Zip4
.
Publishers.Zip(first, second)
.sink { print("Zipped value: \($0)") }
.store(in: &cancellables)
first.zip(with: second)
.sink { print("Zipped value \($0)") }
.store(in: &cancellables)
Publishers.Merge
A publisher created by applying the merge function to two upstream publishers.
Publishers.Merge
emits a new value every time one of the publishers it merges emits a new value. Only a single value is emitted by interleaving all emiited values from the publishers that it merges.
Apple provides various versions of Publishers.Merge
up to Publishers.Merge8
and Publishers.MergeMany
. All versions of Pubilshers.Merge
require that the publishers being merged have the same Output
and Failure
types.
Publishers.Merge(first, second)
.sink { print("Merged value: \($0)") }
.store(in: &cancellables)
first.merge(with: second)
.sink { print("Merged value: \($0)") }
.store(in: &cancellables)
Publishers.CombineLatest
Subscribes to an additional publisher and invokes a closure upon receiving output from either publisher.
Use combineLatest<P,T>(_:)
to combine the current and one additional publisher and transform them using a closure you specify to publish a new value to the downstream. The combined publisher doesn’t produce elements until each of its upstream publishers publishes at least one element.
All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes. If any of the combined publishers terminates with a failure, this publisher also fails.
switchToLatest
Republishes elements sent by the most recently received publishers. Every time a publisher receives a new ‘Publisher’ as output, the previous subscriptions from the stream will automatically be cancelled.
Handle Network Call with Token
struct APIError: Decodable, Error {
let statusCode: Int
}
func refreshToken() -> AnyPublisher<Bool, Never> {
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()
}
UI based on Multiple Network Requests
Display a screen of sport news, including:
- sports highlights
- favourite games
- domestic games
- international games
- recommended games
enum SectionType: String, Decodable {
case highlights, favorites, recommended
}
struct NewsItem: Decodable, Hashable {
let title: String
let body: String
}
struct Content {
let items: [NewsItem]
let sectionType: SectionType
static func highlights(items: [NewsItem]) -> Content {
Content(items: items, sectionType: .highlights)
}
static func favorites(items: [NewsItem]) -> Content {
Content(items: items, sectionType: .favorites)
}
static func recommended(items: [NewsItem]) -> Content {
Content(items: items, sectionType: .recommended)
}
}
var highlightsPublisher = URLSession.shared.dataTaskPublisher(for: highlightsContentURL)
.map { $0. data }
.decode(type: [NewsItem].self, decoder: JSONDecoder())
.replaceError(with: [NewsItem]())
.map { Content.highlights(items: $0) }
.eraseToAnyPublisher()
var recommendedPublisher = URLSession.shared.dataTaskPublisher(for: recommendedContentURL)
.map { $0.data }
.decode(type: [NewsItem].self, decoder: JSONDecoder())
.replaceError(with: [NewsItem]())
.map { Content.recommended(items: $0) }
.eraseToAnyPublisher()
class LocalFavorites {
static func fetchAll() -> AnyPublisher<[NewsItem], Never> {
// …
}
}
var localFavoritesPublisher = LocalFavorites.fetchAll()
var remoteFavoritesPublisher = URLSession.shared.dataTaskPublisher(for: favoritesContentURL)
.map { $0.data }
.decode(type: [NewsItem].self, decoder: JSONDecoder())
.replaceError(with: [Event]())
.eraseToAnyPublisher()
var favouritesPublisher = Publishers.Zip(localFavoritesPublisher, remoteFavoritesPublisher)
.map {
let uniqueFavorites = Set($0.0 + $0.1)
return Content.favorites(items: Array(uniqueFavorites))
}
.eraseToAnyPublisher()
var homeContentPublisher = Publishers.Merge3(highlightsPublisher,
favoritesPublisher,
recommendedPublisher)
homeContentPublisher
.sink { content in
switch content.sectionType {
case .highlight: // render
case .favorites: // render
case .recommended: // render
}
}
.store(in: &cancellables)
Ask for Push Permissions
extension UNUserNotificationCenter {
func getNotificationSettings() -> Future<UNNotificationSettings, Never> {
Future { promise in
self.getNotificationSettings { settings in
promise(.success(settings))
}
}
}
func requestAuthorization(options: UNAuthorizationOptions) -> Future<Bool, Error> {
Future { promise in
self.requestAuthorization(options: options) { result, error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(result))
}
}
}
}
}
- as soon as
requestAuthorization(options:)
is called, the closure passed to theFuture
immediately executes even without subscriber, and the result will be broadcast to all future subscribers. In this case we wouldn’t want to trigger theFuture
more than once
var cancellables = Set<AnyCancellable>()
UNUserNotificationCenter.current().getNotificationSettings()
.flatMap { settings -> AnyPublisher<Bool, Never> in
switch settings.authorizationStatus {
case .notDetermined:
return UNUserNotificationCenter.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 { hasPermission in
if hasPermission {
// …
} else {
// …
}
}
.store(in: &cancellables)
Observe UIControl Event with Publisher
Refer to: https://github.com/CombineCommunity/CombineCocoa for more.
Build a UIControl
extension that can be subscribed to deal with specified event:
slider
.publisher(for: .valueChanged)
.sink { control in
guard let slider = control as? UISlider else {
return
}
// handle slider.value
}
Extension:
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(onEvent), for: event)
}
func request(_ demand: Subscribers.Demand) {
currentDemand += demand
}
func cancel() {
subscriber = nil
control.removeTarget(self, action: #selector(onEvent), for: event)
}
@objc
func onEvent() {
if currentDemand > 0 {
currentDemand += subscriber?.receive(control) ?? .none
currentDemand -= 1
}
}
}
struct EventPublisher: Publisher {
typealias Output = UIControl
typealias Failure = Never
let control: UIControl
let controlEvent: UIControl.Event
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, UIControl == S.Input {
let subscription = EventSubscription(control: control, event: controlEvent, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
func publisher(for event: Event) -> EventPublisher {
return EventPublisher(control: self, controlEvent: event)
}
}