DEV Community

Takahiro Suzuki
Takahiro Suzuki

Posted on

Separate `action` to concise SwiftUI View

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 action
    • Action class has testability

If you too are having trouble with complex View code, please try this method.

Top comments (0)