Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Second Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

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 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.

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.

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.

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:

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.

When using SwiftUI, on the other hand, you need to adopt a new approach towards building user interfaces. And let me put you at rest, that new approach is much better than what I just described above.

In SwiftUI, the user interface rendered on screen is a function of your data. You maintain a single copy of the data being called a “source of truth” and the UI is being derived dynamically from that single data source. This way, your UI is always up-to-date with the state of your app. Additionally, by using a higher abstraction for building your interface, you allow the framework to take care of a lot of the nitty-gritty implementation details across all supported operating systems.

Since you already have some solid experience with Combine, I’m sure your imagination is already running wild with ideas on how to plug your publishers into your app’s UI via SwiftUI.

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

With this new way to manage your UI, you need to stop thinking of how to update the user interface. Instead, you need to focus on which pieces of data are represented on-screen and effectively mutate them whenever you’d like SwiftUI to refresh your views.

In this chapter, you will work through a number of tasks that cover both interoperations 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:

Heyi Fosov Kees Rojqsurjob Muol

Zuujlir tej yaffavoifr: Lzhiqb UUMukur jod poqq: Wjjubc?

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]()
Cukruwdh Seoboc NiogPopar Yohremqd Hofe

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 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