Xcode 12 comes with Swift toolchain 5.3 which brings resource and localisation support to Swift packages. Nice thing is that Swift package support only depends on the toolchain’s version and does not have additional OS requirements. At the same time, let’s keep in mind that OS requirements come from the code we actually add to the package. This means that it is a good time to start using Swift packages for separating code into separate libraries and stop using separate framework projects in a workspace. Additional benefit is that, if needed, it is pretty easy to move the package out of the workspace and creating a sharable package what can be publish and reused in other projects. But for now, let’s take a look on how to set up a new workspace with an app project and a Swift package which represents a design library with custom button style.
File structure of the workspace
The app project will have a name “ButtonGallery” and the Swift package will have a name “ButtonKit”. But first, let’s create a folder named “SwiftPackageAppWorkspace” which is the root folder of the project. The app project and the Swift package will go to that folder in separate folders.
Adding the workspace, the app project, and the Swift package
New workspace can be created by selecting the Workspace menu item in File>New menu in the Xcode. Save the workspace in the “SwiftPackageAppWorkspace” folder what we created just before. Xcode opens the created workspace after clicking on save and then the next step is to add a new Swift package. Easiest is to use the plus button at the bottom of the left corner, selecting “New Swift Package”, and saving the package in the “SwiftPackageAppWorkspace” folder. Uncheck the option on the save panel for creating a git repository because the git repository should be added in the “SwiftPackageAppWorkspace” instead (we skipped this step). Third step is to add the app project by using File>New menu. Xcode also offers an option to add the new project to the workspace. Therefore make sure “Add to” and “Group” have the workspace selected on the save panel. Described steps are shown below.
Selecting a new workspace in the main menu.Using the plus button in the workspace for creating a new package.Saving Swift package in the root folder.Adding a new Xcode project.Selecting template for the project.Adding a name to the app project.Saving a new app project and adding to an existing workspace.Workspace with a Swift package and an app project.
Linking the Swift package in the app project
Swift package needs to be added to the app target: select “ButtonGallery” in the project navigator, click on the iOS target, General, and then on the plus button in the “Frameworks, Libraries, and Embedded Content” section, select the “ButtonKit” library.
Navigating to iOS target’s general settings.Linking with the ButtonKit.
Now the workspace is configured but there is not any useful code in the “ButtonKit”. Let’s fix that next and add a FunkyButtonStyle.swift to the package and set minimum platforms in the package manifest because we’ll use SwiftUI in the implementation and it has minimum platform requirements. Because FunkyButtonStyle is in a separate module and by default access control is set to internal, then we’ll need to make it public before it can be imported to the app target.
This file contains hidden or 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 hidden or 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 hidden or 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 in the app target which imports ButtonKit and uses its FunkyButtonStyle.
Summary
We created a new workspace what contains a Swift package and an app project. We looked into how to provide functionality in the package and making it available for the main app target.
Picker is used for presenting a selection consisting of multiple options. UISegmentedControl which is known from UIKit is just a different picker style in SwiftUI. Let’s take a quick look on how to create a form with two items: picker with default style and picker with segmented style.
Creating a from
Form should be typically presented in a NavigationView which enables picker to present a list of items on a separate view in the navigation stack. Using the code below, we can create a form view which has a familiar visual style.
This file contains hidden or 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
Form view in navigation view for enabling navigation stack.
Example app presenting a form with pickers.
Setting up a picker and providing data with enum
Creating a picker requires specifying a title, a binding for selection, and function for providing a content. In the example below, we are using ForEach for creating views on demand. The number of options is defined by the number of items in the array. Each string is also used as identification of the view provided by the ForEach.
This file contains hidden or 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
An enum which is conforming to CaseIterable protocol is a convenient way for defining data for a picker. In the example below, data is identified by enum case’s rawValue and each view has a tag equal to enum’s case. Without a tag, selection would not be displayed because there would not be a connection between the selection binding and the view returned by ForEach.
This file contains hidden or 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
Creating a form view which uses the familiar style from iOS requires only a little bit of code in SwiftUI. Adding pickers and segmented controls, which are used a lot in forms, are pretty easy to set up.
SwiftUI provides wrappers for UIViewController and UIView on iOS. Same wrappers are also available for AppKit views on macOS. Let’s see how to use those wrappers for rendering UIKit views in SwiftUI previews and therefore benefiting from seeing changes immediately. Note that even when a project can’t support SwiftUI views because of the minimum deployment target, then this is still something what can be used when compiling the project with debug settings. Preview related code should only be compiled in debug builds and is never meant to be compiled in release builds. Before we jump in, there are two very useful shortcuts for keeping in mind: option+command+return for toggling previews and option+command+p for refreshing previews.
UIViewControllerRepresentable for wrapping UIViewControllers
UIViewControllerRepresentable is a protocol which can be used for wrapping UIViewController and representing it in SwiftUI. We can add a struct which conforms to that protocol and then creating an instance of the view controller in the makeUIViewController method. Second step is to add another struct which implements PreviewProvider protocol and which is used by Xcode for rendering previews. In simple cases we can get away only with such implementation but in more complex view controllers we would need to set up dependencies and generate example data for the preview. If need to do this, then all that code can be added to the makeUIViewController method.
This file contains hidden or 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
Wrapping UIViewController with UIViewControllerRepresentable.
UIViewController shown using SwiftUI
UIViewRepresentable for wrapping UIViews
UIViewRepresentable follows the same flow. In the example below, we use Group for showing two views with fixed size and different appearances at the same time.
This file contains hidden or 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
Wrapping UIView subclass with UIViewRepresentable.
Multiple UIViews shown in SwiftUI preview at the same time.
Summary
We looked into how to wrap view controllers and views for SwiftUI previews. Previews only required a little bit of code and therefore it is something what we can use for improving our workflows when working with UIKit views.
iOS apps can add quick actions which are displayed when force touching on the app’s icon. We’ll add quick actions to my open source WaterMyPlants example app. Quick actions can be static and dynamic: static actions are defined in the Info.plist and dynamic actions are configured in the code by updating UIApplication’s shortcutItems property. In the WaterMyPlants app, we’ll add one static action for adding a new plant and dynamic actions for opening added plants.
Home screen quick actions.
Static quick actions
Static quick actions are defined in the Info.plist. We need to add the UIApplicationShortcutItems key with array of dictionaries. Every dictionary in the array defines a quick action. Quick actions are required to have type and title and optionally we can add a subtitle and an icon. In the example below, we used one of the predefined icons. Predefined icons can be used by adding a key UIApplicationShortcutItemIconType with a string matching a format UIApplicationShortcutIconType<name>. Custom images are defined by the UIApplicationShortcutItemIconFile key where the string value is the name of an image in the asset catalog.
This file contains hidden or 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
Actions which depend on the data or state of the app can be added by setting the UIApplications’s shortcutItems property. Note that when adding items then we can use UIApplicationShortcutIcon‘s systemImageName initializer which enables us using any of the SF Symbols. Otherwise it is pretty much the same as defining a static quick action: setting type and title. It is useful to add an enum containing all the action types which becomes handy when we are handling actions. WaterMyPlants app uses scene delegates and therefore a good time to set dynamic quick actions is when the scene is resigning active status (see sceneWillResignActive(_:)).
This file contains hidden or 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
Quick actions are handled either in the UIApplicationDelegate or in the UISceneDelegate. WaterMyPlants uses scene delegates, therefore we’ll look into how to perform actions using scene delegates. We need to keep in mind that when selecting a shortcut launches the app, then we would need to check the shortcut property in the UIScene.ConnectionOptions and use it for configuring the UI to perform the action (windowScene(_:performActionFor:completionHandler:) is not called in that case). But if the app is already running in the background, then we can handle the action in the performActionFor delegate callback.
This file contains hidden or 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 added home screen quick actions to the WaterMyPlants app. We looked into how to add static and dynamic quick actions and how to perform the selected action.
I am happy to announce that Signal Path 2.0 is available now for macOS and iOS. Signal Path uses Apple’s universal purchase offering – buy it once for both platforms.
I spent a lot of time architecting both apps in a way that they reuse as much functionality as possible: from Metal pipelines to view models powering the UI. Most of the UI is written in SwiftUI, but there are a couple of views using UIKit (iOS) and AppKit (macOS) directly. Now when the groundwork is done, every next release will offer the same core functionality on both platforms and also integrating OS specific features. Future is bright, give Signal Path a try!
What is Signal Path
Signal Path is the most performant spectrum viewing app with beautiful user interface. You can record audio spectrums using microphone or open large recordings containing I/Q data. Read more about Signal Path.
Let’s take a look on how to set an equal width to multiple views where the width equals to the widest Text() view in SwiftUI. SwiftUI does not have an easy to use view modifier for this at the moment, therefore we’ll add one ourselves. Example use case is displaying two text bubbles with width equaling to the widest bubble.
Creating a content view with text bubbles
The content view contains two TextBubble views with different text. The minimum width for both Text() values are stored in the @State property. The property is updated by custom equalWidth() view modifiers in TextBubble views using bindings. In other words, the view modifier updates the minimum width based on the text in a TextBubble view and uses the calculated value for both TextBubble views.
This file contains hidden or 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
PreferenceKeys in SwiftUI are used for propagating values from child views to ancestor views. This is what we are using here as well: TextBubble views are reading their size using a GeometryReader and then setting the width of the text to our custom EqualWidthPreferenceKey. The background view modifier is used for layering GeometryReader behind the content view which avoids GeometryReader to affect the layout. Transparent color view is only used for producing a value for the preference key which is then set to the binding in the preference key change callback. The frame view modifier reads the value and makes the returned view wider if needed.
This file contains hidden or 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
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 hidden or 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 hidden or 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 hidden or 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.
Creating screenshots manually for App Store is time consuming because of the number of devices and localisations. Moreover, as soon as UI changes we would need to update screenshots again. Therefore, it is best to automate the process. One way is to use fastlane and creating nice device framed screenshots with additional localised text. This time we’ll see how to add fastlane to existing project, setting up UI-testing target, scheme for App Store screenshot tests, and framing screenshots with additional text.
End result of a framed screenshot with additional text.
Installing required tools
First step is installing fastlane and as frameit command requires imagemagick, let’s install it now as well. When using homebrew installation goes as: brew install fastlane imagemagick.
Adding UI-testing target
Fastlane uses UI-tests for displaying views and then capturing screenshots. If project does not have UI-testing target set up, then this is the first step. In Xcode, open add target view (File > New > Target), choose UI-Testing Bundle and add it to the project. In our case the testing target’s name is FastlaneAppStoreScreenshotsUITests.
Support files for capturing screenshots
First thing we’ll do with fastlane is running the fastlane snapshot init command in the project folder. It will generate 2 files: Snapfile and SnapshotHelper.swift. First one contains configuration for snapshot command: devices, locales etc. Helper file we’ll move to UI-testing target: first drag it to testing target folder (FastlaneAppStoreScreenshotsUITests) and then to Xcode’s file navigator. Make sure to add it to the UI-testing target. Create subfolder named fastlane and move Snapfile to there. We’ll modify it a little bit later.
Project structure after adding UI-testing target and supporting SnapshotHelper.swift
Adding UI-test case for screenshots
We are going to define tests generating App Store screenshots in a separate XCTestCase. In Xcode, add a new UI-test case class (File > New File > UI-Test Case Class) and name it to AppStoreScreenshotUITests.
Best is when it is possible to record a UI-test which does not need any special configuration: launch the app, tap here and there, take a screeshot. In more complicated apps, we need to use launch arguments and configuring the app when it launches based on the launch arguments. In document based apps it might be needed to bundle example files in the app and then using launch arguments for choosing the file to open when running the test.
Firstly, let’s add 3 UI-tests which will cover 3 different views and calling the function snapshot(“file_name”) which is part of the SnapshotHelper.swift and handles creating screenshots and storing them in the folder specified by Snapfile.
final class AppStoreScreenshotUITests: XCTestCase {
private var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
// Example of passing in launch arguments which can be read in the app
// by calling CommandLine.arguments.contains("--uitesting")
// app.launchArguments.append("--uitesting")
setupSnapshot(app)
app.launch()
}
override func tearDownWithError() throws {
app = nil
}
func testScreenshot1() throws {
snapshot("1-mainview")
}
func testScreenshot2() throws {
XCTAssertTrue(app.buttons["Button1"].waitForExistence(timeout: 10))
app.buttons["Button1"].tap()
snapshot("2-sheet1")
}
func testScreenshot3() throws {
XCTAssertTrue(app.buttons["Button2"].waitForExistence(timeout: 10))
app.buttons["Button2"].tap()
snapshot("3-sheet2")
}
}
Separate scheme for App Store screenshots
UI-tests are set up for App Store screenshots, next is to add a separate scheme and adding those tests to the Test action. This scheme is the one fastlane is going to use for running tests.
For adding a new scheme, open manage schemes view (Product > Scheme > Manage Schemes). Click on the + button, select the app target and name it to AppStoreScreenshots.
AppStoreScreenshots scheme in Manage Scheme view.
After that, select the scheme and click on the Edit, open Test action and select only App Store tests. Also make sure the Shared checkbox is selected for allowing fastlane to run it.
Selecting tests in Test action for AppStoreScreenshots schemes.
Configuring Snapfile
Snapfile contains configuration for the snapshot command. There we define all the devices, locales, scheme name, location where to store generated screenshots etc. For getting started, we can start with one locale and two devices. We also need to select the scheme we added before.
App Store screen generation contains of two main steps: capturing screenshots and framing those. Fastfile allows us to create custom commands, lanes, and it is useful of adding one for app store screenshots. Create a file named Fastfile and add it to previously created Fastlane folder.
default_platform(:ios)
platform :ios do
desc 'Generate App Store screenshots'
lane :app_store_screenshots do
capture_screenshots(
clean: true
)
frame_screenshots
end
end
Running fastlane app_store_screenshots will then generate screenshots and create framed versions as well. If you run it now, you’ll get screenshots with and without device frames.
Adding titles and keyword to framed screenshots
Now we have nice framed screenshots, only what is missing is adding some text to all the screenshots. If we ran fastlane app_store_screenshots in the previous step we can see that AppStoreScreenshots folder has appeared and taken screenshots are in the en-US folder. Adding text and background to each of the screenshot requires a couple of steps.
First we’ll create Framefile.json file in the AppStoreScreenshots folder which contains of configuration for frameit.
Default dictionary contains shared settings for every frame operation. We can customise by adding data array which contains screenshot specific configuration (more). Filter in data dictionaries are used for matching the configuration to screenshot files (filenames used in UI-tests). In this example we have different backgrounds for every screenshot (background files are in the same AppStoreScreenshots folder). Fonts are configured by adding a path to Framefile.json and storing fonts in a subfolder.
Localised titles and keywords are handled with strings files. We’ll need to add title.strings and keyword.strings to the locale folder, in our case, en-US. Keys in strings files work the same way as filters in Framefile.json, therefore partial match with filename works. Example strings file for title.strings is shown here:
I suggest creating strings files with Xcode making sure the file encoding is correct. If we run the fastlane app_store_screenshots command again, we’ll get nice framed screenshots with additional text.
Summary
Automating App Store screenshots with fastlane is not difficult to set up. The most difficult part in this process is writing UI-tests and providing proper data in the app which is highly app specific.
By default SwiftUI view content gets behind a keyboard when editing text. Therefore let’s create a view modifier which can be easily added to any SwiftUI view and enables revealing content behind the keyboard with animation. In the end we will fix this broken looking view where TextField is behind the keyboard.
SwiftUI view with open keyboard hiding text field.
Observing keyboard notifications
I wrote about keyboard notifications a while ago in “Observing keyboard visibility on iOS”. We’ll also create a class named KeyboardObserver and its responsibility is to observe keyboardWillChangeFrameNotification, keyboardWillShowNotification and keyboardWillHideNotification and extracting values from the notification’s userInfo. In addition, we’ll add Info struct which holds animation duration, curve and end frame. Note that user info contains more values but we are only interested in those. With this set, we can subscribe to KeyboardObserver and get notified when keyboard changes. Next step is to use those values and reserving space for keyboard in SwiftUI view.
fileprivate final class KeyboardObserver: ObservableObject {
struct Info {
let curve: UIView.AnimationCurve
let duration: TimeInterval
let endFrame: CGRect
}
private var observers = [NSObjectProtocol]()
init() {
let handler: (Notification) -> Void = { [weak self] notification in
self?.keyboardInfo = Info(notification: notification)
}
let names: [Notification.Name] = [
UIResponder.keyboardWillShowNotification,
UIResponder.keyboardWillHideNotification,
UIResponder.keyboardWillChangeFrameNotification
]
observers = names.map({ name in
NotificationCenter.default.addObserver(forName: name,
object: nil,
queue: .main,
using: handler)
})
}
@Published var keyboardInfo = Info(curve: .linear, duration: 0, endFrame: .zero)
}
fileprivate extension KeyboardObserver.Info {
init(notification: Notification) {
guard let userInfo = notification.userInfo else { fatalError() }
curve = {
let rawValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
return UIView.AnimationCurve(rawValue: rawValue)!
}()
duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
}
}
Creating view modifier
View modifiers in SwiftUI transform the original view and return a new version of the original view. In the view modifier we’ll observe KeyboardObserver and add bottom padding to the original view based on the keyboard height and current safeAreaInsets. In addition, we’ll wrap it into animation block which tells SwiftUI to animate changes.
The view modifier uses GeometryReader for reading safeAreaInsets. It’s important to take this into account when keyboard is open, otherwise there will be unnecessary spacing.
View modifiers can be added to a view by using modifier function and passing an instance of view modifier to it. We’ll add a convenience method for it.
Next step is to adding the view modifier to a content view. The view modifier should be added to the root view and it is recommended to use ScrollView for making sure all the content is viewable when keyboard is open. It’s time to fix the view mentioned in the beginning of the post. We’ll add keyboardVisibility view modifier to the root view.
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack(spacing: 16) {
Spacer()
Ellipse().foregroundColor(.red)
.aspectRatio(contentMode: .fit)
.frame(height: 200)
Text("Welcome").font(.title)
Text("Please enter your name")
TextField("Name", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Spacer()
}.keyboardVisibility()
}
}
KeyboardVisibility view modifier will make sure the content view has bottom spacing equal to keyboard height and the end result looks like this.
SwiftUI view with keyboard visibility view modifier.
Summary
We created a view modifier which is easy to add to existing views. It observers keyboard notifications and animates the content view along with keyboard.
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.