App Development

SwiftUI: An Introduction and the Gotchas to be Aware of

Have you heard of SwiftUI yet? SwiftUI is the new declarative UI framework that Apple is championing as the future of frontend iOS development. Although only a year old, SwiftUI is a robust framework ready for consumption. Here is a brief introduction to the framework and a few quirks to be aware of when considering the change from UIKit to SwiftUI on your next iOS Project.

State and New Property Wrappers

The real beauty of SwiftUI comes from its simplification of the view architectural pattern. UIKit architecture commonly requires three parts to accomplish the view lifecycle: ViewModel, ViewController, View. The ViewModel tells the ViewController what the data looks like. The ViewController applies the ViewModel to the View. The ViewController handles user interaction on the View and modifies the ViewModel as necessary. And the cycle repeats again. SwiftUI removes the ViewController middleman and leaves the View in charge of handling ViewModel changes. How does SwiftUI accomplish this? New Property Wrappers. State management with Property Wrappers fulfils the responsibility of connecting Data Models to usable View elements.

@State

The @State wrapper demonstrates the true power of SwiftUI in giving each View the ability to manage its own state. @State properties are private by convention as they manage state changes in one singular View. Everytime a @State property changes values, all View elements (i.e. Text, Button, Image, etc.) that reference that property are re-rendered.

 struct MyView: View {
    @State private var count: Int = 0 // State property
    var body: some View {
        Button(action: {
            self.count += 1 // Triggers Text refresh on change
        }) {
            Text("Increase Count: " + String(count)) // Referenced State property
        }
    }
 }

@Binding

Another guiding principle of SwiftUI is having a Single Source of Truth. Imagine a scenario where you have a parent View that tracks a property using @State and has a child View that needs to access the same property. You could duplicate the property and have a @State variable in both Views to track. However, this becomes quite messy when you now need to update changes to the property in two different locations. This is where bindings come in. Bindings connect the @State property in the parent View to the @Binding property in the child View via a binding (“$” syntax). Any changes to the property are reflected in both the parent and children Views.

 struct ParentView: View {
    @State private var count: Int = 0 // Parent State property
    var body: some View {
        ChildView(count: $count) // Binding of property
    }
 }

 struct ChildView: View {
    @Binding var count: Int // Child Binding property
    var body: some View {
        Text("Count: " + String(count))
    }
 }

@ObservedObject + @Published

@State is typically used to manage the state of properties of built-in types (i.e. String, Int, Bool, etc.). However, a common use case would be to track the state of a variable of a custom type. Use @ObservedObject to declare a state variable of custom type in a View and use @Published on the properties of the custom type that should trigger a refresh of the View when their values change.

 struct MyView: View {
    @ObservedObject var customObject: MyCustomType(count: 0, name: "Name")
    var body: some View {
        Text(customObject.name + " Count: " + String(customObject.count))
    }
 }

 class MyCustomType: ObservableObject {
    @Published var count: Int // Triggers Text refresh on change
    var name: String // Does not trigger Text refresh on change
 }

@EnvironmentObject

Sometimes there are scenarios where certain Views need access to a particular property that other Views are not concerned about. Imagine that both the root View of an app and some distant child View in the View hierarchy both need access to the same property. One solution is to manage the @State property in the root View and pass the property value via bindings through every child View until we reach the desired View. This seems quite wasteful as none of the Views in between access the property. Instead, use the @EnvironmentObject wrapper to allow all descendants of a View to access the property without the use of bindings.

 class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        ...
        let contentView = RootView().environmentObject(UserSettings())
        ...
    }
 }

 struct DistantChildView: View {
    @EnvironmentObject var settings: UserSettings
    var body: some View {
        Text("Name: \(settings.name)")
    }
 }

 class UserSettings: ObservableObject {
    @Published var name
 }

Order Matters with View Modifiers

A View Modifier is a method on a View that performs a style change on that View and returns a modified version of the View. Modifiers can be chained together to quickly alter the color, size, shape, position, etc of a View. It is important to understand that each modifier returns a new version of the View and not the original unmodified View (Similar to the Builder Design Pattern). Meaning each successive chained modifier is essentially interacting with a different View. Consider the following examples:

swift cherry 1


swift cherry image 2

We apply the same four modifiers to the same image yet we yield different results. Why is this? The answer - modifier order matters. The only difference between the two examples is that we apply the .cornerRadius(20) modifier before the .padding() modifier in the first example. Why does this matter? As mentioned, modifiers return a new version of the View each time. In the first example, when we call .cornerRadius(20) we are returned a View with corners of radius 20. When we call .padding() we add standard padding to the new View which already features a corner radius. In the second example, we add standard padding with .padding() first and then add a corner radius with .cornerRadius(20) on the new View. Since the image already has padding when the corner radius is applied, the radius changes occur on the invisible padding and are not visible on the View.

Auto-Rendering of NavigationLink Views

One of the useful new View elements in SwiftUI is the NavigationLink. A NavigationLink is a View that navigates to a destination View when pressed.

NavigationLink(destination: MyDestinationView())

Interestingly, NavigationLinks initialize their destination Views before actually attempting to navigate to them. This creates circumstances where Views that are never actually navigated to are fully initialized. A common convention in SwiftUI is to handle ViewModel initialization in the init() method of a View. This becomes especially problematic when the ViewModel is populated via a Data Service or Manager. If one of these Views with ViewModel initialization and network requests is used in a NavigationLink, we could potentially be wasting resources. Do not fear… there is a solution to this in the form of a simple wrapper.

 struct NavigationLazyView<Content: View>: View {
    let build: () -> Content

    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }

    var body: Content {
        build()
    }
 }

The wrapper delays the initialization of the destination View until SwiftUI needs to use the body of the NavigationLazyView (which does not occur until an actual navigation attempt). Modify your NavigationLink code to use the wrapper like so:

NavigationLink(destination: NavigationLazyView(MyDestinationView())) 

and eliminate your fears of accidentally initializing unused Views or making extraneous network requests.

NavigationLink Unique Tag Identifier

Keeping along with the theme of navigation, an alternate way of initializing a NaviationLink is by providing a selection and tag as well:

NavigationLink(destination: MyDestinationView(), tag: item.id, selection: self.$selection)

This is commonly used when iterating over an array and creating a collection of View elements for each member of the array. Each element in the array becomes a NavigationLink and each NavigationLink is given a tag as an identifier. Whenever the selection value is changed, the NavigationLink with the matching tag is executed and its destination View is navigated to. Each NavigationLink must have a unique and consistent (does not change) tag. Failure to comply with this convention does not result in a compilation or runtime error but creates frustrating unintended consequences such as navigation becoming invalidated and the destination View only appearing for a split second. To avoid this problem, make sure to use libraries like UUID() (which guarantee unique identifiers) when setting ID values in your ViewModels.

SwiftUI is a powerful tool that aims to simplify the iOS UI development process. If you can master the differences from UIKit and recognize some of its gotchas, SwiftUI is more than a viable tool to use in production applications. Just as Swift has taken over Objective-C as the primary iOS development language, SwiftUI seems like a safe bet to take over UIKit as the primary UI framework in the near future.

Want to learn more development best practices from WillowTree’s engineering team? Get in touch today.

Security Scanning and Automation in a CI/CD Pipeline: Being Proactive In Security

Exposition How do we make our applications more secure upon release? Over the...

Read the article