RFC: General tips and tricks #1666
Replies: 26 comments 50 replies
-
Try to make your |
Beta Was this translation helpful? Give feedback.
-
Avoid mutating property observers like |
Beta Was this translation helpful? Give feedback.
-
Name your actions according to their source rather than their expected effect. For example, you should probably use |
Beta Was this translation helpful? Give feedback.
-
When composing domains, name your state properties and embedded actions similarly. If you embed the |
Beta Was this translation helpful? Give feedback.
-
Use the |
Beta Was this translation helpful? Give feedback.
-
Likely a matter of taste, but name your features/domains directly when possible, without a |
Beta Was this translation helpful? Give feedback.
-
If you're using a template to bootstrap your feature's views, add a simple |
Beta Was this translation helpful? Give feedback.
-
Great list! Especially this one. 😁 I think soon we will be able to get a document together with best practices, and this provides a great start. We could also add the view/internal/effect/delegate action cases a la @krzysztofzablocki's and @IanKeen's style. |
Beta Was this translation helpful? Give feedback.
-
Something that may not be immediately clear to newcomers: you don't need a Furthermore, if your Button("Send") { ViewStore(store.stateless).send(.userDidTapButton) } Note: I'm using |
Beta Was this translation helpful? Give feedback.
-
I will add to this list that we should discuss how running tests in an application target can cause failures to leak out, as described in this discussion: #1652. I don't know if there is something we can do at the library level, but it's annoying enough that we should get some documentation on it. |
Beta Was this translation helpful? Give feedback.
-
This one is rather unfortunate and may hopefully fix itself in a next version of Xcode: When implementing |
Beta Was this translation helpful? Give feedback.
-
I'll shoot... Should you be using a multi-module project, you have to define every library you use a preview in as a target in your |
Beta Was this translation helpful? Give feedback.
-
You can use vscode for TCA - the SPM extension and xcode tools installation is enough to get pretty quality linking, especially with the multi-package setup. The big benefit you get with vscode is github copilot: you can get amazing first approximations / boilerplate generation with the following steps: First, do import preambles, top-level struct defintion, and state definition. Be very careful to name your variables exactly their abstract intent. From that (at least in my project), copilot can generate at least basic reducers and even a reasonable view implementation! It can also generate great dependency definitions - again, define the protocol / struct first before touching any implementation / mocks. |
Beta Was this translation helpful? Give feedback.
-
If you are using Sourcery Pro, I have made the bellow simple template. When you want to implement a new Reducer, you need to just write its definition ie: result: public struct MyFeatureReducer: ReducerProtocol {
// MARK: - State
public struct State: Equatable {
public var temp: String
// @BindableState
// public var tempBindable: String
}
// MARK: - Action
public enum Action: Equatable /*, BindableAction*/ {
case nada
// case binding(BindingAction<State>)
}
// MARK: - Dependencies
// @Dependency(\.apiClient) var apiClient
// MARK: - Init
public init() {}
// MARK: - Body
public var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .nada:
state.temp = ""
return .none
// case .binding:
// return .none
}
}
}
} The template:
|
Beta Was this translation helpful? Give feedback.
-
When |
Beta Was this translation helpful? Give feedback.
-
Looks like this might be the first linter-related comment on here :) I have found the following to be useful in my .swiftlint.yml: custom_rules:
tca_explicit_generics_reducer:
included: ".*\\.swift"
name: "Explicit Generics for Reducer"
regex: 'Reduce\s+\{'
message: "Use explicit generics in ReducerBuilder (Reduce<State, Action>) for successful autocompletion."
severity: error
tca_scope_unused_closure_parameter:
name: "TCA Scope Unused Closure Parameter"
regex: '\.scope\(\s*state\s*:\s*\{\s*\_'
message: "Explicitly use closure parameter when scoping store (ensures the right state is being mutated)"
severity: error
xctassertnodifference_preferred:
name: "XCTAssertNoDifference Preferred"
regex: 'XCTAssertEqual\('
message: "Use PointFree's XCTAssertNoDifference from CustomDump library when possible"
severity: warning |
Beta Was this translation helpful? Give feedback.
-
I don't know if I'm the only TCA Boundary user (TCA Action Boundaries by Krzysztof Zablocki, Thoughts on "Action Boundaries" Discussion) that has nested switch statements to group reducer actions, but I strongly advise that you don't do this. When your reducer gets large, it becomes SO hard to figure out whether the current case is for your X action or Y action. Flattening out the switch statements may mean you have a long case like switch action {
-case let .view(viewAction):
- switch viewAction {
- case .didLoad:
- return .run { send in
- send(._internal(.loadSongs))
- }
- }
-case let ._internal(internalAction):
- switch internalAction {
- case .loadSongs:
- state.songs = fetchSongs()
- return .none
+case .view(.viewDidLoad):
+ return .run { send in
+ send(._internal(.loadSongs))
}
+
+case ._internal(.loadSongs):
+ state.songs = fetchSongs()
+ return .none
} |
Beta Was this translation helpful? Give feedback.
-
When reusing a Reducer at multiple places at the same time and using a .watch-function that you make |
Beta Was this translation helpful? Give feedback.
-
If you're going to use protocols to ensure that your views conform to using TCA's stores and viewStores, make sure you do NOT name your associated types public protocol TCAStoreSupport: CombineSupport {
associatedtype StoreState // Should NOT be 'State'
associatedtype StoreAction: TCAAction
associatedtype ViewState
var store: Store<StoreState, StoreAction> { get }
var viewStore: ViewStore<ViewState, StoreAction.ViewAction> { get set }
var cancellables: Set<AnyCancellable> { get set }
func setupUI()
func setupBindings()
func setupActions()
} |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
For the 2 developers in the world that use UIKit, utilize ViewState transforms, and are already removing import Combine
import ComposableArchitecture
import Foundation
extension Store {
public func transformState<ViewState>(
to viewState: @escaping (_ state: State) -> ViewState
) -> Store<ViewState, Action> {
scope(state: viewState, action: { $0 })
}
} Then all you need to do is just pass the ViewState initializer to it like this: let viewStore = store.transformState(to: ViewState.init) |
Beta Was this translation helpful? Give feedback.
-
In .onChange(of: isActive) { newIsActive in
Self.logger.log(
"""
update_to_unset=\(String(describing: viewStore.state), privacy: .public)
"""
)
DispatchQueue.main.async {
// If that is set, unset it.
store.send(.unset(viewStore.state))
}
} // .onChange The solution is to assign Note: This only happened under certain preconditions. This warning here is just so you are aware of the potential issue. In my case I first had to switch away from the tab and back again, only then the issue arose (semi-reproducabily). I also used state variables and view vanishing due to having been deleted (that triggered the state change and the .onChange here). I use the ReducerReader and my own instrumentation patch ontop TCA 1.0. But.. the debugging was painful. |
Beta Was this translation helpful? Give feedback.
-
Addendum to cancellation: The same values used in |
Beta Was this translation helpful? Give feedback.
-
I created a swift macro annotation that calls fatalError because it is tedious to rewrite the closure of Annotate your struct or class with import FatalTestValue
@FatalTestValue
struct Example {
var create: @Sendable (Int) async throws -> Void
var read: @Sendable (Int) async throws -> String
var update: @Sendable (Int, String) async throws -> Void
var delete: (Int) -> Void
} This will automatically generate an extension with a extension Example {
public static let testValue = Example(
create: { _ in
fatalError()
},
read: { _ in
fatalError()
},
update: { _, _ in
fatalError()
},
delete: { _ in
fatalError()
}
)
} https://github.com/CuriositySoftware/swift-fatal-test-value It's fun to use swift macro to solve TCA's boilerplate.😀 |
Beta Was this translation helpful? Give feedback.
-
I don't know if this is useful for anyone else but we're using the namespaced actions like And so we've started to use
It's sort of like
Using this it means that if you have any Also... using Xcode code folding it looks so nice 😄 |
Beta Was this translation helpful? Give feedback.
-
Larger TCA codebases can be hard to navigate especially if you have the same action coming from multiple places, you can use xcodes symbol search feature to find where a certain action is being used throughout your codebase: |
Beta Was this translation helpful? Give feedback.
-
Hello everyone!
When using TCA, there are a few simple prescriptions that are not immediately obvious, but that could make your life easier in the long run.
I didn't think in depth about the question, but I'll post as individual comments a few tips that immediately come to my mind. I'll post using an imperative form, but these are only personal (and open) prescriptions, and not something absolute or mandatory.
Feel free to comment or add your finds if you spotted something interesting while building apps with TCA. Maybe we can build a more structured list at some point.
Beta Was this translation helpful? Give feedback.
All reactions