Unidirectional reactive architecture using new Apple Combine framework

Overview

CombineFeedback

Unidirectional Reactive Architecture. This is a Combine implemetation of ReactiveFeedback and RxFeedback

Diagram

Motivation

Requirements for iOS apps have become huge. Our code has to manage a lot of state e.g. server responses, cached data, UI state, routing etc. Some may say that Reactive Programming can help us a lot but, in the wrong hands, it can do even more harm to your code base.

The goal of this library is to provide a simple and intuitive approach to designing reactive state machines.

Core Concepts

State

State is the single source of truth. It represents a state of your system and is usually a plain Swift type. Your state is immutable. The only way to transition from one State to another is to emit an Event.

Event

Represents all possible events that can happen in your system which can cause a transition to a new State.

Reducer

A Reducer is a pure function with a signature of ( inout State, Event) -> Void. While Event represents an action that results in a State change, it's actually not what causes the change. An Event is just that, a representation of the intention to transition from one state to another. What actually causes the State to change, the embodiment of the corresponding Event, is a Reducer. A Reducer is the only place where a State can be changed.

Feedback

While State represents where the system is at a given time, Event represents a state change, and a Reducer is the pure function that enacts the event causing the state to change, there is not as of yet any type to decide which event should take place given a particular current state. That's the job of the Feedback. It's essentially a "processing engine", listening to changes in the current State and emitting the corresponding next events to take place. Feedbacks don't directly mutate states. Instead, they only emit events which then cause states to change in reducers.

To some extent it's like reactive Middleware in Redux

Dependency

Dependency is the type that holds all services that feature needs, such as API clients, analytics clients, etc.

Store

Store - is a base class responsible for initializing a UI state machine. It provides two ways to interact with it.

  • We can start a state machine by observing var state: AnyPublisher .
  • We can send input events into it via public final func send(event: Event).

This is useful if we want to mutate our state in response to user input. Let's consider a Counter example

struct State {
    var count = 0
}

enum Event {
    case increment
    case decrement
}

When we press + button we want the State of the system to be incremented by 1. To do that somewhere in our UI we can do:

Button(action: {
    store.send(event: .increment)
}) {
    return Text("+").font(.largeTitle)
}

Also, we can use the send(event:) method to initiate side effects. For example, imagine that we are building an infinite list, and we want to trigger the next batch load when a user reaches the end of the list.

enum Event {
    case didLoad(Results)
    case didFail(Error)
    case fetchNext
}

struct State: Builder {
    var batch: Results
    var movies: [Movie]
    var status: Status
}
enum Status {
    case idle
    case loading
    case failed(Error)
}

struct MoviesView: View {
    typealias State = MoviesViewModel.State
    typealias Event = MoviesViewModel.Event
    let context: Context

    
   var body: 
   some View {
        List {
            
   ForEach(context.
   movies.
   identified(
   by: \.
   id)) { movie 
   in
                
   MovieCell(
   movie: movie).
   onAppear {
                
   // When we reach the end of the list

                   
   // we send `fetchNext` event

                       
   if 
   self.
   context.
   movies.
   last 
   == movie {
                        
   self.
   context.
   send(
   event: .
   fetchNext)
                    }
                }
            }
        }
    }
}
  

When we send .fetchNext event, it goes to the reducer where we put our system into .loading state, which in response triggers effect in the whenLoading feedback, which is reacting to particular state changes

    static func reducer(state: inout State, event: Event) {
        switch event {
        case .didLoad(let batch):
            state.movies += batch.results
            state.status = .idle
            state.batch = batch
        case .didFail(let error):
            state.status = .failed(error)
        case .retry:
            state.status = .loading
        case .fetchNext:
            state.status = .loading
        }
    }

    static var feedback: Feedback
    {
        
   return 
   Feedback(
   lensing: { 
   $0.
   nextPage }) { page 
   in
            URLSession.
   shared
                .
   fetchMovies(
   page: page)
                .
   map(Event.
   didLoad)
                .
   replaceError(
   replace: Event.
   didFail)
                .
   receive(
   on: DispatchQueue.
   main)
        }
    }
  

Composition

Taking inspiration from TCA CombineFeedback is build with a composition in mind.

Meaning that we can compose smaller states into bigger states. For more details please see Example App.

