Giter VIP home page Giter VIP logo

reactiveswift-composable-architecture's Introduction

A Reactive Swift fork of The Composable Architecture

CI

Point-Free's The Composable Architecture uses Apple's Combine framework as the basis of its Effect type.

This fork has adapted The Composable Architecture to use Reactive Swift as the basis for the Effect type. The original motivation for this was to allow using The Composable Architecture in OS versions prior to iOS 13 and macOS 10.15, which are the earliest versions that support Combine and, therefore, TCA. Linux compatibility was also added since release 0.8.1.

However, for a number of reasons, maintaining support for older Apple OSes has become impractical, so as of release 0.33.1, the minimum OS requirements are the same as that of TCA: iOS 13 and macOS 10.15. Releases 0.28.1 and earlier of course still support older OS versions.

Effect type implementations

Combine vs ReactiveSwift

In Pointfreeco's composable architecture the Effect type wraps a Combine Producer, and also conforms to the Publisher protocol. This is required due to the way that each operation on a Publisher (e.g. map) returns a new type of Publisher (Publishers.Map in the case of map), so in order to have a single Effect type these publisher types always need to be erased back to just the Effect type with eraseToEffect().

Using ReactiveSwift, which doesn't use Combine's type model, Effect<Output, Failure> is simply a typealias for SignalProducer<Value, Error>. There is never a need to type erase. Also, due to ReactiveSwift's lifetime based disposable handling, you rarely need to keep a reference to a Disposable, unlike in Combine, where you must always keep a reference to any Cancellable otherwise it will terminate immediately.

The Composable Architecture

The Composable Architecture (TCA, for short) is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS), and also on Linux.

The Composable Architecture (TCA, for short) is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).

What is the Composable Architecture?

This library provides a few core tools that can be used to build applications of varying purpose and complexity. It provides compelling stories that you can follow to solve many problems you encounter day-to-day when building applications, such as:

  • State management
    How to manage the state of your application using simple value types, and share state across many screens so that mutations in one screen can be immediately observed in another screen.

  • Composition
    How to break down large features into smaller components that can be extracted to their own, isolated modules and be easily glued back together to form the feature.

  • Side effects
    How to let certain parts of the application talk to the outside world in the most testable and understandable way possible.

  • Testing
    How to not only test a feature built in the architecture, but also write integration tests for features that have been composed of many parts, and write end-to-end tests to understand how side effects influence your application. This allows you to make strong guarantees that your business logic is running in the way you expect.

  • Ergonomics
    How to accomplish all of the above in a simple API with as few concepts and moving parts as possible.

Learn More

The Composable Architecture was designed over the course of many episodes on Point-Free, a video series exploring functional programming and the Swift language, hosted by Brandon Williams and Stephen Celis.

You can watch all of the episodes here, as well as a dedicated, multipart tour of the architecture from scratch.

video poster image

Examples

Screen shots of example applications

This repo comes with lots of examples to demonstrate how to solve common and complex problems with the Composable Architecture. Check out this directory to see them all, including:

Looking for something more substantial? Check out the source code for isowords, an iOS word search game built in SwiftUI and the Composable Architecture.

Basic Usage

To build a feature using the Composable Architecture you define some types and values that model your domain:

  • State: A type that describes the data your feature needs to perform its logic and render its UI.
  • Action: A type that represents all of the actions that can happen in your feature, such as user actions, notifications, event sources and more.
  • Reducer: A function that describes how to evolve the current state of the app to the next state given an action. The reducer is also responsible for returning any effects that should be run, such as API requests, which can be done by returning an Effect value.
  • Store: The runtime that actually drives your feature. You send all user actions to the store so that the store can run the reducer and effects, and you can observe state changes in the store so that you can update UI.

The benefits of doing this are that you will instantly unlock testability of your feature, and you will be able to break large, complex features into smaller domains that can be glued together.

As a basic example, consider a UI that shows a number along with "+" and "−" buttons that increment and decrement the number. To make things interesting, suppose there is also a button that when tapped makes an API request to fetch a random fact about that number and then displays the fact in an alert.

