Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

15. In Practice: Combine & SwiftUI
Written by Marin Todorov

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

SwiftUI is Apple’s latest technology for building app UIs declaratively. It’s a big departure from the older UIKit and AppKit frameworks. It offers a very lean and easy to read and write syntax for building user interfaces.

Note: In case you’re already well versed with SwiftUI, you can skip ahead directly to Getting started with “News”.

The SwiftUI syntax clearly represents the view hierarchy you’d like to build:

HStack(spacing: 10) {
  Text("My photo")
  Image("myphoto.png")
    .padding(20)
    .resizable()
}

You can easily visually parse the hierarchy. The HStack view — a horizontal stack — contains two child views: A Text view and an Image view.

Each view can have a list of modifiers — which are methods you call on the view. In the example above, you use the view modifier padding(20) to add 20 points of padding around the image. Additionally, you also use resizable() to enable resizing of the image content.

SwiftUI also unifies the approach to building cross-platform UIs. For example, a Picker control displays a new modal view in your iOS app allowing the user to pick an item from a list, but on macOS the same Picker control displays a dropbox.

A quick code example of a data form could be something like this:

VStack {
  TextField("Name", text: $name)
  TextField("Proffesion", text: $profession)
  Picker("Type", selection: $type) {
    Text("Freelance")
    Text("Hourly")
    Text("Employee")
  }
}

This code will create two separate views on iOS. The Type picker control will be a button taking the user to a separate screen with a list of options like so:

On macOS, however, SwiftUI will consider the abundant UI screen space on the mac and create a single form with a drop-down menu instead:

Finally, in SwiftUI, the user interface rendered on screen is a function of your state. You maintain a single copy of this state referred to as the “source of truth”, and the UI is being derived dynamically from that state. Lucky for you, a Combine publisher can easily be plugged as a data source to SwiftUI views.

Hello, SwiftUI!

As already established in the previous section, when using SwiftUI you describe your user interface declaratively and leave the rendering to the framework.

Each of the views you declare for your UI — text labels, images, shapes, etc. — conform to the View protocol. The only requirement of View is a property called body.

Any time you change your data model, SwiftUI asks each of your views for their current body representation. This might be changing according to your latest data model changes. Then, the framework builds the view hierarchy to render on-screen by calculating only the views affected by changes in your model, resulting in a highly optimized and effective drawing mechanism.

In effect, SwiftUI makes UI “snapshots” triggered by any changes of your data model like so:

time SwiftUI data change data change data change

In this chapter, you will work through a number of tasks that cover both inter-operations between Combine and SwiftUI along with some of the SwiftUI basics.

Memory management

Believe it or not, a big part of what makes all of the above roll is a shift in how memory management works for your UI.

No data duplication

Let’s look at an example of what that means. When working with UIKit/AppKit you’d, in broad strokes, have your code separated between a data model, some kind of controller and a view:

Sepe Zihon Yiux Puxmnahniq Yaoy

Saovzoz zij rufpidoezh: Wnjiqc OOWetav ruy capw: Wzyobg?

Less need to “control” your views

As an additional bonus, removing the need for having “glue” code between your model and your view allows you to get rid of most of your view controller code as well!

Getting started with “News”

The starter project for this chapter already includes some code so that you can focus on writing code connecting Combine and SwiftUI.

A first taste of managing view state

Build and run the starter project and you will see an empty table on screen and a single bar button titled “Settings”:

self.presentingSettingsSheet = true

@State var presentingSettingsSheet = false
.sheet(isPresented: self.$presentingSettingsSheet, content: {
  SettingsView()
})

Fetching the latest stories

Next, time for you to go back to some Combine code. In this section, you will Combine-ify the existing ReaderViewModel and connect it to the API networking type.

import Combine
private var subscriptions = Set<AnyCancellable>()
func fetchStories() {

}
api
  .stories()
  .receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
  if case .failure(let error) = completion {
    self.error = error
  }
}, receiveValue: { stories in
  self.allStories = stories
  self.error = nil
})
.store(in: &subscriptions)
ReaderView(model: viewModel)
  .onAppear {
    viewModel.fetchStories()
  }
private var allStories = [Story]() {
  didSet {
    print(allStories.count)
  }
}
1
2
3
4
...

