Skip to content

hudishkin/vvsi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

20 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

View - ViewState - Interactor (VVSI)

VVSI is an experimental (very) simple architecture for SwiftUI applications, representing something between TCA (The Composable Architecture) and MVVM. The library offers a structured and predictable approach to managing state and business logic, while maintaining ease of use.

πŸ“Š Architecture Diagram

VVSI Architecture Diagram

🌟 Benefits

  • Separation of concerns β€” clear separation of UI, state, and business logic
  • Predictable data flow β€” unidirectional data flow makes the application more stable
  • Testability β€” isolated components are easy to test
  • Out-of-the-box asynchronicity β€” support for asynchronous operations using Swift Concurrency
  • Native SwiftUI integration β€” works naturally with the SwiftUI system

πŸ“¦ Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/username/VVSI.git", from: "0.0.5")
]

πŸš€ How to Use

VVSI consists of three main components:

  1. View β€” responsible for UI and state display
  2. ViewState β€” stores the current state and processes actions
  3. Interactor β€” contains business logic and handles side effects

Example: Simple List

1. Create state and action components

// ListView+State.swift

extension ListView {

    struct Options {
        let count: Int
        let length: Int
    }

    struct VState: StateProtocol {
        var items: [String] = []
    }

    enum VAction: ActionProtocol {
        case add
        case remove
        case random(Options)
    }

    enum VNotification: NotificationProtocol {
        case error(String)
    }
}

2. Implement the interactor to handle business logic

// ListView+Interactor.swift

extension ListView {

    final class Interactor: ViewStateInteractorProtocol {

        typealias S = VState
        typealias A = VAction
        typealias N = VNotification

        let notifications: PassthroughSubject<N, Never> = .init()
        let service: Dependencies.Service

        init(dependencies: Dependencies = .shared) {
            service = dependencies.service
        }

        @MainActor
        func execute(
            _ state: @escaping CurrentState<S>,
            _ action: VAction,
            _ updater: @escaping StateUpdater<S>
        ) {
            switch action {
            case .add:
                Task.detached { [weak self] in
                    guard await state().items.count < 5 else {
                        self?.notifications.send(.error("Max items count is 5"))
                        return
                    }

                    await updater { state in
                        state.items.append("New item")
                    }
                }

            case .remove:
                Task.detached {
                    await updater { state in
                        if !state.items.isEmpty {
                            state.items.removeLast()
                        }
                    }
                }
            case .random(let opt):
                Task {
                    let strings = (0..<opt.count).map { _ in randomString(length: opt.length) }
                    await updater { state in
                        state.items = strings
                    }
                }
            }
        }
    }
}

3. Create a view that uses ViewState

// ListView.swift

struct ListView: View {

    enum AlertType: Identifiable {
        var id: String { "\(self)" }
        case error(String)
    }

    @StateObject
    var viewState = ViewState(.init(), Interactor())

    @State
    private var alertType: AlertType? = nil

    var body: some View {
        ForEach(viewState.state.items, id: \.self) { item in
            Text(item)
        }

        HStack {
            Button {
                viewState.trigger(.add)
            } label: {
                Text("Add")
            }

            Button {
                viewState.trigger(.remove)
            } label: {
                Text("Remove")
            }
            Button {
                viewState.trigger(.random(.init(count: Int.random(in: 1..<10), length: Int.random(in: 1..<5))))
            } label: {
                Text("Random")
            }
        }
        .alert(item: $alertType) { item in
            switch item {
            case .error(let error):
                Alert(
                    title: Text("Error"),
                    message: Text(error),
                    dismissButton: .default(Text("Ok"))
                )
            }
        }
        .onReceive(viewState.notifications) { notification in
            switch notification {
            case .error(let message):
                alertType = .error(message)
            }
        }
    }
}

🧩 Core Concepts

  • StateProtocol β€” protocol for state structures
  • ActionProtocol β€” protocol for action enumerations
  • NotificationProtocol β€” protocol for notifications from interactor to view
  • ViewStateInteractorProtocol β€” protocol for interactors handling business logic

πŸ“š Complete Example

A complete implementation example is available in the VVSIExample directory.

πŸ“„ License

This project is released under the MIT license. See LICENSE for details.

πŸ‘₯ Contributors

We welcome your suggestions and contributions to the project! Create Issues and Pull Requests.

About

Lightweight architecture for SwiftUI application. Something between TCA and MVVM.

Topics

Resources

License

Stars

Watchers

Forks

Languages