5 February, 2023
SwiftUI Technology Selection Considerations
Overview
SwiftUI and Combine were introduced in iOS13, and async/await was introduced in Swift5.5,
causing a significant change from the existing UIKit implementation.
As time has passed since the introduction of SwiftUI, related articles and case studies have increased.
There are fewer applications that still support iOS13 or earlier, and these refactoring tasks are becoming necessary.
Technology Selection Considerations
The main focus of this article is two points:
- Coexistence of "Source of Truth" promoted by SwiftUI and Combine implementation
- Use of Combine asynchronous processing and Swift's async/await asynchronous processing
I believe that by designing these issues in a clean manner with the UI architecture, each of their strengths can be maximized. The following figure shows a MVVM architecture design.
Data update only in UI
- Follow Apple's "Source of Truth" promotion
- Maintain properties at the highest layer of the View Hierarchy and render updates hierarchically top-down with data updates
- Make it a single directional data flow, like Flux or Redux
Data management of Domain and UI connection
- Use Combine to subscribe to data changes with ObservableObject
- Bilateral binding with View, but design each data flow to be a single flow
- Enable asynchronous actions separated from the Main Thread
Data handling of Domain
- The domain logic is designed to be modularized by functionality, and dependencies are made clear by dependency injection into the UI logic
- Implementations like functional programming, where functions are processed when called from the UI logic
- Processes are executed simply and asynchronously without using the Observable pattern, using Swift's async/await function.
View
In SwiftUI, all data within the View hierarchy must be defined with a single source of truth. Unlike in UIKit, where Views are defined as a Class type, SwiftUI defines Views as Struct type. This change is significant because Views do not maintain state, but instead render a blueprint each time the data is updated.
SwiftUI provides many new Property Wrappers specifically for Views. By properly utilizing these, you can achieve a single-direction data flow similar to that of Redux. Additionally, being the single source of truth ensures that changes can be made safely from any thread.
The State is defined at the top-most View of the necessary hierarchy. Lower-level Views make use of the State by either Binding or copying the Data from the top-most View.
- @Binding allows for Read & Write operations on Child Views
- let allows for Read Only operations on Child Views
ViewModel
ViewModel is a design method used in MVVM UI architecture that binds UI logic and domain logic bidirectionally. It aims to subscribe to the results of domain logic processing using the Observer pattern or to process it isolated from the main thread.
To maximize the effects of the Observer pattern, Combine, which was introduced in iOS 13, is adopted. RxSwift, which has been supported for a long time to realize reactive programming in Swift, is still very convenient as an open-source.
However, as iOS natively supports conversion to Combine through features such as NotificationCenter.default.publisher, Timer.publish(), and NSObject.KeyValueObservingPublisher, let's actively adopt Combine.
The bidirectional binding in MVVM has the disadvantage that data changes in both directions. I introduce a mechanism for changing data in one direction, like Redux.
Please define ViewModel based on the above as a template. ViewModel is defined as a struct and consists of three components: Input, which collects triggers, Output, which collects observable data, and a definition of the data flow from Input to Output.
To prevent bidirectional reading and writing of data, we make Input a request for data changes and Output a read-only data, and subscribe to data changes in one direction.
The key here is the ObservableObject and Published of Combine. By conforming to ObservableObject, data will be reloaded in the view when there is a change. The Published property wrapper can tell SwiftUI to trigger the view's reload.
Model
The domain logic separates itself from the UI logic by utilizing the concept of separation of concerns (SoC). SoC means building a program with components that are separated by concerns or responsibilities. By modularizing the functionality, the necessary functions can be tied to the necessary UI. This is called Dependency Injection.
For example, as shown in the figure, the Purchase Service, which has payment-related functions, can be applied to the UI that requires payment information and processing, and made inaccessible from other UIs. Similarly, by tying the Auth Service to the UI that requires authentication information and processing, it is easy to understand which screens require authentication information.
Implementing this modular, function-based Model, which is separated by concerns, makes it possible to use it as a module or SDK call, using a different asynchronous description with async/await, rather than Combine.
Combine and Swift async/await are often referred to as overlapping technologies from the perspective of asynchronous processing, but GCD is more commonly described as being replaced by async/await.