To implement this feature we create a new type that will house the domain and behavior of the feature by conforming to ReducerProtocol:

import ComposableArchitecture

struct Feature: ReducerProtocol {
}

In here we need to define a type for the feature's state, which consists of an integer for the current count, as well as an optional string that represents the title of the alert we want to show (optional because nil represents not showing an alert):

struct Feature: ReducerProtocol {
  struct State: Equatable {
    var count = 0
    var numberFactAlert: String?
  }
}

We also need to define a type for the feature's actions. There are the obvious actions, such as tapping the decrement button, increment button, or fact button. But there are also some slightly non-obvious ones, such as the action of the user dismissing the alert, and the action that occurs when we receive a response from the fact API request:

struct Feature: ReducerProtocol {
  struct State: Equatable {  }
  enum Action: Equatable {
    case factAlertDismissed
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse(TaskResult<String>)
  }
}

And then we implement the reduce method which is responsible for handling the actual logic and behavior for the feature. It describes how to change the current state to the next state, and describes what effects need to be executed. Some actions don't need to execute effects, and they can return .none to represent that:

struct Feature: ReducerProtocol {
  struct State: Equatable {  }
  enum Action: Equatable {}
  
  func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
    switch action {
      case .factAlertDismissed:
        state.numberFactAlert = nil
        return .none

      case .decrementButtonTapped:
        state.count -= 1
        return .none

      case .incrementButtonTapped:
        state.count += 1
        return .none

      case .numberFactButtonTapped:
        return .task { [count = state.count] in
          await .numberFactResponse(
            TaskResult {
              String(
                decoding: try await URLSession.shared
                  .data(from: URL(string: "http://numbersapi.com/\(count)/trivia")!).0,
                as: UTF8.self
              )
            }
          )
        }

      case let .numberFactResponse(.success(fact)):
        state.numberFactAlert = fact
        return .none

      case .numberFactResponse(.failure):
        state.numberFactAlert = "Could not load a number fact :("
        return .none
    }
  }
}

And then finally we define the view that displays the feature. It holds onto a StoreOf<Feature> so that it can observe all changes to the state and re-render, and we can send all user actions to the store so that state changes. We must also introduce a struct wrapper around the fact alert to make it Identifiable, which the .alert view modifier requires:

struct FeatureView: View {
  let store: StoreOf<Feature>

  var body: some View {
    WithViewStore(self.store, observe: { $0 }) { viewStore in
      VStack {
        HStack {
          Button("") { viewStore.send(.decrementButtonTapped) }
          Text("\(viewStore.count)")
          Button("+") { viewStore.send(.incrementButtonTapped) }
        }

        Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
      }
      .alert(
        item: viewStore.binding(
          get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
          send: .factAlertDismissed
        ),
        content: { Alert(title: Text($0.title)) }
      )
    }
  }
}

struct FactAlert: Identifiable {
  var title: String
  var id: String { self.title }
}

It is also straightforward to have a UIKit controller driven off of this store. You subscribe to the store in viewDidLoad in order to update the UI and show alerts. The code is a bit longer than the SwiftUI version, so we have collapsed it here:

Click to expand!
class FeatureViewController: UIViewController {
  let viewStore: ViewStoreOf<Feature>

  init(store: StoreOf<Feature>) {
    self.viewStore = ViewStore(store)
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    let countLabel = UILabel()
    let incrementButton = UIButton()
    let decrementButton = UIButton()
    let factButton = UIButton()

    // Omitted: Add subviews and set up constraints...

    self.viewStore.produced
      .map { "\($0.count)" }
      .assign(to: \.text, on: countLabel)

    self.viewStore.produced.numberFactAlert
      .startWithValues { [weak self] numberFactAlert in
        let alertController = UIAlertController(
          title: numberFactAlert, message: nil, preferredStyle: .alert
        )
        alertController.addAction(
          UIAlertAction(
            title: "Ok",
            style: .default,
            handler: { _ in self?.viewStore.send(.factAlertDismissed) }
          )
        )
        self?.present(alertController, animated: true, completion: nil)
      }
  }

