Replies: 19 comments 94 replies
-
I've been keeping up with the latest TCA stuff on a side project. It's not huge but it does have 15 small-ish features with reducers now. I just sat down to play with the new stuff but I'm having issues migrating. The project compiled and ran fine pointed at the I tried starting at the app reducer level, as recommended in the migration guide, but I'm struggling already with my tabView. I added the
|
Beta Was this translation helpful? Give feedback.
-
Dear Stephan and Brandon, amazing work, this is all looking incredible. 1.
I find that in the case of an extension housing the Action for X, the following code is generated by the macro:
For the avoidance of doubt: I am pointing out that the extension statement is inserted in an existing extension statement. This is not valid swift code. 2.
That I get the error: "Immutable value 'self._$observationRegistrar' may only be initialized once". I am not sure why this is happening, but the code otherwise compiles and works correctly pre-observation-beta. I hope these findings are helpful to you. Thank you for providing us with this extraordinary library. |
Beta Was this translation helpful? Give feedback.
-
In our project, we previously managed different representations of the same state using two distinct viewStates: one for a minimal cell in a list and another for a detailed state page. This was achieved using a viewStore. Now, with the viewStore removed, how can we maintain this level of differentiation in our views? @mbrandonw @stephencelis |
Beta Was this translation helpful? Give feedback.
-
This might be a silly question as I don't have a lot of experience with macros yet, but wouldn't it be possible to abstract @PerceptionTracking
struct FeatureView: View {
var body: some View {
VStack { ... }
}
// synthesized:
var body: some View {
WithPerceptionTracking {
VStack { ... }
}
}
} |
Beta Was this translation helpful? Give feedback.
-
My initial feedback. 1. It would be great having more examples of using new tools. The best for me is always isowords. Thanks for such a great changes. |
Beta Was this translation helpful? Give feedback.
-
I had been struggling to create a heterogenous list view that would display based on enum case and kept getting lost trying to keep track of all my IfCaseLet and scoping logic, so I pared back to the simplest example I could think of... and still got lost. But after reading the migration guide I implemented a trivial example and was so impressed how much better this syntax flows. The only issue I ran into was a TextField that wouldn't update with Very simple example but thanks so much for the work that went into this update! |
Beta Was this translation helpful? Give feedback.
-
@mbrandonw @stephencelis I just finished migrating my app fully. It's still in development and targeting iOS 17. I did get to use/ update just about every component mentioned in the migration guide. 1. Does your project still compile? 2. Does your test suite still pass and does your app still work the same as it did before? 3. Can you update some of your features to use @ObservableState and let us know if it works for you? Does your app still behave the same? 4. Can you update some of your uses of IfLetStore, ForEachStore, SwitchStore, etc. to instead use simpler constructs, as detailed in the migration guide. Does everything still behave the same? 5. Can you update some of your uses of sheet(store:) with sheet(item:), popover(store:) with popover(item:), etc., as detailed in the migration guide. I did notice one thing in the console after migrating that doesn't happen on my main branch without the migration. This output gets logged to the console when I switch tabs sometimes: Granted this is my own app, the migration was very trivial following the migration guide. The only things that slowed me down were the known issues of needing to have This was really fun and exciting and I am looking forward to the next video. Thanks for all of your hard work. One question: The |
Beta Was this translation helpful? Give feedback.
-
I am experiencing @propertyWrapper
struct EquatableFormItem<T: Equatable>: Equatable {
let original: T
var wrappedValue: T
init(item: T) {
original = item
wrappedValue = item
}
var projectedValue: EquatableFormItem<T> {
self
}
var changed: Bool {
original != wrappedValue
}
}
@Reducer
struct FormFeature<T: Equatable> {
@ObservableState
struct State: Equatable {
@EquatableFormItem var item: T // <-- error in this line
}
enum Action { ... }
var body: some ReducerOf<Self> { ... }
} |
Beta Was this translation helpful? Give feedback.
-
Looks that most issues will be connected with iOS <17. |
Beta Was this translation helpful? Give feedback.
-
Hey @mbrandonw, I still had the code of your composable architecture tutorial on my machine and I thought of just trying to convert it based on your migration guide. All of this went very smoothly and, like others, I really like the changes you're implementing here! 🙏 However, one issue showed up in regards to the Below is the code from the tutorial converted according to your migration guide:
This code fails to compile due to Would you just set an empty contact here as a default inside the
|
Beta Was this translation helpful? Give feedback.
-
Following the migration guide, I'm having trouble making NavigationStack work. Original Code: struct AppFeature: Reducer {
struct State {
var path = StackState<Path.State>()
var questionList = QuestionListFeature.State()
}
enum Action {
case path(StackAction<Path.State, Path.Action>)
case questionList(QuestionListFeature.Action)
}
struct Path: Reducer {
enum State {
case detail(QuestionDetailFeature.State)
}
enum Action {
case detail(QuestionDetailFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: /State.detail, action: /Action.detail) {
QuestionDetailFeature()
}
}
}
var body: some ReducerOf<Self> {
Scope(state: \.questionList, action: /Action.questionList) {
QuestionListFeature()
}
Reduce { state, action in
switch action {
case .path:
return .none
case .questionList:
return .none
}
}
.forEach(\.path, action: /Action.path) {
Path()._printChanges()
}
}
}
struct AppView: View {
let store: StoreOf<AppFeature>
var body: some View {
NavigationStackStore(
self.store.scope(state: \.path, action: { .path($0) })
) {
QuestionListView(
store: self.store.scope(state: \.questionList, action: { .questionList($0) })
)
} destination: { state in
switch state {
case .detail:
CaseLet(/AppFeature.Path.State.detail, action: AppFeature.Path.Action.detail, then: QuestionDetailView.init)
}
}
}
} This compiles and works as expected. Following the guide, I ended up with: @Reducer
struct AppFeature {
@ObservableState
struct State {
var path = StackState<Path.State>()
var questionList = QuestionListFeature.State()
}
enum Action {
case path(StackAction<Path.State, Path.Action>)
case questionList(QuestionListFeature.Action)
}
struct Path: Reducer {
enum State {
case detail(QuestionDetailFeature.State)
}
enum Action {
case detail(QuestionDetailFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: /State.detail, action: /Action.detail) {
QuestionDetailFeature()
}
}
}
var body: some ReducerOf<Self> {
Scope(state: \.questionList, action: /Action.questionList) {
QuestionListFeature()
}
Reduce { state, action in
switch action {
case .path:
return .none
case .questionList:
return .none
}
}
.forEach(\.path, action: /Action.path) {
Path()._printChanges()
}
}
}
struct AppView: View {
@State var store: StoreOf<AppFeature>
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) { // error is here
QuestionListView(
store: store.scope(state: \.questionList, action: \.questionList)
)
} destination: { store in
switch store.state {
case .detail:
if let store = store.scope(state: \.detail, action: \.detail) {
QuestionDetailView(store: store)
}
}
}
}
} This does not compile, with 4 errors, all on the NavigationStack line:
|
Beta Was this translation helpful? Give feedback.
-
With ViewStore being an ObservableObject it could be used as an EnvironmentObject in child views needing access to the store. With the new tools, is the only option manually passing the store to child views? |
Beta Was this translation helpful? Give feedback.
-
I'm really excited about this release - thanks for sharing the beta preview! I've been attempting to use the Perception framework sans ComposableArchitecture on some existing projects, but I can't seem to get it to react to any changes, ie: import Perception
@Perceptible
class MyClass {
var otherObject: SomeOtherObject?
init(otherObject: SomeOtherObject?) {
self.otherObject = otherObject
self.observeChanges()
}
func observeChanges() {
_ = withPerceptionTracking {
_ = self.otherObject
} onChange: {
DispatchQueue.main.async {
print("Hi!")
self.observeChanges()
}
}
}
} Am I doing something obviously wrong? |
Beta Was this translation helpful? Give feedback.
-
Testing with a stripped down Feature/View on iOS 16 triggers this log message repeatedly:
I noticed this first in my app that was working great with iOS 17. Testing against iOS 16 triggered an endless stream of the message in the log and brought the app to a crawl. The app actually continued to work (albeit with super slow response). So I tested the following in a new test app and was able to trigger the log messages: @Reducer
public struct TestFeature {
public init() {}
@ObservableState
public struct State: Equatable {
var testValue = "Change Me"
public init() {}
}
public enum Action: BindableAction {
case binding(BindingAction<State>)
}
public var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
}
}
}
}
struct TestView: View {
@State var store: StoreOf<TestFeature>
var body: some View {
WithPerceptionTracking {
VStack {
TextField("Binding Test", text: $store.testValue)
}
}
}
} If I remove the binding, and just display the "testValue", the log message goes away. So it appears that using a binding will trigger the log message regardless of using WithPerceptionTracking. This is using XCode 15.1 beta 3 |
Beta Was this translation helpful? Give feedback.
-
I've updated to the latest commits on observation-beta which fixed the WithPerceptionTracking log messages on iOS 16. However, the app is very slow on iOS 16 – each time a list view is rendered the CPU pegs at 100% for several seconds and the app is unresponsive to taps and scrolling (this is on Simulator -- I don't have an iOS 16 device to test currently). After the CPU returns to 0%, the app will respond to taps again. Navigating to a detail, then back to the list using a NavigationStack results in the CPU pegging again. iOS 17 performs great, so I'm guessing WithPerceptionTracking is doing some pretty heavy work. Is this something you've seen before? Thanks! |
Beta Was this translation helpful? Give feedback.
-
I was introduced to TCA a few weeks ago. I've read about the core concepts, so I am excited to dive in! Unless there is a beta branch of TCA tutorial? |
Beta Was this translation helpful? Give feedback.
-
Hi, I find view is not updating when trying to migrate features that have this shape.
I am on revision import ComposableArchitecture
import SwiftUI
// a vanilla SwiftUI view that uses binding of optional
struct OptionalFieldView: View {
@Binding var field: Int?
var body: some View {
Button {
if field == nil {
field = 0
} else {
field = nil
}
} label: {
Image(systemName: field == nil ? "circle" : "checkmark.circle.fill")
}
}
}
// models
struct ItemDetail: Equatable {
var field: Int?
}
struct Item: Equatable {
// some additional properties
// ...
// properties
var detail: ItemDetail
}
// a feature that operates on Item.Detail
@Reducer
struct DetailFeature {
@ObservableState
struct State: Equatable {
var detail: ItemDetail
}
enum Action: BindableAction {
case binding(BindingAction<State>)
}
var body: some ReducerOf<Self> {
BindingReducer()
}
}
struct DetailView: View {
@State var store: StoreOf<DetailFeature>
var body: some View {
OptionalFieldView(field: $store.detail.field)
}
}
// a feature that operates on Item, and integrates DetailFeature
@Reducer
struct ItemFeature {
@ObservableState
struct State: Equatable {
var item: Item
var toggle: DetailFeature.State {
get {
.init(detail: item.detail)
}
set {
item.detail = newValue.detail
}
}
}
enum Action {
case toggle(DetailFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: \.toggle, action: \.toggle) {
DetailFeature()
}
}
}
struct ItemView: View {
@State var store: StoreOf<ItemFeature>
var body: some View {
DetailView(store: store.scope(state: \.toggle, action: \.toggle))
}
}
#Preview {
ItemView(store: StoreOf<ItemFeature>(initialState: ItemFeature.State(item: Item(detail: ItemDetail(field: nil)))) {
ItemFeature()
})
// DetailView(store: StoreOf<DetailFeature>(initialState: DetailFeature.State(detail: ItemDetail(field: nil))) {
// DetailFeature()
// })
} |
Beta Was this translation helpful? Give feedback.
-
Hello everyone, this discussion has gotten far too big for us to reasonably keep track of, so we are locking it for now. Please create a new discussion if you have questions or comments about the beta. |
Beta Was this translation helpful? Give feedback.
-
I'm sharing some of my findings on the performance of the I did my own quick and dirty instrumentation using a DEBUG build (and without The table below shows the two use cases and how many times
From this simple use case, there is a lot of computation going on to perform the perception check. This results in a severe degradation in performance and explains why typing in a text field in the simulator is painfully slow with 0.75 seconds of computation per keystroke. The good news is the main culprit seems to be the unnecessary perception checks inside of reducers. Reducer call stack frames are particularly large and also have very complicated mangled names. So not only are they responsible for over 75% of the calls to Although this use case was a simple interactive UI use case, any heavy use of read access on observed state inside a reducer is going to incur a large penalty when running the perception back port in DEBUG mode due to the perception check. Since we don't care about read access of observed state in a reducer for the purpose of the perception check, it seems that there is some low hanging fruit to detect if Thoughts? |
Beta Was this translation helpful? Give feedback.
-
Important
This discussion has gotten too long for us to reasonable keep track of, so we are locking it. If you have questions or comments about the observation beta, please create a new discussion.
Hello all, today we are beginning what is perhaps the most important beta for the Composable Architecture, ever.
We are bringing Swift 5.9’s observation tools to the library, and we are doing so in a way that is backwards compatible with the current version of the library, and backwards compatible with older versions of iOS. That’s right. Even if you are targeting iOS 13 you will be able to use all the new tools we are previewing today.
To try out the beta you can point your dependency on the Composable Architecture to the
observation-beta
branch. We have prepared a migration guide that can be followed to try updating some of your features.We do not recommend depending on this beta branch for long term, active development of your application. It is likely to have many breaking changes in the coming weeks or months until its final release.
But we would appreciate if everyone could point their projects to the
observation-beta
branch and let us know a few things:@ObservableState
and let us know if it works for you? Does your app still behave the same?IfLetStore
,ForEachStore
,SwitchStore
, etc. to instead use simpler constructs, as detailed in the migration guide. Does everything still behave the same?sheet(store:)
withsheet(item:)
,popover(store:)
withpopover(item:)
, etc., as detailed in the migration guide.Beta Was this translation helpful? Give feedback.
All reactions