swift-unidirectional-flow

repository·main·Indexed 43 days ago

https://github.com/mecid/swift-unidirectional-flow

A Swift library providing a Unidirectional Data Flow architecture using modern Swift Generics and Concurrency features. It implements a pattern based on State, Action, Reducer, Middleware, and Store. Version v0.4.x supports Swift 6 strict concurrency, while v0.3.x leverages the Observation framework for iOS 17+ and macOS 14+.

Tokens
2.1K
Snippets
1
Records
2
Agent score
5%

What's inside swift-unidirectional-flow

  1. Implement Unidirectional Flow with Store, Reducer, and Middleware

    main

    The library implements a unidirectional data flow pattern using three core components:

    1. State: A struct representing the single source of truth for your feature.
    2. Action: An enum representing all possible events or intents.
    3. Reducer: A protocol that defines how the current State is transformed into a new State given an Action.
    4. Middleware: An actor that intercepts actions to perform side effects (like API calls) and returns new actions based on the results.
    5. Store: The central object that manages the state, applies reducers, and runs middlewares.

    Implementation Example

    // 1. Define State
    struct SearchState: Equatable {
        var repos: [Repo] = []
        var isLoading = false
    }
    
    // 2. Define Actions
    enum SearchAction: Equatable {
        case search(query: String)
        case setResults(repos: [Repo])
    }
    
    // 3. Implement Reducer
    struct SearchReducer: Reducer {
        func reduce(oldState: SearchState, with action: SearchAction) -> SearchState {
            var state = oldState
            switch action {
            case .search:
                state.isLoading = true
            case let .setResults(repos):
                state.repos = repos
                state.isLoading = false
            }
            return state
        }
    }
    
    // 4. Implement Middleware for side effects
    actor SearchMiddleware: Middleware {
        struct Dependencies {
            var search: (String) async throws -> SearchResponse
            // ... production implementation ...
        }
    
        let dependencies: Dependencies
        init(dependencies: Dependencies) { self.dependencies = dependencies }
        
        func process(state: SearchState, with action: SearchAction) async -> SearchAction? {
            switch action {
            case let .search(query):
                let results = try? await dependencies.search(query)
                return .setResults(repos: results?.items ?? [])
            default:
                return nil
            }
        }
    }
    
    // 5. Initialize the Store
    typealias SearchStore = Store<SearchState, SearchAction>
    let store = SearchStore(
        initialState: .init(),
        reducer: SearchReducer(),
        middlewares: [SearchMiddleware(dependencies: .production)]
    )
    struct SearchState: Equatable {
        var repos: [Repo] = []
        var isLoading = false
    }
    
    enum SearchAction: Equatable {
        case search(query: String)
        case setResults(repos: [Repo])
    }
    
    struct SearchReducer: Reducer {
        func reduce(oldState: SearchState, with action: SearchAction) -> SearchState {
            var state = oldState
            
            switch action {
            case .search:
                state.isLoading = true
            case let .setResults(repos):
                state.repos = repos
                state.isLoading = false
            }
            
            return state
        }
    }
    
    actor SearchMiddleware: Middleware {
        struct Dependencies {
            var search: (String) async throws -> SearchResponse
            
            static var production: Dependencies {
                .init { query in
                    guard var urlComponents = URLComponents(string: "https://api.github.com/search/repositories") else {
                        return .init(items: [])
                    }
                    urlComponents.queryItems = [.init(name: "q", value: query)]
                    
                    guard let url = urlComponents.url else {
                        return .init(items: [])
                    }
                    
                    let (data, _) = try await URLSession.shared.data(from: url)
                    return try JSONDecoder().decode(SearchResponse.self, from: data)
                }
            }
        }
    
        let dependencies: Dependencies
        init(dependencies: Dependencies) {
            self.dependencies = dependencies
        }
        
        func process(state: SearchState, with action: SearchAction) async -> SearchAction? {
            switch action {
            case let .search(query):
                let results = try? await dependencies.search(query)
                guard !Task.isCancelled else {
                    return .setResults(repos: state.repos)
                }
                return .setResults(repos: results?.items ?? [])
            default:
                return nil
            }
        }
    }
    
    typealias SearchStore = Store<SearchState, SearchAction>
    
    struct SearchContainerView: View {
        @State private var store = SearchStore(
            initialState: .init(),
            reducer: SearchReducer(),
            middlewares: [SearchMiddleware(dependencies: .production)]
        )
        @State private var query = ""
        
        var body: some View {
            List(store.repos) { repo in
                VStack(alignment: .leading) {
                    Text(verbatim: repo.name)
                        .font(.headline)
                    
                    if let description = repo.description {
                        Text(verbatim: description)
                    }
                }
            }
            .redacted(reason: store.isLoading ? .placeholder : [])
            .searchable(text: $query)
            .task(id: query) {
                await store.send(.search(query: query))
            }
            .navigationTitle("Github Search")
        }
    }
  2. Install swift-unidirectional-flow via Swift Package Manager

    main

    To use this library, add it as a Swift Package dependency in Xcode:

    1. Go to File > Add Package Dependencies...
    2. Enter the GitHub repository URL: https://github.com/mecid/swift-unidirectional-flow.

    Version Selection Guide

    • v0.4.x: Use this version for Swift 6 strict concurrency support.
    • v0.3.x: Use this version if you want to leverage the Observation framework (requires iOS 17+ or macOS 14+).
    • v0.2.x: Use older versions if you need to support legacy iOS or macOS versions.