UIKit and SwiftUI Integration
Declarative UI updates from user input
Goal:
Querying a API and returning the data to be displayed in your UI
A pattern for integrating Combine with UIKit is setting up a variable which will hold a reference to the updated state, and linking the controls using IBAction.
The sample is a portion of the code at in a larger view controller implementation.
This example overlaps with the next pattern Cascading UI updates including a network request, which builds upon the initial publisher.
UIKit-Combine/GithubViewController.swift
import UIKit
import Combine
class GithubViewController: UIViewController {
@IBOutlet weak var github_id_entry: UITextField! 1️⃣
var usernameSubscriber: AnyCancellable?
// username from the github_id_entry field, updated via IBAction
// @Published is creating a publisher $username of type <String, Never>
@Published var username: String = "" 2️⃣
// github user retrieved from the API publisher. As it's updated, it
// is "wired" to update UI elements
@Published private var githubUserData: [GithubAPIUser] = []
// MARK - Actions
@IBAction func githubIdChanged(_ sender: UITextField) {
username = sender.text ?? "" 3️⃣
print("Set username to ", username)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
usernameSubscriber = $username 4️⃣
.throttle(for: 0.5, scheduler: myBackgroundQueue, latest: true) 5️⃣
// ^^ scheduler myBackGroundQueue publishes resulting elements
// into that queue, resulting on this processing moving off the
// main runloop.
.removeDuplicates() 6️⃣
.print("username pipeline: ") // debugging output for pipeline
.map { username -> AnyPublisher<[GithubAPIUser], Never> in 7️⃣
return GithubAPI.retrieveGithubUser(username: username)
}
// ^^ type returned by retrieveGithubUser is a Publisher, so we use
// switchToLatest to resolve the publisher to its value
// to return down the chain, rather than returning a
// publisher down the pipeline.
.switchToLatest() 8️⃣
// using a sink to get the results from the API search lets us
// get not only the user, but also any errors attempting to get it.
.receive(on: RunLoop.main)
.assign(to: \.githubUserData, on: self) 9️⃣1️⃣ The
UITextFieldis the interface element which is driving the updates from user interaction.2️⃣ We defined a
@Publishedproperty to both hold the data and reflect updates when they happen. Because its a@Publishedproperty, it provides a publisher that we can use with Combine pipelines to update other variables or elements of the interface.3️⃣ We set the variable
*username*from within anIBAction, which in turn triggers a data flow if the publisher$usernamehas any subscribers.4️⃣ We in turn set up a subscriber on the publisher
$usernamethat does further actions. In this case it uses updated values of username to retrieves an instance of aGithubAPIUserfrom Github’s REST API. It will make a new HTTP request to the every time the username value is updated.5️⃣ The
throttleis there to keep from triggering a network request on every possible edit of the text field. The throttle keeps it to a maximum of 1 request every half-second.6️⃣
removeDuplicatescollapses events from the changing username so that API requests are not made on the same value twice in a row. TheremoveDuplicatesprevents redundant requests from being made, should the user edit and the return the previous value.7️⃣
mapis used similarly toflatMapin error handling here, returning an instance of a publisher. The API object returns a publisher, which this map is invoking. This doesn’t return the value from the call, but the publisher itself.8️⃣
switchToLatestoperator takes the instance of the publisher and resolves out the data.switchToLatestresolves a publisher into a value and passes that value down the pipeline, in this case an instance of[GithubAPIUser].9️⃣ And
assignat the end up the pipeline is the subscriber, which assigns the value into another variable:githubUserData.
The pattern Cascading UI updates including a network request expands upon this code to multiple cascading updates of various UI elements.
Cascading multiple UI updates, including a network request
Goal:
Have multiple UI elements update triggered by an upstream subscriber
References:
The ViewController with this code is in the github project at UIKit-Combine/GithubViewController.swift. You can see this code in operation by running the UIKit target within the github project.
The GithubAPI is in the github project at UIKit-Combine/GithubAPI.swift
The example provided expands on a publisher updating from Declarative UI updates from user input, adding additional Combine pipelines to update multiple UI elements as someone interacts with the provided interface.
The general pattern of this view starts with a textfield that accepts user input, from which the following actions flow:
Using an
IBActionthe@Publishedusernamevariable is updated.We have a subscriber (
usernameSubscriber) attached$usernamepublisher, which publishes the value on change and attempts to retrieve the GitHub user. The resulting variablegithubUserData(also@Published) is a list of GitHub user objects. Even though we only expect a single value here, we use a list because we can conveniently return an empty list on failure scenarios: unable to access the API or the username isn’t registered at GitHub.We have the
passthroughSubjectapiNetworkActivitySubscriberto reflect when theGithubAPIobject starts or finishes making network requests.We have a another subscriber
repositoryCountSubscriberattached to$githubUserDatapublisher that pulls the repository count off the github user data object and assigns it to a text field to be displayed.We have a final subscriber
avatarViewSubscriberattached to$githubUserDatathat attempts to retrieve the image associated with the user’s avatar for display.
Tips: The empty list is useful to return because when a
usernameis provided that doesn’t resolve, we want to explicitly remove any avatar image that was previously displayed. To do this, we need the pipelines to fully resolve to some value, so that further pipelines are triggered and the relevant UI interfaces updated. If we used an optionalString?instead of an array ofString[], the optional does not trigger some of the pipeline when it isnil, and we always want a result value - even an empty value - to come from the pipeline.
The subscribers (created with assign and sink) are stored as AnyCancellable variables on the view controller instance. Because they are defined on the class instance, the Swift compiler creates deinitializers which will cancel and clean up the publishers when the class is torn down.
Info: A number of developers comfortable with RxSwift are using a "CancelBag" object to collect cancellable references, and cancel the pipelines on tear down. An example of this can be seen at here. This is accommodated within Combine with the
storefunction onAnyCancellablethat easily allows you to put a reference to the subscriber into a collection, such asSet<AnyCancellable>.
The pipelines have been explicitly configured to work on a background queue using the subscribe operator. Without that additional detail configured, the pipelines would be invoked and run on the main runloop since they were invoked from the UI, which may cause a noticeable slow-down in responsiveness in the user interface. Likewise when the resulting pipelines assign or update UI elements, the receive operator is used to transfer that work back onto the main runloop.
Warning: To have the UI continuously updated from changes propagating through
@Publishedproperties, we want to make sure that any configured pipelines have a<Never>failure type. This is required for theassignoperator. It is also a potential source of bugs when using asinkoperator. If the pipeline from a@Publishedvariable terminates to asinkthat accepts an Error failure type, thesinkwill send a termination signal if an error occurs. This will then stop the pipeline from any further processing, even when the variable is updated.
1️⃣ The decodable struct created here is a subset of what’s returned from the GitHub API. Any pieces not defined in the struct are simply ignored when processed by the
decodeoperator.2️⃣ The code to interact with the GitHub API was broken out into its own object, which I would normally have in a separate file. The functions on the API struct return publishers, and are then mixed and merged with other pipelines in the
ViewController.3️⃣ This struct also exposes a publisher using
passthroughSubjectto reflect Boolean values when it is actively making network requests.4️⃣ I first created the pipelines to return an optional
GithubAPIUserinstance, but found that there wasn’t a convenient way to propagate "nil" or empty objects on failure conditions. The code was then recreated to return a list, even though only a single instance was ever expected, to conveniently represent an "empty" object. This was important for the use case of wanting to erase existing values in following pipelines reacting to theGithubAPIUserobject "disappearing" - removing the repository count and avatar images in this case.5️⃣ The logic here is simply to prevent extraneous network requests, returning an empty result if the username being requested has less than 3 characters.
6️⃣ the
handleEventsoperator is how we are triggering updates for the network activity publisher. We define closures that trigger on subscription and finalization (both completion and cancel) that invokesend()on thepassthroughSubject. This is an example of how we can provide metadata about a pipeline’s operation as a separate publisher.7️⃣
tryMapadds additional checking on the API response from github to convert correct responses from the API that aren’t valid User instances into a pipeline failure condition.8️⃣
decodetakes the Data from the response and decodes it into a single instance ofGithubAPIUser9️⃣
mapis used to take the single instance and convert it into a list of1item, changing the type to a list ofGithubAPIUser:[GithubAPIUser].🔟
catchoperator captures the error conditions within this pipeline, and returns an empty list on failure while also converting the failure type toNever.1️⃣1️⃣
eraseToAnyPublishercollapses the complex types of the chained operators and exposes the whole pipeline as an instance of AnyPublisher.
UIKit-Combine/GithubViewController.swift
1️⃣ We add a subscriber to our previous controller from that connects notifications of activity from the GithubAPI object to our activity indicator.
2️⃣ Where the
usernameis updated from theIBAction(from our earlier example Declarative UI updates from user input) we have the subscriber make the network request and put the results in a new variable (also@Published) on ourViewController.3️⃣ The first subscriber is on the publisher
$githubUserData. This pipeline extracts the count of repositories and updates the UI label instance. There is a bit of logic in the middle of the pipeline to return the string "unknown" when the list is empty.4️⃣ The second subscriber is connected to the publisher
$githubUserData. This triggers a network request to request the image data for the github avatar. This is a more complex pipeline, extracting the data fromgithubUser, assembling a URL, and then requesting it. We also usehandleEventsoperator to trigger updates to theactivityIndicatorin our view. We usereceiveto make the requests on a background queue and later to push the results back onto the main thread in order to update UI elements. Thecatchand failure handling returns an emptyUIImageinstance in the event of failure.5️⃣ A final subscriber is attached to the
UILabelitself. Any Key-Value Observable object fromFoundationcan produce a publisher. In this example, we attach a publisher that triggers a print statement that the UI element was updated.
Info: While we could simply attach pipelines to UI elements as we’re updating them, it more closely couples interactions to the actual UI elements themselves. While easy and direct, it is often a good idea to make explicit state and updates to separate out actions and data for debugging and understandability. In the example above, we use two
@Publishedproperties to hold the state associated with the current view. One of which is updated by anIBAction, and the second updated declaratively using a Combine publisher pipeline. All other UI elements are updated publishers hanging from those properties getting updated.
Merging multiple pipelines to update UI elements
Goal:
Watch and react to multiple UI elements publishing values, and updating the interface based on the combination of values updated.
References:
The
ViewControllerwith this code is in the github project at UIKit-Combine/FormViewController.swift
This example intentionally mimics a lot of web form style validation scenarios, but within
UIKitand usingCombine.

A viewController is set up with multiple elements to declaratively update. The viewController hosts 3 primary text input fields:
value1value2value2_repeat
It also hosts a button to submit the combined values, and two labels to provide feedback.
The rules of these update that are implemented:
The entry in
value1has to be at least5characters.The entry in
value2has to be at least5characters.The entry in
value2_repeathas to be the same asvalue2.
If any of these rules aren’t met, then we want the submit button to be disabled and relevant messages displayed explaining what needs to be done.
This can be achieved by setting up a cascade of pipelines that link and merge together.
There is a
@Publishedproperty matching each of the user input fields.combineLatestis used to take the continually published updates from the properties and merge them into a single pipeline. Amapoperator enforces the rules about characters required and the values needing to be the same. If the values don’t match the required output, we pass anilvalue down the pipeline.Another validation pipeline is set up for
value1, just using amapoperator to validate the value, or returnnil.The logic within the
mapoperators doing the validation is also used to update the label messages in the user interface.A final pipeline uses
combineLatestto merge the two validation pipelines into a single pipeline. A subscriber is attached to this combined pipeline to determine if the submission button should be enabled.
The example below shows these all connected.
UIKit-Combine/FormViewController.swift
1️⃣ The start of this code follows the same patterns laid out in Declarative UI updates from user input.
IBActionmessages are used to update the@Publishedproperties, triggering updates to any subscribers attached.2️⃣ The first validation pipeline uses a
mapoperator to take the string value input and convert it tonilif it doesn’t match the validation rules. This is also converting the output type from the published property of<String>to the optional<String?>. The same logic is also used to trigger updates to the messages label to provide information about what is required.3️⃣ Since we are updating user interface elements, we explicitly make those updates wrapped in
DispatchQueue.main.asyncto invoke on the main thread.4️⃣
combineLatesttakes two publishers and merges them into a single pipeline with an output type that is the combined values of each of the upstream publishers. In this case, the output type is a tuple of (<String>, <String>).5️⃣ Rather than use
DispatchQueue.main.async, we can use thereceiveoperator to explicitly run the next operator on the main thread, since it will be doing UI updates.6️⃣ The two validation pipelines are combined with
combineLatest, and the output of those checked and merged into a single tuple output.7️⃣ We could store the assignment pipeline as an
AnyCancellable?reference (to map it to the life of theviewController) but another option is to create something to collect all the cancellable references. This starts as an empty set, and any sinks or assignment subscribers can be added to it to keep a reference to them so that they operate over the full lifetime of the view controller. If you are creating a number of pipelines, this can be a convenient way to maintain references to all of them.8️⃣ If any of the values are
nil, themapoperator returnsnildown the pipeline. Checking against anilvalue provides the boolean used to enable (or disable) the submission button.9️⃣ the
storemethod is available on theCancellableprotocol, which is explicitly set up to support saving off references that can be used to cancel a pipeline.
Creating a repeating publisher by wrapping a delegate based API
Goal:
To use one of the Apple delegate APIs to provide values for a Combine pipeline.
Where a
Futurepublisher is great for wrapping existing code to make a single request, it doesn’t serve as well to make a publisher that produces lengthy, or potentially unbounded, amount of output.
Apple’s Cocoa APIs have tended to use a object/delegate pattern, where you can opt in to receiving any number of different callbacks (often with data). One such example of that is included within the CoreLocation library, which offers a number of different data sources.
If you want to consume data provided by one of these kinds of APIs within a pipeline, you can wrap the object and use passthroughSubject to expose a publisher. The sample code below shows an example of wrapping CoreLocation’s CLManager object and consuming the data from it through a UIKit viewController.
UIKit-Combine/LocationHeadingProxy.swift
1️⃣
CLLocationManageris the heart of what is being wrapped, part ofCoreLocation. Because it has additional methods that need to be called for using the framework, I exposed it as a public read-only property. This is useful for requesting user permission to use the location API, which the framework exposes as a method onCLLocationManager.2️⃣ A
privateinstance ofPassthroughSubjectwith the data type we want to publish provides our inside-the-class access to forward data.3️⃣ A
publicpropertypublisherexposes the publisher from that subject for external subscriptions.4️⃣ The heart of this works by assigning this class as the delegate to the
CLLocationManagerinstance, which is set up at the tail end of initialization.5️⃣ The
CoreLocationAPI doesn’t immediately start sending information. There are methods that need to be called to start (and stop) the data flow, and these are wrapped and exposed on this proxy object. Most publishers are set up to subscribe and drive consumption based on subscription, so this is a bit out of the norm for how a publisher starts generating data.6️⃣ With the
delegatedefined and theCLLocationManageractivated, the data will be provided via callbacks defined on theCLLocationManagerDelegate. We implement the callbacks we want for this wrapped object, and within them we usepassthroughSubject.send()to forward the information to any existing subscribers.7️⃣ While not strictly required, the
delegateprovided anErrorreporting callback, so we included that as an example of forwarding an error throughpassthroughSubject.
UIKit-Combine/HeadingViewController.swift
1️⃣ One of the quirks of
CoreLocationis the requirement to ask for permission from the user to access the data. The API provided to initiate this request returns immediately, but provides no detail if the user allowed or denied the request. TheCLLocationManagerclass includes the information, and exposes it as a class method when you want to retrieve it, but there is no information provided to know when, or if, the user has responded to the request. Since the operation doesn’t provide any return, we provide an integer as the pipeline data, primarily to represent that the request has been made.2️⃣ Since there isn’t a clear way to judge when the user will grant permission, but the permission is persistent, we simply use a
delayoperator before attempting to retrieve the data. This use simply delays the propagation of the value for two seconds.3️⃣ After that delay, we invoke the class method and attempt to update information in the interface with the results of the current provided status.
4️⃣ Since
CoreLocationrequires methods to be explicitly enabled or disabled to provide the data, this connects aUISwitchtoggleIBActionto the methods exposed on our publisher proxy.5️⃣ The heading data is received in this
sinksubscriber, where in this example we write it to a text label.
Responding to updates from NotificationCenter
Goal:
Receiving notifications from
NotificationCenteras a publisher to declaratively react to the information provided.
A large number of frameworks and user interface components provide information about their state and interactions via Notifications from
NotificationCenter. Apple’s documentation includes an article on receiving and handling events with Combine specifically referencingNotificationCenter.
Notifications flowing through NotificationCenter provide a common, central location for events within your application.
You can also add your own notifications to your application, and upon sending them may include an additional dictionary in their userInfo property. An example of defining your own notification .myExampleNotification:
Notification names are structured, and based on Strings. Object references can be passed when a notification is posted to the NotificationCenter, indicating which object sent the notification. Additionally, Notifications may include a userInfo, which has a type of [AnyHashable : Any]?. This allows for arbitrary dictionaries, either reference or value typed, to be included with a notification.
When creating the NotificationCenter publisher, you provide the name of the notification for which you want to receive, and optionally an object reference to filter to specific types of objects. A number of AppKit components that are subclasses of NSControl share a set of notifications, and filtering can be critical to getting the right notification.
An example of subscribing to AppKit generated notifications:
1️⃣ TextFields within AppKit generate a
textDidChangeNotificationwhen the values are updated.2️⃣ An AppKit application can frequently have a large number of text fields that may be changed. Including a reference to the sending control can be used to filter to text changed notifications to which you are specifically interested in responding.
3️⃣ the
mapoperator can be used to get into the object references included with the notification, in this case the.stringValueproperty of the text field that sent the notification, providing its updated value4️⃣ The resulting string can be assigned using a writable
KeyValuepath.
An example of subscribing to your own notifications:
SwiftUI Integration: Using ObservableObject with SwiftUI models as a publisher source
Goal:
SwiftUI includes
@ObservedObjectand theObservableObjectprotocol, which provides a means of externalizing state for a SwiftUI view while alerting SwiftUI to the model changing.
The SwiftUI example code:
SwiftUI views are declarative structures that are rendered based on some known state, being invalidated and updated when that state changes. We can use Combine to provide reactive updates to manipulate this state and expose it back to SwiftUI. The example provided here is a simple form entry input, with the goal of providing reactive and dynamic feedback based on the inputs to two fields.
The following rules are encoded into Combine pipelines:
the two fields need to be identical - as in entering a password or email address and then validating it by a second entry.
the value entered is required to be a minimum of
5characters in length.A button to submit is enabled or disabled based on the results of these rules.
This is accomplished with SwiftUI by externalizing the state into properties on a class and referencing that class into the model using the ObservableObject protocol.
Two properties are directly represented:
firstEntryandsecondEntryasStringsusing the@Publishedproperty wrapper to allow SwiftUI to bind to their updates, as well as update them.A third property
submitAllowedis exposed as a Combine publisher to be used within the view, which maintains the@Stateinternally to the view.A fourth property - an array of
Strings calledvalidationMessages- is computed within the Combine pipelines from the first two properties, and also exposed to SwiftUI using the@Publishedproperty wrapper.
SwiftUI-Notes/ReactiveFormModel.swift
1️⃣ & 2️⃣ : omit...
3️⃣
combineLatestis used to merge updates from either offirstEntryorsecondEntryso that updates will be triggered from either source.4️⃣
maptakes the input values and uses them to determine and publish a list of validating messages. This overall flow is the source for two follow on pipelines.5️⃣ The first of the follow on pipelines uses the list of validation messages to determine a
trueorfalseBoolean publisher that is used to enable, or disable, the submit button.6️⃣ The second of the follow on pipelines takes the validation messages and updates them locally on this
ObservedObjectreference for SwiftUI to watch and use as it sees fit.
The two different methods of exposing state changes - as a publisher, or as external state, are presented as examples for how you can utilize either pattern. The submit button enable/disable choice could be exposed as a @Published property, and the validation messages could be exposed as a publisher of <String[], Never>. If the need involves tracking as explicit state, it is likely cleaner and less directly coupled by exposing @Published properties - but either mechanism can be used.
The model above is coupled to a SwiftUI View declaration that uses the externalized state.
SwiftUI-Notes/ReactiveForm.swift
1️⃣ The
modelis exposed to SwiftUI using@ObservedObject.2️⃣
@StatebuttonIsDisabledis declared locally to this view, with a default value oftrue.3️⃣ The projected value from the property wrapper (
$model.firstEntryand$model.secondEntry) are used to pass aBindingto theTextFieldview element. TheBindingwill trigger updates back on the reference model when the user changes a value, and will let SwiftUI’s components know that changes are about to happen if the exposed model is changing.4️⃣ The validation messages, which are generated and assigned within the
modelis invisible to SwiftUI here as a combine publisher pipeline. Instead this only reacts to the model changes being exposed by those values changing, irregardless of what mechanism changed them.5️⃣ As an example of how to use a published with
onReceive, anonReceivesubscriber is used to listen to a publisher which is exposed from the model reference. In this case, we take the value and store is locally as@Statewithin the SwiftUI view, but it could also be used after some transformation if that logic were more relevant to just the view display of the resulting values. In this case, we use it withdisabledonButtonto enabled SwiftUI to enable or disable that UI element based on the value stored in the@State.
Last updated