ViewContext

ViewContext - is a rendering context that we can use to interact with UI and render information. Via @dynamicMemberLookup it has all of the properties of the State and several conveniences methods for more seamless integration with SwiftUI. (Credits to @andersio)

init(store: Store ) { self.store = store } var body: some View { WithContextView(store: store) { context in Form { Section { TextField(context.binding(for: \.email, event: Event.emailDidChange)) TextField(context.binding(for: \.password, event: Event.passwordDidCange)) Button(action: context.action(for: .signIn)) { Text("Sign In") } } } } } }">
struct State  {
    var email = ""
    var password = ""
}
enum Event {
	case signIn
}
struct SignInView: View {
    private let store: Store
    
    
    init(
    store: Store
    
     ) {
        
     self.
     store 
     = store
    }
    
    
     var body: 
     some View {
      
     WithContextView(
     store: store) { context 
     in
        Form {
            Section {
                
     TextField(context.
     binding(
     for: \.
     email, 
     event: Event.
     emailDidChange))
                
     TextField(context.
     binding(
     for: \.
     password, 
     event: Event.
     passwordDidCange))
                
     Button(
     action: context.
     action(
     for: .
     signIn)) {
                    
     Text(
     "Sign In")
                }
            }
        }
      }
    }
}
    
   

Example

Counter Infinite List SignIn Form Traffic Light

References

Automata theory TCA Finite-state machine Mealy machine

