RFC: General tips and tricks #1666
Replies: 26 comments 50 replies
|
Try to make your |
|
Avoid mutating property observers like |
|
Name your actions according to their source rather than their expected effect. For example, you should probably use |
|
When composing domains, name your state properties and embedded actions similarly. If you embed the |
|
Use the |
|
Likely a matter of taste, but name your features/domains directly when possible, without a |
|
If you're using a template to bootstrap your feature's views, add a simple |
|
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. |
|
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 |
|
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. |
|
This one is rather unfortunate and may hopefully fix itself in a next version of Xcode: When implementing |
|
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 |
|
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. |
|
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: |
|
When |
|
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 |
|
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
} |
|
When reusing a Reducer at multiple places at the same time and using a .watch-function that you make |
|
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()
} |
|
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) |
|
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))
}
} // .onChangeThe 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. |
|
Addendum to cancellation: The same values used in |
|
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.😀 |
|
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 😄 |






Uh oh!
There was an error while loading. Please reload this page.
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.
All reactions