When I implement apps with SwiftUI, the View code gets bigger. Especially, UI definitions and model logic are mixed so the view code gets harder to read. I tried to move model logic to other classes. Then the view code got simpler.
Before
I created an app to examine. In this app, the app receives a text from the user and checks whether its counts within 140. If the count is over 140, the post button is disabled.
Here is the code.
import SwiftUI
struct ContentView: View {
@State private var content = ""
@State private var canPost = false
private let session = MySession()
var body: some View {
TextField(
"Content", text: $content
)
.onChange(of: content) { newValue in
canPost = (1...140).contains(newValue.count)
}
Button("Post") {
Task {
await session.upload(content)
}
}
.disabled(!canPost)
}
}
When I implement this code, there are some codes to improve.
- View has a model property
- In some cases, the view might have properties such as ViewModel or Interactor.
- Validation logic is in the View definition
- Because SwiftUI is a declarative UI framework, it is natural but may cause complex code.
If the view code is big, it is hard to tell apart view definitions and logic.
After
I tried to move logic to other classes. For example, the codes of the action
argument in Button and the perform
argument in the .onChange
modifier.
- action in
Button(action: @escaping () -> Void, label: () -> Label)
- perform in
.onChange<V>(of value: V, perform action: @escaping (V) -> Void)
Here is the code.
import SwiftUI
struct ContentView: View {
@State private var content = ""
@State private var canPost = false
var body: some View {
TextField(
"Content", text: $content
)
.onChange(
of: content,
perform: ContentValidator($canPost).validate(_:)
)
Button(
"Post", action: PostAction($content).action
)
.disabled(!canPost)
}
}
struct ContentValidator {
@Binding private var canPost: Bool
init(_ canPost: Binding<Bool>) { _canPost = canPost }
func validate(_ newValue: String) {
canPost = (1...140).contains(newValue.count)
}
}
struct PostAction {
@Binding private var content: String
// better to DI
private let session = MySession()
init(_ content: Binding<String>) { _content = content }
func action() {
Task {
await session.upload(content)
}
}
}
Although I need to code import SwiftUI
for all classes, the view code gets small. In addition, the codes of action have testability.
Conclusion
I tried to move the action
logic in the view to other classes. Although I don't know the best practice for implementing the SwiftUI app, this way is easy to improve my code.
The amount of code increased, but I got some profits by using this method.
- Readable view code
- No action codes
- No model property
-
Testable actionAction class has testability
If you too are having trouble with complex View code, please try this method.
Top comments (0)