Using ObservableObject for model types

ObservableObject is a protocol that makes plain old data models observable and lets an observing SwiftUI View know the data has changed, so it’s able to rebuild any user interface that depends on this data.

class ReaderViewModel: ObservableObject {
@Published private var allStories = [Story]()
@Published var error: API.Error? = nil
@ObservedObject var model: ReaderViewModel

Displaying errors

You will also display errors in the same way you display the fetched stories. At present, the view model stores any errors in its error property which you could bind to a UI alert on-screen.

.alert(item: self.$model.error) { error in
  Alert(
    title: Text("Network error"), 
    message: Text(error.localizedDescription),
    dismissButton: .cancel()
  )
}

Subscribing to an external publisher

Sometimes you don’t want to go down the ObservableObject/ObservedObject route, because all you want to do is subscribe to a single publisher and receive its values in your SwiftUI view. For simpler situations like this, there is no need to create an extra type — you can simply use the onReceive(_) view modifier. It allows you to subscribe to a publisher directly from your view.

import Combine
private let timer = Timer.publish(every: 10, on: .main, in: .common)
  .autoconnect()
  .eraseToAnyPublisher()
.onReceive(timer) {
  self.currentDate = $0
}

@State var currentDate = Date()

Initializing the app’s settings

In this part of the chapter, you will move on to making the Settings view work. Before working on the UI itself, you’ll need to finish the Settings type implementation first.

import Combine
@Published var keywords = [FilterKeyword]()
final class Settings: ObservableObject {
let userSettings = Settings()
private var subscriptions = Set<AnyCancellable>()
init() {
  userSettings.$keywords
    .map { $0.map { $0.value } }
    .assign(to: \.filter, on: viewModel)
    .store(in: &subscriptions)
}
@Published var filter = [String]()
Kabhipfc Gaamoj TiesXavej Xendaxwb Joca

Editing the keywords list

In this last part of the chapter, you will look into the SwiftUI environment. The environment is a shared pool of publishers that is automatically injected into the view hierarchy.

System environment

The environment contains publishers injected by the system, like the current calendar, the layout direction, the locale, the current time zone and others. As you see, those are all values that could change over time. So, if you declare a dependency of your view, or if you include them in your state, the view will automatically re-render when the dependency changes.

@Environment(\.colorScheme) var colorScheme: ColorScheme
.foregroundColor(self.colorScheme == .light ? .blue : .orange)

Custom environment objects

As cool as observing the system settings via @Environment(_) is, that’s not all that the SwiftUI environment has to offer. You can, in fact, environment-ify your objects as well!

.environmentObject(userSettings)
@EnvironmentObject var settings: Settings
ForEach([FilterKeyword]()) { keyword in
ForEach(settings.keywords) { keyword in
presentingAddKeywordSheet = true
let new = FilterKeyword(value: newKeyword.lowercased())
self.settings.keywords.append(new)
self.presentingAddKeywordSheet = false
.onMove(perform: moveKeyword)
.onDelete(perform: deleteKeyword)
guard let source = source.first,
      destination != settings.keywords.endIndex else { return }

settings.keywords
  .swapAt(source,
          source > destination ? destination : destination - 1)
settings.keywords.remove(at: index.first!)

Challenges

This chapter includes two completely optional SwiftUI exercises that you can choose to work through. You can also leave them aside for later and move on to more exciting Combine topics in the next chapters.

Challenge 1: Displaying the filter in the reader view

In the first challenge, you will insert a list of the filter’s keywords in the story list header in ReaderView. Currently, the header always displays “Showing all stories”. Change that text to display the list of keywords in case the user has added any, like so:

Challenge 2: Persisting the filter between app launches

The starter project includes a helper type called JSONFile which offers two methods: loadValue(named:) and save(value:named:).

Key points

With SwiftUI, your UI is a function of your state. You cause your UI to render itself by committing changes to the data declared as the view’s state, among other view dependencies. You learned various ways to manage state in SwiftUI:

Where to go from here?

Congratulations on getting down and dirty with SwiftUI and Combine! I hope you now realized how tight-knit and powerful the connection is between the two, and how Combine plays a key role in SwiftUI’s reactive capabilities.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now