Comments
  • [Question] ViewModels notified about state change outside of a view

    [Question] ViewModels notified about state change outside of a view

    Hey, this is a really interesting library, simple and nice to use, but I'm having hard times trying to figure out how to share data between multiple views.

    What is the best approach to communicate state change and get feedbacks outside of a View/ViewModel?

    Should it be done subscribing to the state: AnyPublisher<State, Never> property of the view model? Or sending a Feedback to the view model?

    opened by dchohfi 6
  • Duplicate declarations of feedback static methods in examples

    Duplicate declarations of feedback static methods in examples

    I noticed that the the when_() -> Feedback<State, Event> static methods are defined both in the viewModel and in the corresponding enum in the examples. It's not necessary given that the view model can just access them from the enum type, ex:

    ...
    feedback: [MoviesList.whenLoading()],
    ...
    

    Can you please confirm that the current duplication is not the intended way?

    opened by kaishin 2
  • Is Builder intended to be included in the library?

    Is Builder intended to be included in the library?

    Following the examples in my own project, I realized that the Builder helper protocol is not part of the library. Is that an oversight or should I just copy the set method directly in my own project?

    opened by kaishin 2
  • Type 'Reducer<HostBroadcast.State, HostBroadcast.Event>' (aka '(inout HostBroadcast.State, HostBroadcast.Event) -> ()') has no member 'init'

    Type 'Reducer' (aka '(inout HostBroadcast.State, HostBroadcast.Event) -> ()') has no member 'init'

    I am following the SignIn example and I get the error in the static func reducer() -> Reducer<State, Event> method.

    Type 'Reducer<HostBroadcast.State, HostBroadcast.Event>' (aka '(inout HostBroadcast.State, HostBroadcast.Event) -> ()') has no member 'init'

    Screen Shot 2020-06-25 at 07 27 50

    Below is my HostBroadcastState.swift implementation.

    import Foundation
    import CombineFeedback
    import Combine
    
    enum HostBroadcast {
      
      struct State: Equatable {
        var status = Status.idle
        
        var error: NSError? {
          switch status {
            case .failed(let error):
              return error
            default:
              return nil
          }
        }
        
        enum Status: Equatable {
          case idle
          case connecting
          case connected
          case broadcasting
          case failed(NSError)
        }
      }
      
      enum Event {
        case willConnect
        case didConnect
        case didFail(NSError)
        case retry
      }
      
      static func reducer() -> Reducer<State, Event> {
        .init { state, event in  // ERROR HERE
          switch event {
            case .willConnect:
              state.status = .connecting
            case .didFail(let error):
              state.status = .failed(error)
            case .retry:
              state.status = .connecting
            case .didConnect:
              state.status = .connected
          }
        }
      }
      
    }
    

    And my HostBroadcastViewModel.swift

    import Foundation
    import Combine
    import CombineFeedback
    import CombineFeedbackUI
    
    extension HostBroadcast {
      final class HostBroadcastViewModel: Store<HostBroadcast.State, HostBroadcast.Event> {
        
        init(initial: State = State()){
          super.init(
            initial: initial,
            feedbacks: [
              
            ],
            reducer: HostBroadcast.reducer()
          )
        }
        
      }
    }
    

    Help is appreciated, thank you for the amazing work. Big fan!

    opened by xplatsolutions 1
  • Example how to integrate in my project avoiding copy-paste

    Example how to integrate in my project avoiding copy-paste

    Trying to figure out if this is installed as a git submodule, clone and copy the files (I'm sure not), any help appreciated, a newbie in Swift and I'd expect something like cocoapods. Any keywords for me to Google is appreciated too :)

    opened by xplatsolutions 1
  • Make publisher public

    Make publisher public

    Hi!

    I just added public access modifier to Store.publisher property (similar as Store.state). This allow to handle state changes in non SwiftUI projects (without adding fake Feedback for send state over CurrentValueSubject or some other hacks).

    opened by rock88 0
  • Add TestScheduler and fix System so it always emits the initial state

    Add TestScheduler and fix System so it always emits the initial state

    This PR does the following:

    • Added TestScheduler, heavily influenced by the TestScheduler from ReactiveSwift
    • Fix System so that it always emits the initial state.
    • Change tests to use the TestScheduler

    @sergdort please check this carefully before merging. Particularly the part about adding .prepend(initial) in the system.

    opened by mluisbrown 0
  • Changes for Xcode 11 beta 4

    Changes for Xcode 11 beta 4

    Makes the changes needed to support changes in Xcode 11 beta 4.

    Mainly:

    • Publishers.Empty -> Empty
    • Publishers.Just -> Just
    • Publishers.Once -> Result.Publisher

    However, the CombineFeedbackTests are failing, because it looks like the system is not emitting the initial state. OTOH, the examples seem to be working 🤷‍♀

    opened by mluisbrown 0
  • Add support for Swift Package Manager

    Add support for Swift Package Manager

    • Move source and test files to Sources and Tests directories
    • Create a Package.swift file
    • Rename a couple of instances of Signal to Publisher in the doc comments 😛

    Tested by running swift build on the command line (with the toolchain from Xcode 11 beta1) which builds fine. Also tested by integrating into a personal project using Xcode 11's support for SPM.

    opened by mluisbrown 0
  • multi screens

    multi screens

    Hi I am trying use this nice framework , but I am struggling in problem how to manage state in multi screens application , can you provide a simple example of application with some screens , and in example with signIn :

    static var feedback: Feedback<State, Event> { return Feedback.combine( whenChangingUserName(api: GithubAPI()), whenSubmitting(api: GithubAPI()) ) } why need it ? Thanks in advance.

    opened by basconje 0
  • Programmatic Navigation for MacOS

    Programmatic Navigation for MacOS

    Programmatic navigation has a dependency on NavigationView, but it has Safari-File-Picker behavior on MacOS which is not the most convenient one 🌚. I tried to find workarounds for a while so maybe this will be helpful for other MacOS developers, which will use CombineFeedback 🙂

    • Here is a draft for manual NavigationView implementation (no_animation).
    • Xcode 12 supports switch expressions, which can be handy for navigation, especially in MacOS, so I came to this solution
    opened by maximkrouk 0
  • Documentation - I can help

    Documentation - I can help

    I am having trouble to follow the framework when it comes to the Feedback context and although you can read the code, not sure how (some) feedbacks are connected to the event, it looks like there are various flavors of Feedback, some are invoked every time state change and some are not making it difficult to understand when to use what.

    Example:

    // Having the below ViewModel
    
    final class ViewModel: CombineFeedbackUI.Store<HostBroadcast.State, HostBroadcast.Event> {
        
        init(initial: State = State()){
          super.init(
            initial: initial,
            feedbacks: [
              ViewModel.whenBroadcastFinished(),
              ViewModel.whenSomethingElseHappened()
            ],
            reducer: HostBroadcast.reducer
          )
        }
        
        static func whenSomethingElseHappened() -> Feedback<State, Event> {
          return Feedback.custom { state, consumer in
            print("whenSomethingElseHappened")
            return Empty().eraseToAnyPublisher().start()
          }
        }
        
        static func whenBroadcastFinished() -> Feedback<State, Event> {
          return Feedback(effects: { (state) -> AnyPublisher<Event, Never> in
            print("\(state.status)")
            guard state.status.isBroadcastFinished else {
              print("#Broadcast not finished...")
              return Empty().eraseToAnyPublisher()
            }
            print("#Broadcast finished...")
            return Empty().eraseToAnyPublisher()
          })
        }
        
      }
    

    whenSomethingElseHappened feedback will execute when the whole view is redrawn by the system; in my case every time a tab item selected change, but not when a button action in the view or onAppear emits an event Button(action: context.action(for: .didFinishBroadcast)), .onAppear { self.context.send(event: .shouldConnect) }

    I'd appreciate it if I can further read somewhere how the Feedback lifecycle or connection to the events raised is working. I can also help with documentation.

    opened by xplatsolutions 0
  • How to pass additional @Binding parameters to the View from another parent View

    How to pass additional @Binding parameters to the View from another parent View

    I have a parent View with a TabView and a custom modifier to present a custom full modal view (doesn't exist in SwiftUI so custom work had to be done)

    struct ContentView: View {
      
      @ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 3)
      
      var body: some View {
        
        TabView(selection: $tabData.itemSelected){
          // tab items with views and tags
        }
        .present($tabData.isCustomItemSelected, // Present the modal view when isCustomItemSelected @State is true
                 view: Widget(store: HostBroadcast.ViewModel(), content: BroadcastLiveView.init), // Using CombineFeedbackUI Widget passing the viewmodel and content view
    // I need somehow to link the isPresented: $tabData.isCustomItemSelected so that I can toggle it from the BroadcastLiveView to close the modal, or if feasible signal from HostBroadcast.ViewModel the state change to MainTabBarData
                 style: .fade)
    

    My BroadcastLiveView.init is following 0.7.0 tag source code examples.

      typealias State = HostBroadcast.State
      typealias Event = HostBroadcast.Event
      
      @ObservedObject
      var context: Context<State, Event>
      
      init(context: Context<State, Event>) {
        self.context = context
        logInit(of: self)
      }
    

    I'd love to understand if this somehow can happen in ViewModel context, I have a Feedback where I check if the button to close the view tapped but I lack understanding how to tell the MainTabBarData ObservableObject that isCustomItemSelected should be false.

    opened by xplatsolutions 1
  • What is the best way for systems to communicate to each other.

    What is the best way for systems to communicate to each other.

    There are example project, it showcase how to use stores independently and as a single store. However in real life use case, usually one system need to communicate to each other:

    for example: We have an authentication_system and sign_in_system. A persist_session_event in auth system will be triggered by signed_in_event from sign_in_system.

    Can you share how to achieve that with the framework?

    opened by nielstj 1
Releases(0.6.0)
Owner
iOS @Twitter
null
UDF (Unidirectional Data Flow) architecture on SwiftUI/Combine

The license The SwiftUI-UDF stays under a dual license (email confirmation required): It can be Free for non-commercial use, public repository or star

Max Kuznetsov 13 Nov 10, 2022
Mvi Architecture for SwiftUI Apps. MVI is a unidirectional data flow architecture.

Mvi-SwiftUI If you like to read this on Medium , you can find it here MVI Architecture for SwiftUI Apps MVI Architecture Model-View-Intent (MVI) is a

null 12 Dec 7, 2022
Best architecture for SwiftUI + CombineBest architecture for SwiftUI + Combine

Best architecture for SwiftUI + Combine The content of the presentation: First of the proposed architectures - MVP + C Second of the proposed architec

Kyrylo Triskalo 3 Sep 1, 2022
Github repo search with using mvvm-c and clean architecture and using combine swift

GitSearchWithMVVM-C-CleanArchitecture Github repo search with using mvvm-c and clean architecture and using combine swift. Content Overview How To Run

Muhammad Qasim Majeed 1 Mar 16, 2022
content for Using Combine - notes on learning Combine with UIKit and SwiftUI

SwiftUI-Notes A collection of notes, project pieces, playgrounds and ideas on learning and using SwiftUI and Combine. Changes, corrections, and feedba

Joseph Heck 1.7k Dec 27, 2022
An iOS template project using SwiftUI, Combine and MVVM-C software architecture

SwiftUI-MVVM-C A template project that uses SwiftUI for UI, Combine for event handling, MVVM-C for software architecture. I have done some small proje

Huy Nguyen 107 Jan 2, 2023
Booky heavily-commented demo app built to explore Apple's new 'App Intents' framework introduced in iOS 16

Booky Demo App ℹ️ ABOUT Booky is a work-in-progress, heavily-commented demo app built to explore Apple's new 'App Intents' framework introduced in iOS

Alex Hay 77 Jan 4, 2023
🖼 Gallery App for Harvest (Elm Architecture + Optics) + SwiftUI + Combine.

?? Harvest-SwiftUI-Gallery Gallery App for Harvest (Elm Architecture + Optics) + SwiftUI + Combine. Examples Todo List Stopwatch GitHub Search TimeTra

Yasuhiro Inami 160 Oct 17, 2022
A demo app to showcase testable, modern iOS development with SwiftUI and Combine on MVVM-C architecture.

Coinz_App_iOS A demo app to showcase testable, modern iOS development with SwiftUI and Combine on MVVM-C architecture. Tech Stack: Swift, SwiftUI, Com

Burhan Aras 0 Dec 26, 2021
Anime schedule, korean subtitle for iOS with SwiftUI + Combine and MVVM architecture

AniTime Anime schedule, korean subtitle for iOS with SwiftUI + Combine and MVVM architecture I'm developing a new one for the SwiftUI life cycle annou

Kwangmin Bae 8 Mar 14, 2022
New version of CardinalKit based on the Swift Package Manager and a modularized architecture.

CardinalKit Open-source framework for rapid development of modern, interoperable digital health applications. Contributing Contributions to this proje

Stanford Biodesign for Digital Health 3 Dec 3, 2022
ScrumdingerTCA - Apple’s tutorial app recreated using The Composable Architecture

ScrumdingerTCA Apple’s tutorial app recreated using The Composable Architecture

Pat Brown 9 Nov 29, 2022
Sample iOS project built by SwiftUI + Flux and Combine framework using GitHub API

SwiftUI-Flux Flux enables us to have unidirectional data flow and make it testable. It's used to be implemented using RxSwift or ReactiveSwift in the

Yusuke Kita 87 Nov 25, 2022
Sample iOS project built by SwiftUI + MVVM and Combine framework using GitHub API

SwiftUI-MVVM One of the biggest idea for having MVVM is that most of data flow can be testable. Data binding in view layer by SwiftUI is awesome. Howe

Yusuke Kita 592 Jan 2, 2023
Demo to show Air Quality Indices of Cities (in India) using SwiftUI and Combine Framework

AirQualityMonitoring-SwiftUI-Combine Demo to show Air Quality Indices of Cities (in India) using SwiftUI and Combine Framework Demo Video City List wi

Minhaz Panara 0 Jan 23, 2022
Basic Todo list application built using the new SwiftUI framework and Core Data

Dub Dub Do - A sample TODO List Application in SwiftUI Updated for Xcode 11.5 This is a basic app that lets you create a list of todos, mark them as i

Stephen McMillan 67 Sep 28, 2022
Estudo introdutório sobre o framework Combine

Combine - POC Este projeto destina-se a fazer um estudo introdutório sobre o framework Combine. Baseado neste tutorial do Tibor Bödecs. A funcionalida

Adriano Rodrigues Vieira 0 Dec 25, 2021
Aplikasi CrypTraces adalah MacOS Widget Crypto Tracker dengan SwiftUI, Combine & Cocoa Framework, dan WebSocket & CoinCap API

Aplikasi CrypTraces adalah MacOS Widget Crypto Tracker dengan SwiftUI, Combine & Cocoa Framework, dan WebSocket & CoinCap API. Aplikasi ini berbentuk Widget di Menu Bar MacOS dengan menampilkan beberapa Crypto Currency seperti Bitcoin (BTC), Ethereum (ETH), Dogecoin (DOGE), Monero (XMR), dan Litecoin (LTC).

DK 6 Aug 1, 2022