  @objc private func incrementButtonTapped() {
    self.viewStore.send(.incrementButtonTapped)
  }
  @objc private func decrementButtonTapped() {
    self.viewStore.send(.decrementButtonTapped)
  }
  @objc private func factButtonTapped() {
    self.viewStore.send(.numberFactButtonTapped)
  }
}

Once we are ready to display this view, for example in the app's entry point, we can construct a store. This can be done by specifying the initial state to start the application in, as well as the reducer that will power the application:

import ComposableArchitecture

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      FeatureView(
        store: Store(
          initialState: Feature.State(),
          reducer: Feature()
        )
      )
    }
  }
}

And that is enough to get something on the screen to play around with. It's definitely a few more steps than if you were to do this in a vanilla SwiftUI way, but there are a few benefits. It gives us a consistent manner to apply state mutations, instead of scattering logic in some observable objects and in various action closures of UI components. It also gives us a concise way of expressing side effects. And we can immediately test this logic, including the effects, without doing much additional work.

Testing

For more in-depth information on testing, see the dedicated testing article.

To test use a TestStore, which can be created with the same information as the Store, but it does extra work to allow you to assert how your feature evolves as actions are sent:

@MainActor
func testFeature() async {
  let store = TestStore(
    initialState: Feature.State(),
    reducer: Feature()
  )
}

Once the test store is created we can use it to make an assertion of an entire user flow of steps. Each step of the way we need to prove that state changed how we expect. For example, we can simulate the user flow of tapping on the increment and decrement buttons:

// Test that tapping on the increment/decrement buttons changes the count
await store.send(.incrementButtonTapped) {
  $0.count = 1
}
await store.send(.decrementButtonTapped) {
  $0.count = 0
}

Further, if a step causes an effect to be executed, which feeds data back into the store, we must assert on that. For example, if we simulate the user tapping on the fact button we expect to receive a fact response back with the fact, which then causes the alert to show:

await store.send(.numberFactButtonTapped)

await store.receive(.numberFactResponse(.success(???))) {
  $0.numberFactAlert = ???
}

However, how do we know what fact is going to be sent back to us?

Currently our reducer is using an effect that reaches out into the real world to hit an API server, and that means we have no way to control its behavior. We are at the whims of our internet connectivity and the availability of the API server in order to write this test.

It would be better for this dependency to be passed to the reducer so that we can use a live dependency when running the application on a device, but use a mocked dependency for tests. We can do this by adding a property to the Feature reducer:

struct Feature: ReducerProtocol {
  let numberFact: (Int) async throws -> String}

Then we can use it in the reduce implementation:

case .numberFactButtonTapped:
  return .task { [count = state.count] in 
    await .numberFactResponse(TaskResult { try await self.numberFact(count) })
  }

And in the entry point of the application we can provide a version of the dependency that actually interacts with the real world API server:

@main
struct MyApp: App {
  var body: some Scene {
    FeatureView(
      store: Store(
        initialState: Feature.State(),
        reducer: Feature(
          numberFact: { number in
            let (data, _) = try await URLSession.shared
              .data(from: .init(string: "http://numbersapi.com/\(number)")!)
            return String(decoding: data, as: UTF8.self)
          }
        )
      )
    )
  }
}

But in tests we can use a mock dependency that immediately returns a deterministic, predictable fact:

@MainActor
func testFeature() async {
  let store = TestStore(
    initialState: Feature.State(),
    reducer: Feature(
      numberFact: { "\($0) is a good number Brent" }
    )
  )
}

With that little bit of upfront work we can finish the test by simulating the user tapping on the fact button, receiving the response from the dependency to trigger the alert, and then dismissing the alert:

await store.send(.numberFactButtonTapped)

await store.receive(.numberFactResponse(.success("0 is a good number Brent"))) {
  $0.numberFactAlert = "0 is a good number Brent"
}

await store.send(.factAlertDismissed) {
  $0.numberFactAlert = nil
}

