Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

Declarative syntax

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 of the views might have a number of parameters. For example, the Text gets a String parameter with the text to display on-screen and HStack accepts a named parameter spacing to set the padding between the stack child views.

Finally, each view can have a list of modifiers — which are simply 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. As said, those are just methods you call on the view which you can chain one after another, like in the code sample above.

Cross-platform

Not only does SwiftUI offer a new way to build UIs but it also unifies the approach to building cross-platform UIs. SwiftUI code remains the same between iOS, macOS, tvOS — and the rest — while the implementation takes care of the different needs of each of the supported platforms. 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 will display a dropbox.

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

New memory model

When using UIKit and AppKit, you need to constantly micromanage your data model and your views to keep them in sync. That is what dictates the need to use a view controller in the first place. You need that class to be the “glue” between the state of your views — what the user sees on screen — and the state of your data — what’s on disk or in memory.

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.

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:

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!

Experience with SwiftUI

Unfortunately, we can’t cover SwiftUI in detail in this chapter. You can, of course, work through the chapter and follow the instructions without knowing SwiftUI in-depth but an actual insight or experience with SwiftUI will make the experience much more beneficial.

Getting started with “News”

The starter project for this chapter includes some code so that you can focus on Combine and SwiftUI. That said, the actual UI layout has already been, well, laid out. The syntax layout itself is out of the scope of this chapter.

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)
if let windowScene = scene as? UIWindowScene {
  ...
}
viewModel.fetchStories()
private var allStories = [Story]() {
  didSet {
    print(allStories.count)
  }
}
1
2
3
4
...

Using ObservableObject for model types

Speaking of hooking up the model to the ReaderView, you will do exactly that in this section. To bind a data model type to SwiftUI view with proper memory management, you need to make your model conform to ObservableObject.

import SwiftUI
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, like you did with ReaderViewModel, because you can use a special view modifier called onReceive(_). This allows you to subscribe to a publisher directly in your view code.

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 {
import Combine
let userSettings = Settings()
private var subscriptions = Set<AnyCancellable>()
userSettings.$keywords
  .map { $0.map { $0.value } }
  .assign(to: \.filter, on: viewModel)
  .store(in: &subscriptions)
@Published var filter = [String]()

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!

let rootView = ReaderView(model: viewModel)
let rootView = ReaderView(model: viewModel)
  .environmentObject(userSettings)
@EnvironmentObject var settings: Settings
ForEach([FilterKeyword]()) { keyword in
ForEach(settings.keywords) { keyword in
presentingAddKeywordSheet = true

@EnvironmentObject var settings: Settings
.environmentObject(self.settings)
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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now