I was looking into creating a view which has TextField with NumberFormatter. Typed text would show up in a separate label and when trying to enter non-numbers, the TextField would reject those characters. Although TextField component in SwiftUI has generic initialiser init(_:value:formatter:onEditingChanged:onCommit:) it does not seem to do what we need. Value binding does not update while typing, non-number characters are not discarded, and string is not reloaded when view reloads with different model data. Therefore, I decided to create a wrapper around TextField which deals with transforming numbers to strings and implements all the before mentioned features.
End result after creating a custom NumberTextField.
Content view with temperature limits
Example use-case is basic view for editing temperature limits where model type will force high value to be at least 10 units higher compared to low value. The model type also have separate properties for getting NSNumber instances what we use later (such conversion could also happen on SwiftUI level).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Model type storing temperature limits which are forced to have 10 unit difference.
Content view has text fields, button for randomising limits and label for displaying current values. NumberTextField is a custom view which implements all the features what we listed in the beginning of the post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
View with custom NumberTextFields bound to temperature limits.
Creating NumberTextField with NSNumber binding and NumberFormatter
NumberTextField is a wrapper around TextField and internally handles NSNumber to String and String to NSNumber transformations. Transformations happen inside a separate class called StringTransformer which stores editable string in @Published property. @Published property is first populated with string value by transforming NSNumber to String using the formatter. Changes made by user are captured by subscribing to stringValue publisher (@Published properties provide publishers). String to NSNumber transformation is tried when user edits the string: if successful, NSNumber is send back to model using the value binding, if fails, stringValue is set back to previous value. Note that dropFirst skips initial update when setting up sink and receive operator is used for scheduling updates at later time when SwiftUI has finished current layout update cycle.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
NumberTextField transforming NSNumber to String using NumberFormatter and vice versa.
Summary
TextField’s formatter initialiser does not seem to be operating as expected and therefore we built a custom view. It handles number to string transformations and refreshes the view when string can’t be transformed to number. Hopefully future SwiftUI iterations will fix the init(_:value:formatter:onEditingChanged:onCommit:) initialiser and NumberTextField is not needed at all.
View models in MVVM are responsible of making the model suitable for presenting by a view. Quite often model objects themselves have properties which can be directly presented by the view. SE-0195 added dynamic member lookup types. Using @dynamicMemberLookup we can add all the model object’s properties to the view model itself. Result, model.name is also accessible by calling name on the view model.
View model with @dynamicMemberLookup
Example app presents chemical element: symbol, name, atomic number, and atomic weight. Model object’s properties which can be displayed without transformations are symbol and name. Atomic number and weight are transformed and label is prepended to a value.
View presenting properties of a chemical element.
struct ChemicalElement {
let name: String // Hydrogen
let symbol: String // H
let atomicNumber: Int // 1
let atomicWeight: Double // 1.008
}
View model is initialised with an instance of chemical element. What we would like to have is accessing all the data by asking it from view model: viewModel.name, viewModel.symbol, viewModel.numberDescription, viewModel.weightDescription. Dynamic member lookup enables us adding all the model object’s properties to the view model with only some lines of code. It makes a real difference if the model and view model have a way more data than in the current example.
struct ContentView: View {
let viewModel: ContentViewModel
var body: some View {
VStack(spacing: 4) {
Text(viewModel.symbol).font(.system(size: 42)).fontWeight(.bold)
VStack(spacing: 4) {
Text(viewModel.name)
Group {
Text(viewModel.numberDescription)
Text(viewModel.weightDescription)
}.font(.footnote)
}
}
}
}
Let’s take a look on the view model. When view model is annotated with @dynamicMemerLookup, we’ll just need to implement one method. This is what is used to passing through model object’s data directly to the view. That is all we need to do for exposing model object’s properties on the view model level.
@dynamicMemberLookup
struct ContentViewModel {
private let chemicalElement: ChemicalElement
init(chemicalElement: ChemicalElement) {
self.chemicalElement = element
}
subscript<T>(dynamicMember keyPath: KeyPath<ChemicalElement, T>) -> T {
return chemicalElement[keyPath: keyPath]
}
var numberDescription: String {
return "Number: \(chemicalElement.atomicNumber)"
}
var weightDescription: String {
return "Weight: \(chemicalElement.atomicWeight)"
}
}
Summary
Dynamic member lookup in Swift is useful addition to MVVM pattern when there is need to expose model’s properties directly to the view. When working with models with many properties, it is very useful.
Everything can’t go exactly as planned and therefore, at some point, there is a need for presenting localized error messages to the user. Let’s take a look at how to add custom error type what provides error description, failure reason and recovery suggestion and presenting it in SwiftUI view.
Adding custom error type
Custom error type is needed when we want to propagate errors using Swift’s error handling mechanism. Custom error types need to, at minimum, conform to Error protocol which defines localizedDescription property. If we would like to provide more information to users, including recovery suggestions, then we need to use LocalizedError instead. LocalizedError inherits from Error and defines additional properties which are intended for describing the error further. Note that LocalizedError is very similar to NSError: errorDescription, failureReason, recoverySuggestion, helpAnchor are all represented by NSError.UserInfoKey.
In the example app we’ll use LoginError currently definiing only one error: incorrectPassword.
enum LoginError: LocalizedError {
case incorrectPassword // invalidUserName etc
var errorDescription: String? {
switch self {
case .incorrectPassword:
return "Failed logging in account"
}
}
var failureReason: String? {
switch self {
case .incorrectPassword:
return "Entered password was incorrect"
}
}
var recoverySuggestion: String? {
switch self {
case .incorrectPassword:
return "Please try again with different password"
}
}
}
Presenting error in SwiftUI
Custom error defined, the next step is to present the error using SwiftUI’s alert view modifier: alert(isPresented:content:). Alert view modifier requires boolean binding and Alert container defining title, optional message, and buttons. In the example below, error is handled by the view model and Alert itself is created using convenience initializer which we’ll look at a bit later. Convenience initializer makes the view implementation more readable and reduces code duplication.
Alert view modifier requires a boolean binding controlling if the alert is visible or not. When alert is dismissed, SwiftUI automatically calls the binding with false, indicating that the alert should not be visible anymore. Note that force unwrap is safe here because view model makes sure isPresentingAlert never returns true when underlying error is nil.
final class ContentViewModel: ObservableObject {
@Published private(set) var activeError: LocalizedError?
var isPresentingAlert: Binding<Bool> {
return Binding<Bool>(get: {
return self.activeError != nil
}, set: { newValue in
guard !newValue else { return }
self.activeError = nil
})
}
func showAlertView() {
activeError = LoginError.incorrectPassword
}
}
Boolean binding is created manually and implemented in such way that when activeError is set, isPresentingAlert returns true. When alert is dismissed, set will clear the current active error. This approach makes it simple to handle any errors conforming to LocalizedError in the view model. Like mentioned before, LocalizedError enables us to add detailed information about the alert and we can use that when creating the Alert. Let’s take a look on it next.
Alert’s extension has initializers both for LocalizedError and NSError. NSError is used a lot in Objective-C frameworks so there is high probability that we need to present NSError in the future as well. Here, we can use Swift language’s built-in support of converting Swift error type to NSError and therefore we can implement convenience method only once for NSError. LocalizedError can be bridged to NSError and Swift compiler takes care of keeping the information about the error. In this implementation, I decided to include both the failureReason and recoverySuggestion when creating the message for Alert. This enables custom error types to choose how much information they provide (choosing which properties return text). Moreover, it is better to show as much information about the error as possible.
LoginError.incorrectPassword presented by Alert in SwiftUI
Summary
We created a custom error type and used LocalizedError instead of Error for making it suitable for displaying as an alert. We looked into how to use alert view modifier and MVVM together and introduced design pattern for easy alert presentation. If you need action sheet, then follow similar steps but use actionSheet view modifier with ActionSheet container.
Apps which integrate push or local notifications can customise notifications on Apple Watch. Let’s go through steps required for adding dynamic notifications on Apple Watch. Sample use case is an app which reminds when a plant needs watering. We’ll only concentrate on adding dynamic notification view and leave out sending local notifications from the iOS app.
Adding build target for rich notifications on Apple Watch
If Apple Watch app does not exist in the project the first step is to add it. In Xcode, we’ll add a new build target and configure it to include notification scene. In Xcode, open new target view: File>Target and select Watch App for iOS App template.
Watch App for iOS App template in File>Target.
Make sure SwiftUI is selected in the user interface selection and “Include Notification Scene” is also selected. We’ll embed it in the companion app so make sure current iOS app target is set to “Embed in Companion App”. As a side note, since iOS 13 and WatchOS 6, Apple Watch apps can be independent as well.
Watch App for iOS app build target configuration with notification scene.
Click on Finish and then Xcode will ask about activating the new scheme, click on active. It will just select the new target and we can build it right away. When inspecting the project, Xcode added two targets: watch app and extension. App contains storyboard and extension contains all the code. Storyboard is wired up so that the scene displays HostingController which is WKHostingController subclass and is responsible of hosting your SwiftUI view in the Apple Watch app. In addition, there are scenes for static and dynamic notifications. We are interested in creating dynamic notifications and in the Storyboard we can see that the dynamic view is provided by NotificationController (subclass of WKUserNotificationHostingController) which hosts SwiftUI view for the notification. There we can provide the custom interface for our user notification. Dynamic notification view is selected if the notification category matches with the one defined in the Storyboard.
Parsing notification payload and setting up dynamic notification view
NotificationController’s responsibility is to consume user notification’s payload and configuring the SwiftUI view with it. User notification is provided by didReceive function and there we can extract the information needed for showing the view. When it comes to locally testing the dynamic view, we can add required data to the PushNotificationPayload.apns file. As we show information about plants, let’s add example plant object to the file. Also, we change category to something meaningful. Make sure to update Storyboard when setting new category.
Notification category in Storyboard for defining the connection to dynamic notification view.
Plant related information is available when accessing UNNotification’s request.content.userInfo. We can use Decodable and JSONDecoder for converting Dictionary representing the plant into value type. As JSONDecoder requires JSON data then we can first use JSONSerialization and then passing the JSON data to JSONDecoder. Alternatively we could also manually read values from the userInfo dictionary and then creating the value type. Note that we use view model for providing data to SwiftUI view and not the Plant type directly.
struct Plant: Decodable {
let id: String
let name: String
let lastDate: Date
let nextDate: Date
}
do {
let plantInfo = notification.request.content.userInfo["plant"] as! [String: Any]
let data = try JSONSerialization.data(withJSONObject: plantInfo, options: [])
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let plant = try decoder.decode(Plant.self, from: data)
viewModel = NotificationViewModel(plant: plant)
}
catch let nsError as NSError {
print(nsError.localizedDescription)
}
In addition, we would like to add 3 actions user can take: marking the plant as watered, deferring the reminder for couple of hours, or scheduling it for tomorrow. Actions are represented with instances of UNNotificationAction and when user taps on any of those, UNUserNotificationCenter’s delegate method is called with the identifier in the companion iOS app (userNotificationCenter(_:didReceive:withCompletionHandler:)).
let doneTitle = NSLocalizedString("NotificationAction_Done", comment: "Done button title in notification.")
let laterTitle = NSLocalizedString("NotificationAction_Later", comment: "Later button title in notification.")
let tomorrowTitle = NSLocalizedString("NotificationAction_Tomorrow", comment: "Tomorrow button title in notification.")
notificationActions = [
UNNotificationAction(identifier: "water_done", title: doneTitle, options: []),
UNNotificationAction(identifier: "water_later", title: laterTitle, options: []),
UNNotificationAction(identifier: "water_tomorrow", title: tomorrowTitle, options: [])
]
The full implementation of the NotificationController becomes like this (including the creation of SwiftUI):
final class NotificationController: WKUserNotificationHostingController<NotificationView> {
private var viewModel: NotificationViewModel?
override var body: NotificationView {
return NotificationView(viewModel: viewModel!)
}
override func didReceive(_ notification: UNNotification) {
do {
let plantInfo = notification.request.content.userInfo["plant"] as! [String: Any]
let data = try JSONSerialization.data(withJSONObject: plantInfo, options: [])
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let plant = try decoder.decode(Plant.self, from: data)
viewModel = NotificationViewModel(plant: plant)
}
catch let nsError as NSError {
print(nsError.localizedDescription)
}
let doneTitle = NSLocalizedString("NotificationAction_Done", comment: "Done button title in notification.")
let laterTitle = NSLocalizedString("NotificationAction_Later", comment: "Later button title in notification.")
let tomorrowTitle = NSLocalizedString("NotificationAction_Tomorrow", comment: "Tomorrow button title in notification.")
notificationActions = [
UNNotificationAction(identifier: "water_done", title: doneTitle, options: []),
UNNotificationAction(identifier: "water_later", title: laterTitle, options: []),
UNNotificationAction(identifier: "water_tomorrow", title: tomorrowTitle, options: [])
]
}
}
NotificationView presenting the dynamic notification
Previously mentioned view model NotificationViewModel provides text for NotificationView and it looks pretty simple, mainly dealing with creating strings with formatted dates. As we only want to show day and month then we need to create dateFormat using current locale.
struct NotificationViewModel {
private let plant: Plant
init(plant: Plant) {
self.plant = plant
}
var title: String {
return plant.name
}
var subtitle: String {
return NSLocalizedString("NotificationView_Subtitle", comment: "Notification suggestion text")
}
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "dMMMM", options: 0, locale: .current)
return formatter
}()
var lastWatering: String {
let format = NSLocalizedString("NotificationView_LastWatering", comment: "Last watering date.")
return String(format: format, dateFormatter.string(from: plant.lastDate))
}
var nextWatering: String {
let format = NSLocalizedString("NotificationView_NextWatering", comment: "Next watering date.")
return String(format: format, dateFormatter.string(from: plant.nextDate))
}
}
SwiftUI view is also pretty simple with containing 4 text labels and one divider.
struct NotificationView: View {
let viewModel: NotificationViewModel
var body: some View {
VStack {
Text(viewModel.title).font(.title)
Text(viewModel.subtitle).font(.subheadline)
Divider()
Text(viewModel.lastWatering).font(.body).multilineTextAlignment(.center)
Text(viewModel.nextWatering).font(.body).multilineTextAlignment(.center)
}
}
}
Dynamic notification with 4 labels and divider.
Buttons in dynamic notification.
Summary
We added Apple Watch app to an existing iOS app and implemented dynamic notification view for one notification category. We looked into how to parse data associated with the user notification, create SwiftUI view and add action buttons. Next steps would be to handle notification actions in the companion iOS app based on the notification button identifier.
CoreData is Apple’s object graph and persistence framework. It provides data sources for synchronising data with view. Let’s take a look on how to use those data sources in SwiftUI views. Starting with NSFetchedResultsController what is used for list and collection views, after that observing NSManagedObject directly from SwiftUI view and lastly subscribing to managed object context notifications.
NSFetchedResultsController
NSFetchedResultsController is used for providing data in table and collection views. It supports sorting and filtering data and arranging data into sections. We can use delegate for getting change callbacks. NSFetchedResultsControllerDelegate contains several methods:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In SwiftUI we are not going to directly manipulate views. Only what we need to do is letting SwiftUI view know that data is about to change. Let’s take a look on simple app with list of items stored by CoreData. Data is represented by ColorItem where only stored value is hex string of the color (e.g. #AA22BB).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ColorItems are managed by ContentView’s view model. View model creates NSFetchedResultsController, performs fetch and provides array of fetched ColorItems to the SwiftUI’s List. In addition, view model is delegate of the NSFetchedResultsController (requires view model to be NSObject subclass). As view model is ObservableObject, we can very easily let SwiftUI view know that it should refresh. We need to do two things: firstly, implementing controllerWillChangeContent delegate method and calling send() on objectWillChange publisher. Secondly, view model property must use @ObservedObject property wrapper in SwiftUI view. Result is that SwiftUI view subscribes to objectWillChange publisher and refreshes whenever publisher emits an event.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
And finally let’s see the ContentView implementation. NSManagedObject has objectID property what we can use in List for identifying every ColorItem. Cell is custom view what we’ll take a look at next.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
NSManagedObject implements ObservableObject protocol and therefore it is possible to use it together with @ObservedObject property wrapper and getting SwiftUI view refreshed automatically when any of the ColorItem properties change. NSFetchedResultsController required a little bit of code for setting up delegate but that is not the case with NSManagedObject.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In addition, if there are cases where we would like to observe specific property, then Combine provides publisher for key path. Because NSManagedObject supports key-value observing we can use the publisher and subscribe to individual property changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
When we need to observe changes in persistent store we can observe notifications sent by the framework. As NotificationCenter supports publishers, we can subscribe to it, unpack data from notification and do something with the data. For making this easier we can introduce a separate type. It will unpack the user info dictionary and filter by type. This allows to easily observe, for example, ColorItem insertions. Or, if we would like to receive every possible change, we can specify NSManagedObject as the generic type.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
We took a look at how to use NSFetchedResultsController and NSManagedObject in SwiftUI views. We saw that integrating NSFetchedResultsController requires only a little bit of code and using NSManagedObject even less. In addition, we looked at subscribing to CoreData notifications and unpacking notification payload.
Let’s build a simple app using MVVM (model-view-view model) where every SwiftUI view has its own view model. It’s going to be an app with two views: list of movies and add a movie view what utilises Form view. Added movies are stored in MovieStore which is shared by the two view models. We will use environment for sharing the MovieStore. It will be read from the environment when we need to create AddMovieView with its view model.
Movie and MovieStore representing data
Movie is a small struct and just stores the title and rating. Title and rating are mutable as we are going to update those in AddMovieView. We also conform to protocol Identifiable because we are going to use List view for showing all the movies. List needs a way of identifiyng the content and its the simplest way of satisfiying the requirement.
struct Movie: Equatable, Identifiable {
let id = UUID()
var fullTitle: String
var givenRating: Rating = .notSeen
}
extension Movie {
enum Rating: Int, CaseIterable {
case notSeen, terrible, poor, decent, good, excellent
}
}
MovieStore is also a pretty simple although in a more sophisticated app it would contain much more logic: persistence, deleting etc. We use Published property wrapper which automatically provides a publisher we can use to subscribe against.
final class MovieStore {
@Published private(set) var allMovies = [Movie]()
func add(_ movie: Movie) {
allMovies.append(movie)
}
}
For inserting shared MovieStore to environment, we’ll use custom EnvironmentKey. Custom key is just an object conforming to EnvironmentKey protocol. We need to provide the type and default value.
struct MovieStoreKey: EnvironmentKey {
typealias Value = MovieStore
static var defaultValue = MovieStore()
}
extension EnvironmentValues {
var movieStore: MovieStore {
get {
return self[MovieStoreKey]
}
set {
self[MovieStoreKey] = newValue
}
}
}
If we do not insert our own instance of MovieStore to the environment, the instance returned by defaultValue is used. Typically we would like to use a specific instance initialised outside of the view hierarchy. Therefore let’s take a look how to do that next.
SceneDelegate and MovieScene presentation
MovieStore dependency is passed into view models with initialiser. We’ll use the instance stored in SceneDelegate. Yet again, in a real app, it would probably live in a separate dependency container or in something similar. MovieListView is the first view we need to present, therefore we’ll initialise view model, view and insert instance of MovieStore to environment for later use (movieStore keypath is the one we just defined in EnvironmentValues‘ extension).
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private let movieStore = MovieStore()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let viewModel = MovieListView.ViewModel(movieStore: movieStore)
let contentView = MovieListView(viewModel: viewModel).environment(\.movieStore, movieStore)
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
MovieListView and its ViewModel
We still haven’t taken a look on MovieListView and its view model, let’s do it now. View model conforms to protocol ObservableObject and uses @Published property wrappers. ObservableObject’s default implementation provides objectWillChange publisher. @Published property wrapper automatically fires the publisher when the property value is about to change. On MovieListView we have declared view model property with @ObservedObject property wrapper. This will make the view to subscribe to objectWillChange publisher and will refresh the view when-ever objectWillChange fires.
extension MovieListView {
final class ViewModel: ObservableObject {
private let movieStore: MovieStore
private var cancellables = [AnyCancellable]()
init(movieStore: MovieStore) {
self.movieStore = movieStore
cancellables.append(movieStore.$allMovies.assign(to: \.movies, on: self))
}
@Published private(set) var movies = [Movie]()
@Published var isPresentingAddMovie = false
}
}
struct MovieListView: View {
@Environment(\.self) var environment
@ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
List(self.viewModel.movies) { movie in
Text(movie.fullTitle)
}.navigationBarTitle("Movies")
.navigationBarItems(trailing: navigationBarTrailingItem)
}
}
private var navigationBarTrailingItem: some View {
Button(action: {
self.viewModel.isPresentingAddMovie = true
}, label: {
Image(systemName: "plus").frame(minWidth: 32, minHeight: 32)
}).sheet(isPresented: self.$viewModel.isPresentingAddMovie) {
self.makeAddMovieView()
}
}
private func makeAddMovieView() -> AddMovieView {
let movieStore = environment[MovieStoreKey]
let viewModel = AddMovieView.ViewModel(movieStore: movieStore)
return AddMovieView(viewModel: viewModel)
}
}
Changes in MovieStore are observed by subscribing to allMovies subscriber and then assigning the new list of movies to view model’s own property. Note that assignment is triggered on subscribing and when changes happen: like KVO with initial option. Only downside is that now the list is duplicated but that’s OK. We would need to do that anyway when we would like to sort or filter the list later on.
AddMovieView and its view model are created when user taps on the plus button in the navigation bar. Environment property wrapper can be used to get the whole environment or any of the values using a specific key. In current case I went for accessing the whole environment object and then getting MovieStore using a MovieStoreKey later when needed. Then the MovieStore is not available in the whole view scope and only when creating the AddMovieView. Other option would be to use @Environment(\.movieStore) var movieStore instead.
AddMovieView and its ViewModel
AddMovieView’s view model is initialised with MovieStore and internally it represents and instance of Movie. Published property wrapper is used similarly like in MovieListView’s view model. The model object is a private property and instead of direct access, two bindings are provded for TextField and Picker. Binding represents a two way connection between the view and model. In addition, there is canSave property what is used for enabling the save button in the navigation bar. Save button should be enabled only when title is filled. To recap the view update flow: TextField or Picker will use Binding to update private property newMovie. As newMovie property uses @Published property wrapper, it will fire ObservableObject’s objectWillChange publisher. SwiftUI automatically subscribes to objectWillChange because view model’s property uses @ObservedObject.
extension AddMovieView {
class ViewModel: ObservableObject {
private let movieStore: MovieStore
init(movieStore: MovieStore) {
self.movieStore = movieStore
}
@Published private var newMovie = Movie(fullTitle: "")
lazy var title = Binding<String>(get: {
self.newMovie.fullTitle
}, set: {
self.newMovie.fullTitle = $0
})
lazy var rating = Binding<Movie.Rating>(get: {
self.newMovie.givenRating
}, set: {
self.newMovie.givenRating = $0
})
var canSave: Bool {
return !newMovie.fullTitle.isEmpty
}
func save() {
movieStore.add(newMovie)
}
}
}
struct AddMovieView: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
Form {
titleSection
ratingSection
}.navigationBarTitle("Add Movie", displayMode: .inline)
.navigationBarItems(leading: leadingBarItem, trailing: trailingBarItem)
.navigationViewStyle(StackNavigationViewStyle())
}
}
private var titleSection: some View {
Section() {
TextField("Title", text: viewModel.title)
}
}
private var ratingSection: some View {
Section() {
Picker(LocalizedStringKey("Rating"), selection: viewModel.rating) {
ForEach(Movie.Rating.allCases, id: \.rawValue) {
Text($0.localizedName).tag($0)
}
}
}
}
private var leadingBarItem: some View {
Button(action: { self.presentationMode.wrappedValue.dismiss() }, label: {
Text("Cancel")
})
}
private var trailingBarItem: some View {
Button(action: {
self.viewModel.save()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save").disabled(!self.viewModel.canSave)
})
}
}
Summary
We created a simple app with two views. Both views had its own view model and both view models used the same dependency: MovieStore. One view model triggered changes in MovieStore and those changes were observed by the other view model. In addition, we looked into how to use SwiftUI’s environment and how to trigger view updates from view models.
One building block for navigating from one view to another is NavigationView which is a representation of UINavigationController in UIKit. This time, let’s take a look on how to transition from one SwiftUI view to another one without NavigationView.
AppFlowCoordinator managing choosing the view
The idea is to have a root SwiftUI view with only responsibility of presenting the active view. State is stored in AppFlowCoordinator which can be accessed from other views and therefore other views can trigger navigation. Example case we’ll build, is animating transitions from login view to main view and back. As said, AppFlowCoordinator stores the information about which view should be on-screen at a given moment. All the views are represented with an enum and based on the value in enum, views are created. This coordinator is ObservableObject what makes it easy to bind to a SwiftUI view – whenever activeFlow changes, SwiftUI view is updated. The term flow is used because views can consist of stack of other views and therefore creating a flow of views.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
RootView selects which view is currently visible. It accesses coordinator through environment. SwiftUI requires EnvironmentObjects to be ObservableObjects, therefore this view is automatically refreshed when activeFlow changes in the AppFlowCoordinator. RootView’s body is annotated with @ViewBuilder which will enable the view to return a body with type depending on the current state (HStack is also a ViewBuilder). Other options are wrapping the views with AnyView or using Group. In our case the view types are LoginView and ContentView. Both views also define the transition animation what is used when view refresh is triggered in withAnimation closure in AppFlowCoordinator. Asymmetric enables defining different transitions when view is added and removed from the view hierarchy.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Updating currently visible flow with transition animations
Triggering navigation from SwiftUI view
Last piece we need to take a look at is how to trigger transition. As AppFlowCoordinator is in environment, any view can access the coordinator and call any of the navigation methods. When login finishes, LoginView can tell the coordinator to show the main content view.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
We took a look on how to navigate from one SwiftUI view to another by using a coordinator object. Coordinator stored the information about which view we should currently display on screen. We saw how easy it is to trigger navigation from any of the currently visible views.
Instead of initializing SwiftUI views with dependencies, SwiftUI also offers other ways for injecting dependencies. This time let’s take a look on EnvironmentKey which defines a key for inserting objects to environment and vise-versa. We need to create a EnvironmentKey, adding an object to environment and then getting the object in SwiftUI view.
Creating EnvironmentKey and extending EnvironmentValues
Example object we use is DependencyManager what in real app can contains loads of dependencies. EnvironmentKey is a protocol in SwiftUI what requires to define associated type and default value. Default value is used when we have not explicitly inserted an instance of DependencyManager to the environment, more about it a little bit later. EnvironmentValues is a struct containing a collection of environment objects. We’ll add a property to EnvironmentValues which later will be used by @Environment property wrapper and also when setting an instance of the object to the environment.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Custom EnvironmentKey and EnvironmentValues extension for accessing dependencies
Inserting objects to environment
If we would like to insert a DependencyManager to the Environment, we can use func environment(_ keyPath: WritableKeyPath, _ value: V) -> some View using our DependencyManagerKey and an instance created by us. If we do not insert our own, SwiftUI will use the instance returned by defaultValue when the key is first time accessed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Setting instance of DependencyManager to SwiftUI environment
Getting objects from environment
Objects can be read from the environment using @Environment property wrapper and specifing the EnvironmentKey.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Accessing the instance of DependencyManager in environment
Summary
We created an environment key and inserted an object into environment. We looked into how SwiftUI handles default values for environment objects and used @Environment property wrapper for getting the instance from the environment using the created key.
This year Apple added CGAnimateImageAtURLWithBlock and CGAnimateImageDataWithBlock for animating GIFs and APNGs on all the platforms to the ImageIO framework. We can pass in URL or data and get callbacks when animation changes the current frame. In Xcode 11 beta 7 implicit bridging to Swift is disabled for those APIs and therefore we need to create a small wrapper around it in Objective-C.
Creating ImageFrameScheduler for managing CGAnimateImageAtURLWithBlock in Objective-C
Calling CGAnimateImageAtURLWithBlock starts the animation immediately. When animation frame changes, the handler block is called with frame index, current animation frame image and stop argument. When setting stop to YES, we can stop the animation. With this in mind we can create ImageFrameScheduler what takes in URL and has methods for starting and stopping the animation. Then we can expose this class to Swift and use it for managing the animation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ImageAnimator conforming to ObservableObject in Swift
When updating views in SwiftUI, we can use ObservableObject protocol and @Published property wrapper what enables SwiftUI to get notified when the ObservableObject changes. This means that we need a model object written in Swift what stores our Objective-C class ImageFrameScheduler and exposes the current animation frame when animation is running. Whenever we update the property internally, property wrapper will take care of notifying SwiftUI to update the view.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ContentView displaying animation frames in SwiftUI
Integrating ImageAnimator with ContentView is now pretty straight-forward, we check if animation frame image is available and display it. Animation is started when SwiftUI appears and stopped when it disappears.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Although CGAnimateImageAtURLWithBlock and CGAnimateImageDataWithBlock are not directly usable in Swift, we can get away from it by adding a simple wrapper class in Objective-C. ImageFrameScheduler could be used in non-SwiftUI views by updating UIImageView when frame changes. In SwiftUI, views can use ImageAnimator for storing the current animation frame and using @Published property wrapper for letting SwiftUI view to know when to refresh.
Apple’s Vision framework contains computer vision related functionality and with iOS 13 it can detect text on images as well. Moreover, Apple added a new framework VisionKit what makes it easy to integrate document scanning functionality. For demonstrating the usage of it, let’s build a simple UI what can present the scanner and display scanned text.
Cropping text area when scanning documents
Scanning text with Vision
VisionKit has VNDocumentCameraViewController and when presented, it allows scanning documents and cropping scanned documents. It uses delegate for publishing scanned documents via an instance of VNDocumentCameraScan. This object contains all the taken images (documents). Next, we can use VNImageRequestHandler in Vision for detecting text on those images.
final class TextRecognizer {
let cameraScan: VNDocumentCameraScan
init(cameraScan: VNDocumentCameraScan) {
self.cameraScan = cameraScan
}
private let queue = DispatchQueue(label: "com.augmentedcode.scan", qos: .default, attributes: [], autoreleaseFrequency: .workItem)
func recognizeText(withCompletionHandler completionHandler: @escaping ([String]) -> Void) {
queue.async {
let images = (0..<self.cameraScan.pageCount).compactMap({ self.cameraScan.imageOfPage(at: $0).cgImage })
let imagesAndRequests = images.map({ (image: $0, request: VNRecognizeTextRequest()) })
let textPerPage = imagesAndRequests.map { image, request -> String in
let handler = VNImageRequestHandler(cgImage: image, options: [:])
do {
try handler.perform([request])
guard let observations = request.results as? [VNRecognizedTextObservation] else { return "" }
return observations.compactMap({ $0.topCandidates(1).first?.string }).joined(separator: "\n")
}
catch {
print(error)
return ""
}
}
DispatchQueue.main.async {
completionHandler(textPerPage)
}
}
}
}
Presenting document scanner with SwiftUI
As VNDocumentCameraViewController is UIKit view controller we can’t directly present it in SwiftUI. For making this work, we’ll need to use a separate value type conforming to UIViewControllerRepresentable protocol. UIViewControllerRepresentable is the glue between SwiftUI and UIKit and enables us to present UIKit views. This protocol requires us to define the class of the view controller and then implementing makeUIViewController(context:) and updateUIViewController(_:context:). In addition, we’ll also create coordinator what is going to be VNDocumentCameraViewController’s delegate. SwiftUI uses UIViewRepresentableContext for holding onto the coordinator and managing the view controller updates behind the scenes. Our case is pretty simple, we just use completion handler for passing back scanned text or nil when it was closed or error occurred. No need to update the view controller itself, only to pass data from it back to SwiftUI.
ContentView is the main SwiftUI view presenting the content for our very simple UI with static text, button and scanned text. When pressing on the button, we’ll set isShowingScannerSheet property to true. As it is @State property, then this change triggers SwiftUI update and sheet modifier will take care of presenting ScannerView with VNDocumentCameraViewController. When view controller finishes, completion handler is called and we will update the text property and set isShowingScannerSheet to false which triggers tearing down the modal during the next update.
struct ContentView: View {
private let buttonInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
var body: some View {
VStack(spacing: 32) {
Text("Vision Kit Example")
Button(action: openCamera) {
Text("Scan").foregroundColor(.white)
}.padding(buttonInsets)
.background(Color.blue)
.cornerRadius(3.0)
Text(text).lineLimit(nil)
}.sheet(isPresented: self.$isShowingScannerSheet) { self.makeScannerView() }
}
@State private var isShowingScannerSheet = false
@State private var text: String = ""
private func openCamera() {
isShowingScannerSheet = true
}
private func makeScannerView() -> ScannerView {
ScannerView(completion: { textPerPage in
if let text = textPerPage?.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) {
self.text = text
}
self.isShowingScannerSheet = false
})
}
}
Summary
With the new addition of VisionKit and text recognition APIs, it is extremely easy to add support of scanning text using camera.