We can also improve the ergonomics of using the numberFact dependency in our application. Over time the application may evolve into many features, and some of those features may also want access to numberFact, and explicitly passing it through all layers can get annoying. There is a process you can follow to “register” dependencies with the library, making them instantly available to any layer in the application.

For more in-depth information on dependency management, see the dedicated dependencies article.

We can start by wrapping the number fact functionality in a new type:

struct NumberFactClient {
  var fetch: (Int) async throws -> String
}

And then registering that type with the dependency management system by conforming the client to the DependencyKey protocol, which requires you to specify the live value to use when running the application in simulators or devices:

extension NumberFactClient: DependencyKey {
  static let liveValue = Self(
    fetch: { number in
      let (data, _) = try await URLSession.shared
        .data(from: .init(string: "http://numbersapi.com/\(number)")!)
      return String(decoding: data, as: UTF8.self)
    }
  )
}

extension DependencyValues {
  var numberFact: NumberFactClient {
    get { self[NumberFactClient.self] }
    set { self[NumberFactClient.self] = newValue }
  }
}

With that little bit of upfront work done you can instantly start making use of the dependency in any feature:

struct Feature: ReducerProtocol {
  struct State {  }
  enum Action {}
  @Dependency(\.numberFact) var numberFact
  
}

This code works exactly as it did before, but you no longer have to explicitly pass the dependency when constructing the feature's reducer. When running the app in previews, the simulator or on a device, the live dependency will be provided to the reducer, and in tests the test dependency will be provided.

This means the entry point to the application no longer needs to construct dependencies:

@main
struct MyApp: App {
  var body: some Scene {
    FeatureView(
      store: Store(
        initialState: Feature.State(),
        reducer: Feature()
      )
    )
  }
}

And the test store can be constructed without specifying any dependencies, but you can still override any dependency you need to for the purpose of the test:

let store = TestStore(
  initialState: Feature.State(),
  reducer: Feature()
) {
  $0.numberFact.fetch = { "\($0) is a good number Brent" }
}

That is the basics of building and testing a feature in the Composable Architecture. There are a lot more things to be explored, such as composition, modularity, adaptability, and complex effects. The Examples directory has a bunch of projects to explore to see more advanced usages.

Documentation

The documentation for releases and main are available here:

Other versions

There are a number of articles in the documentation that you may find helpful as you become more comfortable with the library:

Installation

You can add ComposableArchitecture to an Xcode project by adding it as a package dependency.

  1. From the File menu, select Add Packages...
  2. Enter "https://github.com/pointfreeco/swift-composable-architecture" into the package repository URL text field
  3. Depending on how your project is structured:
    • If you have a single application target that needs access to the library, then add ComposableArchitecture directly to your application.
    • If you want to use this library from multiple Xcode targets, or mix Xcode targets and SPM targets, you must create a shared framework that depends on ComposableArchitecture and then depend on that framework in all of your targets. For an example of this, check out the Tic-Tac-Toe demo application, which splits lots of features into modules and consumes the static library in this fashion using the tic-tac-toe Swift package.

Help

If you want to discuss the Composable Architecture or have a question about how to use it to solve a particular problem, you can start a topic in the discussions tab of this repo, or ask around on its Swift forum.

Translations

The following translations of this README have been contributed by members of the community:

If you'd like to contribute a translation, please open a PR with a link to a Gist!

FAQ

  • How does the Composable Architecture compare to Elm, Redux, and others?

    Expand to see answer The Composable Architecture (TCA) is built on a foundation of ideas popularized by the Elm Architecture (TEA) and Redux, but made to feel at home in the Swift language and on Apple's platforms.

    In some ways TCA is a little more opinionated than the other libraries. For example, Redux is not prescriptive with how one executes side effects, but TCA requires all side effects to be modeled in the Effect type and returned from the reducer.

    In other ways TCA is a little more lax than the other libraries. For example, Elm controls what kinds of effects can be created via the Cmd type, but TCA allows an escape hatch to any kind of effect since Effect conforms to the Combine Publisher protocol.

    And then there are certain things that TCA prioritizes highly that are not points of focus for Redux, Elm, or most other libraries. For example, composition is very important aspect of TCA, which is the process of breaking down large features into smaller units that can be glued together. This is accomplished with reducer builders and operators like Scope, and it aids in handling complex features as well as modularization for a better-isolated code base and improved compile times.

