Separate `action` to concise SwiftUI View
Tags: swiftui
Published: 2023-01-23 15:01:41

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.

```swift
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.

```swift
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 action~~
    - ~~Action class has testability~~

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