Observable state breaks wrapped UITextView on <17.4 #2951
Replies: 8 comments 1 reply
-
The above example project targets iOS 17, but just to rule other things out I also tried:
So this seems to narrow it down somehow to the TCA/perception integration with |
Beta Was this translation helpful? Give feedback.
-
One more test, I updated the SwiftUI example to use @main
struct PerceptionTextViewBugApp_SwiftUI: App {
@State
var store = Store(initialState: ComposeFeature.State()) {
ComposeFeature()
}
@State
var destination: Destination?
enum Destination: Identifiable {
case compose
var id: Self { self }
}
var body: some Scene {
WindowGroup {
Text("SwiftUI App")
Button("Open Compose") {
destination = .compose
}
.sheet(item: $destination) { _ in
ComposeView(store: store)
}
}
}
} |
Beta Was this translation helpful? Give feedback.
-
It seems that the presentation API is a bit of a red-herring. The bug only seems to happen when the root view holds on to a store of some With that in mind, I seem to have narrowed this down to the most minimal reproducer: @Reducer
struct RootFeature: Reducer {
// @ObservableState
struct State: Equatable {
var compose = ComposeFeature.State()
}
enum Action {
case compose(ComposeFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: \.compose, action: \.compose) {
ComposeFeature()
}
}
}
@main
struct PerceptionTextViewBugApp_ObservableSwiftUI: App {
@State
var store = Store(initialState: RootFeature.State()) {
RootFeature()._printChanges()
}
@State
var isPresented: Bool = false
var body: some Scene {
WindowGroup {
Text("Observable TCA + SwiftUI App")
Button("Open Compose") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
ComposeView(store: store.scope(state: \.compose, action: \.compose))
}
}
}
} You can run the above example and see that the text view works as expected if you uncomment out |
Beta Was this translation helpful? Give feedback.
-
Hi @lukeredpath, can you please provide an updated project with the minimal reproducible code? I'm not sure what to update based on your new messages above. |
Beta Was this translation helpful? Give feedback.
-
Some good news - now I have a minimal reproducer, I have found two possible workarounds for this bug:
I'm not sure if there are other side effects of the first workaround and I'm not sure if it would be helpful in the case where you're using The second workaround at least gives me a potential fix for this, although it requires me to put more effort into migrating more leaf features of my app to use I've added an updated demo here: |
Beta Was this translation helpful? Give feedback.
-
Here's an updated project with only the relevant code. Firstly, I was wrong about iOS 16 - turns out this bug does not affect iOS 16. It only affects iOS 17 below 17.4 (although I have not been able to test on 17.0 or 17.1). In the project attached:
Out of curiosity, I also tried swapping out the the text state binding in |
Beta Was this translation helpful? Give feedback.
-
I think this is a problem with over-rendering and Typically SwiftUI can deal with over-renders just fine, but there are a few key areas that it gets overwhelmed. Navigation is the biggest example, but this is another example (or maybe the root cause of the problem is the same in both situations). However, the solution is much simpler. It is not necessary to wrap your So, just making this change fixes it: .sheet(item: $destination) { destination in
switch destination {
case .compose:
// ❗️ No `WithPerceptionTracking` here
ComposeView(store: Self.store.scope(state: \.compose, action: \.compose))
case .observableCompose:
WithPerceptionTracking {
ObservableComposeView(store: Self.store.scope(state: \.observableCompose, action: \.observableCompose))
}
}
} By putting Since this isn't an issue with the library I am going to convert it to a discussion. |
Beta Was this translation helpful? Give feedback.
-
OK, some good news at least - I was able to find a workaround for this as I was able to more precisely figure out what was causing this bug. For some reason, since switching to using observable state on the parent to this view, I was getting a different sequence of events in my wrapped text view that would cause the typed text to be lost. On iOS 17.4, when typing:
Yet on iOS 17.2 and 17.3:
Its very strange, but I was able to refactor my text view wrapper and feature state so that it always sends the latest text state (both the text and selection) at the same time in a single action on any selection or text did change delegate call. |
Beta Was this translation helpful? Give feedback.
-
Description
When a non-observable TCA feature is presented from an observable TCA feature, and that screen that contains a
UITextView
wrapped in a representable view, it is not possible to enter any text into the text view - it's delegate methods fire and thetextView.text
property always remains""
.Checklist
main
branch of this package.Expected behavior
I would expect the text view to work correctly.
Actual behavior
I have been able to reproduce this bug in an iOS 17.2 simulator and the original bug report from one of our internal dev builds was happening on an iOS 17.3 device. This bug does not seem to occur in 17.4. In our actual app, this component is used both on a presented sheet, but also on a different view pushed on to a navigation stack, so the actual method of presentation does not seem to matter.
This happens in one specific circumstance - when the screen with the text view is presented using
@ObservableState
and related TCA APIs. It does not happen when presented from a TCA feature/view that uses@PresentationState
and the deprecated view store APIs. It also does not happen when presenting using pure SwiftUI and@State
.Steps to reproduce
The attached project can be used to reproduce the bug. It contains:
ComposeFeature
is a basic feature that presents a text view and does not use observable state. TheWrappedTextView
is a slimmed down version of the implementation of a component from our app and takes a binding to some text state and focus state. The text view is focused when the compose view appears.@main
to test:PerceptionTextViewBugApp_NonObservable
has a root store ofRootFeatureNonObservable
which uses@PresentationState
. It displays a button which triggers the presentation of a sheet using thesheet(store:)
API. When this app runs, the text view works as expected.PerceptionTextViewBugApp_Observable
- has a bindable root store ofRootFeatureObservable
which uses@Presents
,@ObservableState
and thesheet(item:)
API to present a sheet. When this app runs, the text view does not work.PerceptionTextViewBugApp_SwiftUI
- this app just holds on to a store of the compose feature itself and presents the compose view using a@State
andsheet(isPresented:)
call. This also works as expected.PerceptionTextViewBug.zip
The Composable Architecture version information
1.9.2 - also tried main
Destination operating system
iOS 16.4 - iOS 17.3
Xcode version information
Version 15.3 (15E204a)
Swift Compiler version information
Beta Was this translation helpful? Give feedback.
All reactions