Credits and thanks

The following people gave feedback on the library at its early stages and helped make the library what it is today:

Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr Šíma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, and all of the Point-Free subscribers 😁.

Special thanks to Chris Liscio who helped us work through many strange SwiftUI quirks and helped refine the final API.

And thanks to Shai Mishali and the CombineCommunity project, from which we took their implementation of Publishers.Create, which we use in Effect to help bridge delegate and callback-based APIs, making it much easier to interface with 3rd party frameworks.

Other libraries

The Composable Architecture was built on a foundation of ideas started by other libraries, in particular Elm and Redux.

There are also many architecture libraries in the Swift and iOS community. Each one of these has their own set of priorities and trade-offs that differ from the Composable Architecture.

License

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

reactiveswift-composable-architecture's People

Contributors

alexito4 avatar andreyz avatar filblue avatar finestructure avatar iampatbrown avatar jager-yoo avatar jasdev avatar jeffersonsetiawan avatar kaandedeoglu avatar kalupas226 avatar kgrigsby59 avatar konomae avatar mackoj avatar maximkrouk avatar mbrandonw avatar mluisbrown avatar nmccann avatar ollitapa avatar omaralbeik avatar onevcat avatar p4checo avatar peterkovacs avatar pitt500 avatar rono23 avatar seviocorrea avatar stephencelis avatar tgrapperon avatar ts avatar wendyliga avatar yimajo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

reactiveswift-composable-architecture's Issues

Multiple calls to store.ifLet

Thanks for the hard work in making this fork! I'm currently noticing an issue with calling ifLet on a Store. I have a test project that I've been messing around with to get a good feel for using this fork. I can share the test project if needed.

The issue is that I have two modals that are presented from the root view controller. Both have optional states on the root State. When presenting/dismissing the modals one after the other, I'm noticing that the ifiLet call on the store is called multiple times. This doesn't happen the first time, only after dismissing a modal once or twice, and then it happens every time after that.

Here's what my root state looks like:

struct State {
    var items: ItemsState?
    var favorites: FavoritesState?
    ...
}

I'm presenting the view controllers with the following:

let store = self.store.scope(state: \.favorites, action: Root.Action.favorites)
store.ifLet { [weak self] store in
    guard let self = self else { return }
    let vc = FavoritesViewController(store: store)
    let nc = UINavigationController(rootViewController: vc)
    nc.presentationController?.delegate = self
    self.present(nc, animated: true, completion: nil)
}

I was able to fix my issue by creating another variant of ifLet. The "fix" is to use skipRepeats() instead of .skipRepeats { ($0 != nil) == ($1 != nil) }.

@discardableResult
public func ifLet2<Wrapped: Equatable>(
    then unwrap: @escaping (Store<Wrapped, Action>) -> Void,
    else: @escaping () -> Void
) -> Disposable where State == Wrapped? {
    let elseDisposable =
        self
        .producerScope(
            state: { state -> Effect<Wrapped?, Never> in
                state
                    .skipRepeats()
            }
        )
        .startWithValues { store in
            if ViewStore(store).state == nil { `else`() }
        }

    let unwrapDisposable =
        self
        .producerScope(
            state: { state -> Effect<Wrapped, Never> in
                state
                    .skipRepeats()
                    .compactMap { $0 }
            }
        )
        .startWithValues(unwrap)

    return CompositeDisposable([elseDisposable, unwrapDisposable])
}

I don't want to break something else by doing this though, so I'm curious what I might be missing? I'll try digging in to this a bit more if I get the time.

Object get deallocated on Signal.Observer

This error happen on device not simulator, it happens before pushing a new controller on IfLet.
Could anyone please help me get out of this ?
iOS version : 14.5
Xcode 12.5

