Replies: 30 comments 133 replies
-
Wow, this looks amazing! I've been a bit on the fence of whether I want to completely dive in and fully use TCA, but all this almost completely negates all my reservations I've had with it. Mainly the file scoped reducers straining the compiler so much. Can't wait to give this a try. |
Beta Was this translation helpful? Give feedback.
-
Have you considered Defaulting preview dependencies to live sounds rather crash-prone, given how many live APIs are incompatible with Xcode Previews still. |
Beta Was this translation helpful? Give feedback.
-
If you happen to be using |
Beta Was this translation helpful? Give feedback.
-
OK, wow. This is excellent and the timing couldn't be better. I was just discussing the issue of dependency injection with a colleague on a sort of green field experimental project we're working on. We'll be switching onto this Beta branch for this. (Obvs aware that it may not be stable). But so far everything is switched over including my custom "higher order reducer" which now has even fewer constraints due to the fact that it can bring in the dependency itself rather than relying on the Environment of the Reducer it is wrapping. Amazing 😄 |
Beta Was this translation helpful? Give feedback.
-
I love this, thank you! I just watched today's episode and then found this discussion. Minor question... did you consider |
Beta Was this translation helpful? Give feedback.
-
I like Scope a lot. It makes it very clean to create container reducer.
But noticed that if we have mixed reducer then need to use .ifCaseLet even if we don't have optional state, this means marking state optional. Unless I'm missing something, Scope can't be used with mixed reducer.
|
Beta Was this translation helpful? Give feedback.
-
Love the Super happy about The design of the
public protocol DependencyKey {
/// The associated type representing the type of the dependency key's
/// value.
associatedtype Value
/// The default value for the dependency key.
static var defaultValue: Value { get }
}
Using the getter of the public struct DependencyValues {
// ... omitted for brevity
public subscript<K>(key: K.Type) -> K.Value where K: DependencyKey {
get {
let key = ObjectIdentifier(key)
// If we have stored value for the given key, return and exit.
if let value = storage[key] as? K.Value {
return value
}
#DEBUG
// If app is running in testing mode fail the test.
if ProcessInfo.Arguments.isTesting {
// We are here... it means that dependency value was not explicitly set.
// We should fail the test case and ask the user to explicitly provide the dependency.
let dependencyName = "\"\(K.Value.self)\""
let example = #"DependencyValues[\.pasteboard] = .stub"#
XCTFail("You are trying to access \(dependencyName) dependency in this test case without assigning it. You can set this dependency as: \(example)")
return K.defaultValue
}
#endif
// If app is running in normal mode, return the `default` value.
return K.defaultValue
}
set {
storage[ObjectIdentifier(key)] = newValue
}
}
} Few benefits of this approach:
Custom Rule test_case_superclass:
included: ".*.swift"
name: "Invalid XCTestCase use"
regex: "XCTestCase"
message: "Please \"import XCTestSupport\" and change the superclass to \": TestCase\" to ensure all of the dependencies are correctly set to failing before each test case."
severity: error // swiftlint:disable:next test_case_superclass
open class TestCase: XCTestCase {
open override func setUp() {
super.setUp()
DependencyValues.resetAll()
}
} Here is the current implementation of dependency and friends in its entiretyDependencyKey// MARK: - DependencyKey
/// A key for accessing values in the ``DependencyValues`` container.
///
/// You can create custom dependency values by extending the
/// ``DependencyValues`` structure with new properties. First declare a new
/// dependency key type and specify a value for the required ``defaultValue``
/// property:
///
/// ```swift
/// private struct MyDependencyKey: DependencyKey {
/// static let defaultValue: String = "Default value"
/// }
/// ```
///
/// The Swift compiler automatically infers the associated ``Value`` type as the
/// type you specify for the default value. Then use the key to define a new
/// dependency value property:
///
/// ```swift
/// extension DependencyValues {
/// var myCustomValue: String {
/// get { self[MyDependencyKey.self] }
/// set { self[MyDependencyKey.self] = newValue }
/// }
/// }
///
/// class MyTests: TestCase {
/// // Reset all of the `DependencyValues` storage.
/// func setUp() async throws {
/// DependencyValues.resetAll()
/// }
///
/// func testPasteboard() throws {
/// struct ViewModel {
/// @Dependency(\.myCustomValue) var myCustomValue
/// }
///
/// let viewModel = ViewModel()
/// XCTAssertEqual(viewModel.myCustomValue, "Failing value")
///
/// DependencyValues[\.myCustomValue] = "hello"
/// XCTAssertEqual(viewModel.myCustomValue, "hello")
/// }
/// }
/// ```
public protocol DependencyKey {
/// The associated type representing the type of the dependency key's
/// value.
associatedtype Value
/// The default value for the dependency key.
static var defaultValue: Value { get }
} Dependency// MARK: - Dependency
/// A property wrapper that reads a value from ``DependencyValues`` container.
///
/// Use the `Dependency` property wrapper to read the current value stored in
/// ``DependencyValues`` container. For example, you can create a property that
/// reads the pasteboard using the key path of the
/// ``DependencyValues/myCustomValue`` property:
///
/// ```swift
/// @Dependency(\.myCustomValue) var myCustomValue
/// ```
@propertyWrapper
public struct Dependency<Value> {
private let keyPath: KeyPath<DependencyValues, Value>
/// Creates an dependency property to read the specified key path.
///
/// Don’t call this initializer directly. Instead, declare a property
/// with the ``Dependency`` property wrapper, and provide the key path of
/// the dependency value that the property should reflect:
///
/// ```swift
/// struct MyViewModel {
/// @Dependency(\.myCustomValue) var myCustomValue
///
/// // ...
/// }
/// ```
///
/// - Parameter keyPath: A key path to a specific resulting value.
public init(_ keyPath: KeyPath<DependencyValues, Value>) {
self.keyPath = keyPath
}
/// The current value of the dependency property.
///
/// The wrapped value property provides primary access to the value's data.
/// However, you don't access `wrappedValue` directly. Instead, you read the
/// property variable created with the ``Dependency`` property wrapper:
///
/// ```swift
/// @Dependency(\.myCustomValue) var myCustomValue
///
/// func copy() {
/// print(myCustomValue) // prints "Default value"
/// }
/// ```
public var wrappedValue: Value {
DependencyValues[keyPath]
}
} DependencyValues// MARK: - DependencyValues
/// A collection of dependency values propagated using ``dependency`` property
/// wrapper.
///
/// A collection of values are exposed to your app's in an ``DependencyValues``
/// structure. To read a value from the structure, declare a property using the
/// ``Dependency`` property wrapper and specify the value's key path. For
/// example, you can read the current myCustomValue:
///
/// ```swift
/// @Dependency(\.myCustomValue) var myCustomValue
/// ```
public struct DependencyValues {
static var shared = Self()
var storage: [ObjectIdentifier: Any] = [:]
// Subscripts setter on key-path calls getter of the destination property.
// https://bugs.swift.org/browse/SR-10203
private var isSetterOnKeyPathCalled = false
/// Accesses the dependency value associated with a custom key.
///
/// Create custom dependency values by defining a key that conforms to the
/// ``DependencyKey`` protocol, and then using that key with the subscript
/// operator of the ``DependencyValues`` structure to get and set a value for
/// that key:
///
/// ```swift
/// extension DependencyValues {
/// private struct MyDependencyKey: DependencyKey {
/// static let defaultValue: String = "Default value"
/// }
///
/// var myCustomValue: String {
/// get { self[MyDependencyKey.self] }
/// set { self[MyDependencyKey.self] = newValue }
/// }
/// }
/// ```
public subscript<K>(key: K.Type) -> K.Value where K: DependencyKey {
get {
let key = ObjectIdentifier(key)
// If we have stored value for the given key, return and exit.
if let value = storage[key] as? K.Value {
return value
}
#if DEBUG
// When we are calling set key path using
// `DependencyValues[\.myCustomValue] = "hello"` it invokes the getter first
// and in testing mode it incorrectly fails the tests when trying to set the
// dependency explicitly.
// See: https://bugs.swift.org/browse/SR-10203
if !isSetterOnKeyPathCalled {
// If app is running in testing mode fail the test.
if ProcessInfo.Arguments.isTesting {
let dependencyName = "\"\(K.Value.self)\""
let example = #"DependencyValues[\.pasteboard] = .stub"#
internal_XCTFail("You are trying to access \(dependencyName) dependency in this test case without assigning it. You can set this dependency as: \(example)")
return K.defaultValue
}
}
#endif
// If app is running in normal mode, return the `default` value.
return K.defaultValue
}
set {
storage[ObjectIdentifier(key)] = newValue
}
}
/// Accesses the dependency value associated with a key path.
public static subscript<T>(_ keyPath: KeyPath<Self, T>) -> T {
shared[keyPath: keyPath]
}
/// Accesses or updates the dependency value associated with a key path.
public static subscript<T>(_ keyPath: WritableKeyPath<Self, T>) -> T {
get { shared[keyPath: keyPath] }
set {
shared.isSetterOnKeyPathCalled = true
shared[keyPath: keyPath] = newValue
shared.isSetterOnKeyPathCalled = false
}
}
/// Reset all of the ``DependencyValues`` storage.
public static func resetAll() {
// We are clearing all of the dependencies, which will force next access to go
// through the `getter` in the `subscript` function above.
shared.storage = [:]
}
} Dependency View for Previews// MARK: - Dependency View
extension View {
/// Sets the dependency value of the specified key path to the given value.
///
/// Use this modifier in **previews** to set one of the writable properties of
/// the ``DependencyValues`` structure, including custom values that you create.
/// For example, you can set the value associated with the
/// ``DependencyValues/pasteboard`` key:
///
/// ```swift
/// struct Profile_Previews: PreviewProvider {
/// static var previews: some View {
/// ProfileView()
/// .dependency(\.pasteboard, .live)
/// }
/// }
/// ```
///
/// - Parameters:
/// - keyPath: A key path that indicates the property of the
/// ``DependencyValues`` structure to update.
/// - value: The new value to set for the item specified by `keyPath`.
///
/// - Returns: A view that has the given value set in its dependency.
public func dependency<V>(_ keyPath: WritableKeyPath<DependencyValues, V>, _ value: V) -> some View {
DependencyWriter(content: self) { values in
values[keyPath] = value
}
}
}
/// A view that writes dependency.
///
/// ```swift
/// DependencyWriter { values in
/// values[\.pasteboard] = .live
/// }
/// ```
private struct DependencyWriter<Content>: View where Content: View {
let body: Content
init(
content: @autoclosure @escaping () -> Content,
write: (DependencyValues.Type) -> Void
) {
self.body = content()
write(DependencyValues.self)
}
} |
Beta Was this translation helpful? Give feedback.
-
I'm curious how you can use the @dependency system for dependencies that are not static and that require some dependencies of their own in order to be constructed. For example a I have started integrating TCA into some leaf nodes of my app and putting those features into modules. Consequently I have a huge "outside world" where my API clients and the rest of my app lives and my feature's initial State, Environment, and Store would be constructed by some non-TCA code. My I'm not sure how to build my |
Beta Was this translation helpful? Give feedback.
-
This looks like a great move towards a cleaner API. You mentioned this approach will have performance gains around the number of stack frames and stack size, our biggest performance issue we've had using TCA in production is send recursion when you have a large app with lot's of features and child features. Will this move to protocols have a positive effect on the issue of send recursion? |
Beta Was this translation helpful? Give feedback.
-
Great stuff! I'm looking at updating a project for the beta, and I'm a bit confused about migrating some stuff. The migration guide has the following suggestion for migrating over a single reducer from the old to new style:
should become:
This works for the specific case in the guide, but there don't seem to be any affordances for optional pullbacks here. As far as I can tell the only way to get optionals working is this style, which requires both an empty reducer and for you to explicitly ignore the AnyReducer's argument using an underscore (In my case I have updated the dependencies to use
Am I missing something here, or is this the recommended approach? |
Beta Was this translation helpful? Give feedback.
-
Didn't have time to test this in my project (and great work overall!) but there's already one thing that throws me off: @Dependency(\.date) var date The naming here implies that I will get a Thus, I believe a more explicit naming here would be a better choice considering that long-term using @Dependency(\.nowDate) var nowDate Or even just: @Dependency(\.now) var now |
Beta Was this translation helpful? Give feedback.
-
I'm trying to implement a BindingReducer() in the beta branch following the docs here... https://github.com/pointfreeco/swift-composable-architecture/blob/protocol-beta/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift But I'm getting two errors.
When I try to fix it it adds an empty reduce function.
Not sure if I've missed something somewhere though? Thanks |
Beta Was this translation helpful? Give feedback.
-
Thanks to everyone for all their great questions and feedback! This thread is getting a little more popular than we expected, so we have now created a dedicated beta category in GitHub discussions. Feel free to start a new topic there in order to get more visibility to your questions. 🤠 |
Beta Was this translation helpful? Give feedback.
-
Haven't started using this but it already sounds exciting! |
Beta Was this translation helpful? Give feedback.
-
This is AMAZING!!! I was so excited to learn of the features, I spoiled it all for myself so I can start integrating this functionality to my app right away :) I have a question. I'm trying to figure out how to migrate the ReducerProtocol changes to a UIViewController with a ViewState object:
Is it just a matter of creating a
It's probably as easy as that, but I don't know if I'm missing something :) |
Beta Was this translation helpful? Give feedback.
-
Now that dependencies can be added to a specific module using keys and the @dependency decorator, we no longer have to pass dependencies down a chain like before. My question however is whether this means we are inherently choosing to add some duplicate code if you have a situation where you have two modules that use the same dependency but are not totally related. Let's say for instance that you have a Home, Settings1, and Settings2 module. Let's also say I have a dependency deviceOrientation: () -> UIDeviceOrientation that I want to use in both Settings1 and Settings2. The way it worked before is that when you're instantiating the Settings environment from Home, it would've inherited the dependency at that point. However, there is no longer a "source of truth" because the boilerplate setup is quite annoying. We can't just define these dependencies in Home because if Home depends on Settings, you would introduce a circular dependency. I suppose you can create a new module specifically for dependencies and that would address this problem. In the examples though, I got the impression that these keys are defined in the module that uses the dependency. Is it implied that developers would prefer duplicate key code setup over the boilerplate chaining that was occurring before, or is there a way that I'm missing that would allow users to only setup the DependencyKey once, but would work on two modules that themselves don't have any direct or indirect dependency on each other? I imagine the answer is "Create a dependency module so it acts as source of truth", but I wanted to ask and make sure 🙂 |
Beta Was this translation helpful? Give feedback.
-
Is there a way to have |
Beta Was this translation helpful? Give feedback.
-
The migration article link appears to be dead |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Reducible doesn’t really make sense - the confirming type is not the
thing that is being reduced, it’s the state that is being reduced.
The conforming type is the thing that is capable of doing the reducing
therefore Reducer accurately describes this capability. I’m not sure
there’s a lot of benefit in trying to change a grammatically correct
term of art.
On September 13, 2022, GitHub ***@***.***> wrote:
How about Reducible? This goes along with what @lukeredpath
<https://github.com/lukeredpath> explained, which makes perfect sense.
However, it also makes it sound less of a type and more of a
capability (potentially added to a type or a component). It’s also
consistent with Swift’s native protocol naming like Codable.
—
Reply to this email directly, view it on GitHub
<https://github.com/pointfreeco/swift-composable-
architecture/discussions/1282#discussioncomment-3635178>, or unsubscribe
<https://github.com/notifications/unsubscribe-
auth/AAAAEZLMD5UG3F55HZ5JB7LV6B2XLANCNFSM57ILRSLQ>.
You are receiving this because you were mentioned.Message ID:
<pointfreeco/swift-composable-architecture/repo-
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
Wooow, this is one of the best things to happen to TCA! Thank you @mbrandonw and @stephencelis 👏🏼👏🏼👏🏼 Sometimes I have views that do not send any action, I still create a reducer with a I was wondering if it would make sense to add an extension to provide an empty reducer in this case: public extension ReducerProtocol where Action == Never {
func reduce(
into state: inout State,
action: Action
) -> Effect<Action, Never> {}
} So the reducer becomes struct Row: ReducerProtocol {
struct State {
// ...
}
typealias Action = Never
} What do you think? |
Beta Was this translation helpful? Give feedback.
-
@stephencelis @mbrandonw Great job with ReducerProtocol "beta" (as always), I've tried it in a small example app - and I love it! TL;DR: yar or nay on writing a new app using ReducerProtocol? How mature is it in your mind? I've have begun writing a large and rather complex app using TCA, with structured concurrency migration in place, have progress around 20% of MVP features. I would like to avoid finish the app app in a couple of months only to discover that ReducerProtocol is no longer in beta and is the proposed standard, and then have to rewrite 100% of the Reducers/Features. So my question is, how confident are you that you have "nailed everything" with ReducerProtocol, over 80% confident? :D |
Beta Was this translation helpful? Give feedback.
-
I started migrating to The driver was to redesign some of my internal clients in order to break the core logic into separate libraries. Doing so with the current environment passing method felt overwhelming. Moving everything to Migrating to
Store(
initialState: EditorRootState(),
reducer: Reduce(
EditorRootReducer,
environment: .init()
)
.dependencies {
// Dependencies that need to be initialized manually instead of via `liveValue`
$0.libraryClient = libraryClient
}
) Updating tests:
A few observations:
On that last point, I currently have mixed feelings about default Thinking out loud... on the other hand, I have a feeling that the inability to define a static |
Beta Was this translation helpful? Give feedback.
-
Chipping in with a small question: does it make sense the whole dependency management logic to be added into a separate library? |
Beta Was this translation helpful? Give feedback.
-
Has task cancellation been gone over with the protocol change? I'm wondering how to implement without use of the .cancellable func on |
Beta Was this translation helpful? Give feedback.
-
One question about changing dependencies. I want to add a Demo-Client which can be toggled on or off. This means, in my Settings-Features I have a state struct MultiplatformApp: ReducerProtocol {
struct State: Equatable {
var demoMode: Bool = false
var feature: Feature.State
}
enum Action: Equatable {
case toggleDemoMode
case feature(Feature.Action)
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .toggleDemoMode:
state.demoMode.toggle()
return .none
default:
return .none
}
}
Scope(state: \.feature, action: /Action.feature) {
Feature()
}
.dependency(\.apiClient, /* .demo or .live */ )
}
} This example will also only affect the child features, but in my case, the whole app should use the demo-client. |
Beta Was this translation helpful? Give feedback.
-
Hi, I'd like to ask how can I make dependency not shared between 2 app sessions? For instance I open the app in split view, currently they will share and use the same live dependencies, hence some state I stored in the live dependencies are shared among the 2 apps, which is not what we intend to have. We want to have app session (iOS app) / window session (macOS app) specific dependencies so that one does not affect the other. How could we achieve this using the new dependency system? |
Beta Was this translation helpful? Give feedback.
-
This link is broken now as the filename has changed to Updated link: |
Beta Was this translation helpful? Give feedback.
-
Hello everyone!
We have kicked off a brand new series of episodes and we’d like to start a beta period for the changes to the library like we did for the concurrency tools. We believe this release is an even bigger one than the concurrency release, and will make writing and composing features in the Composable Architecture as friendly and intuitive as building interfaces in SwiftUI.
Although we have been working with the APIs for many, many months, we'd like to give the community some time to test things out and let us know of any problems.
All of these changes should be 100% backwards compatible, and so if you experience any compiler errors please let us know. Some aspects of the new tools introduced in this release only work in Swift 5.7, but we’ve still tried to make the experience for 5.6 as nice as possible. This should make it easier to slowly migrate your application without forcing you to be on the newest Xcode or Swift.
ReducerProtocol
There is a new way to create reducers in the library, and that is by conforming types to the
ReducerProtocol
. So, where you previously would do something like this:…you will now do something like this:
This style of reducer comes with a number of benefits:
…and a whole lot more.
Further, the
ReducerProtocol
gives us a natural place to express the composition of many reducers. If your feature composes many reducers together, such as the root-level reducer that combines reducers for each tab of an application, then previously you would express this using thecombine
andpullback
operators:For brevity we have omitted the transformations for turning the app environment into an activity, profile or settings environment, but suffice it to say that it typically requires quite a few lines.
This composition can now be expressed with the
ReducerProtocol
by implementing abody
requirement, which makes use of result builder syntax to provide a concise API for composing many reducers together in a way that should feel familiar to SwiftUI developers:A couple of things to note:
body
automatically combines reducers together by running them in order and merging their effects. No need to use an explicitcombine
operator.pullback
operator has been reimagined as aScope
reducer that transforms the parent domain into the child domain, and then runs a child reducer on that domain inside a result builder.Composing reducers into optional and collection domains has also improved with the builder style. See the documentation for
ifLet
,ifCaseLet
, andforEach
for more.Dependency management
The second big change in this release is an overhaul of how dependencies are managed when building an application with the library. Currently dependencies are managed rather explicitly by having each feature model an environment of all the dependencies needed to implement its logic and behavior:
Then, every parent feature must also have an environment struct that holds onto all of its dependencies, as well as all the dependencies of any child feature:
It must hold onto the child feature’s dependencies even if the parent feature doesn’t directly need the dependency. And then, when composing reducers via
pullback
,forEach
or any other compositional operator, we have to transform the parent environment to the child feature environment:This is a lot of boilerplate to maintain, and any changes made to dependences at the child layer will force you to make changes to every layer above it. However, if you do maintain all of this code the pay off is absolutely worth it since you get to instantly control the execution environment a reducer operates in. This is great for testing, and has other applications as well.
The
ReducerProtocol
enables us to completely revamp how dependencies are managed in a Composable Architecture application. Dependencies can be added to a reducer using the new@Dependency
property wrapper, which is to reducers what SwiftUI’s@Environment
property is to views. The library even comes with many common dependencies provided right out of the box that you can use with no additional work:For custom, domain-specific dependencies, such as the
APIClient
, you can perform a little bit of upfront work to register the dependency with the library by telling the library where to find the “live” version of the dependency and the “test” version:This allows you to use the
@Dependency
property wrapper with your own dependencies right along side the dependencies that ship with the library:By default the library will use the
liveValue
dependency when running your code in non-test environments. This provides live versions of the dependencies, such as the real mainDispatchQueue
, or the realDate.init
initializer, etc. When your code is run in tests, thetestValue
will be used, which should be a mocked or unimplemented version of the dependency that doesn’t need to interact with the outside world.Migration
We have an article prepared describing some steps you can take to slowly and incrementally migrate an existing Composable Architecture to the new protocol-style. If there are some migration scenarios that are not covered by this article we would love to know so that we can improvement article.
Trying the beta
To give the beta a shot, update your SPM dependencies to point to the protocol-beta branch:
This branch also includes updated demo applications using these APIs, so check them out if you're curious!
We really think these tools will make TCA even more fun and easier to use! If you take things for a spin, please let us know if you have any questions, comments, concerns, or suggestions!
Beta Was this translation helpful? Give feedback.
All reactions