Screenshot 2021-05-11 at 13 22 19

Include TCA Swift Concurrency changes from release 0.39.0

TCA just released version 0.39.0 which includes all the Swift Concurrency changes, and fundamentally changes many internals of the library. For example, Effect no longer conforms to Publisher but instead holds on to a Publisher internally.

The changes encourage moving away from Combine based effects to effects that use Swift Concurrency using Effect.task.

I've been able to maintain this fork in sync with TCA up to the last commit prior to the merge of the Swift Concurrency PR, however I feel that trying to update the fork to include the Swift Concurrency changes would be a huge task, and I don't have the necessary time to dedicate to it.

There's also the question of how useful this fork will continue to be. The objective for @mbrandonw and @stephencelis with TCA is clearly (and rightly) to be able to remove Combine completely from TCA sooner or later, at which point the need for a Reactive Swift fork really ceases to exist. Even now, this fork has a minimum deployment target of iOS 13 so the only use cases for it are:

  • existing projects using it which haven't yet migrated to Combine TCA
  • projects that don't want to use Combine as it's not open source
  • projects using non-Apple OSes like Linux and Android

If anyone is interested in trying to migrate this fork to the new changes, I'd be happy to help out where I can.

Change TestStore so that it isn't only compiled when running in DEBUG

On one of the projects I work on, we are forced to pull in CA manually. This is because we have a build configuration for testing that isn't DEBUG. However, this means that we can not use TestStore since that is guarded by DEBUG. It would be nice to be able to use TestStore regardless of project configuration.

Would it be possible to remove the #if DEBUG here?

Nested properties keyPath support for Produced values

I'd like to add support for the nested properties in Produced values, current implementation use
@dynamicMemberLookup, which does not support nested properties yet, and I have 2.5 solutions:

Zero (not a real one, I mean 😅)
Use

viewStore.property.map(\.nestedProperty).skipRepeats()

which is okay, but not the most convenient stuff.

First
Add another method to the ViewStore type to support default KeyPath stuff:

viewStore.producer(for: \.KeyPath<State, Value>) -> Produced<Value>

That extremely easy and fits ReactiveSwift, but I'm not sure if it fits ComposableArchitecture.
Maybe it worth mentioning in readme or smth if approved.
I use it locally now btw.

Second
Use Produced type as a dynamicMemberLookup container and do not return a producer implicitly, but return mapped itself.

/// A producer of store state.
@dynamicMemberLookup
public struct Produced<Value> { // <- have to drop `SignalProducerConvertible` protocol
    private let _producer: Effect<Value, Never> // <- private stuff
    
    init(by upstream: Effect<Value, Never>) {
        self._producer = upstream
    }
    
    /// Returns the resulting producer of a given key path.
    public subscript<LocalValue>(
        dynamicMember keyPath: KeyPath<Value, LocalValue>
    ) -> Produced<LocalValue> {
        Produced<LocalValue>(by: _producer.map(keyPath)) // <- here is a change
    }
}

extension Produced {
    public var producer: Effect<Value, Never> { _producer } // <- Logic inconsistency {li}
}

extension Produced where Value: Equatable {
    public var producer: Effect<Value, Never> { _producer.skipRepeats() } // <- Logic inconsistency {li}
}

Usage:

viewStore.property.nestedProperty.producer

That will lead to more consistent API, but the implementation will be major so that users won't be able to subscribe to any property without explicitly calling the producer anymore. Also, there is a little logic inconsistency at {li} marks due to Equatable conformance, that should be tested and anyways it may suffer from type erasure stuff.

ViewStore version without Combine/SwiftUI

Hi,

First of all, thank you for doing an amazing job in creating this version of the library. It's been of a lot of help.

At my workplace we're still required to support iOS 12, which means that we can't use Combine/SwiftUI yet.
I wanted to integrate this version, as it's based on ReactiveSwift which can run on older version, but to my surprise, parts of it are still using Combine.

Is there any option to remove dependency from ViewStore to Combine.
It's conforming to ObservableObject(least of all) which makes it impossible to use in older version.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.