ViewStore binding not working when used with a search bar #2090
-
Hello, I'm on iOS 16.4, using .searchable(text: viewStore.binding(
get: \.searchText,
send: Action.searchTextChanged
)) sort of "freezes" the search bar and the action is never fired. Please see the attached sample project. The sample project has an struct AppRootScene: Reducer {
struct State: Equatable {
var itemsA = ItemsArray(uniqueElements: Mocks.items)
var items: ItemsListScene.State {
get { .init(items: itemsA) }
set { /* empty - the items tab does not change the array of items */ }
}
}
enum Action: Equatable, Sendable {
case items(ItemsListScene.Action)
}
var body: some ReducerOf<Self> {
Scope(state: \.items, action: /Action.items) {
ItemsListScene()
}
Reduce { state, action in
switch action {
case let .items(action):
print("AppRootScene :: .items(\(action))")
return .none
}
}
}
struct View: SwiftUI.View {
private let store: StoreOf<AppRootScene>
init(store: StoreOf<AppRootScene>) {
self.store = store
}
var body: some SwiftUI.View {
NavigationStack {
ItemsListScene.View(store: store.scope(
state: \.items,
action: Action.items
))
.navigationTitle("Items")
}
}
}
} and an struct ItemsListScene: Reducer {
struct State: Equatable, Sendable {
var items: ItemsArray
var searchText = ""
init(items: ItemsArray = []) {
self.items = items
}
var searchResults: ItemsArray {
let sortedItems = items
.filter { $0.name.lowercased().starts(with: searchText.lowercased()) }
.sorted()
return .init(uniqueElements: sortedItems)
}
}
enum Action: Equatable, Sendable {
case searchTextChanged(String)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case let .searchTextChanged(searchText):
print("ItemsListScene :: .searchTextChanged(\(searchText))")
state.searchText = searchText
return .none
}
}
}
struct View: SwiftUI.View {
private let store: StoreOf<ItemsListScene>
init(store: StoreOf<ItemsListScene>) {
self.store = store
}
var body: some SwiftUI.View {
// Comment each of these in or out to see what I've tried so far.
View1(store: store)
// View2(store: store)
// View3(store: store)
}
}
}
// The TCA way. The search bar is "frozen" and the binding action never gets sent.
struct View1: SwiftUI.View {
private let store: StoreOf<ItemsListScene>
init(store: StoreOf<ItemsListScene>) {
self.store = store
}
var body: some SwiftUI.View {
WithViewStore(store, observe: { $0 }) { vs in
List {
Section {
ForEach(vs.searchResults) { item in
Text(item.name)
}
}
}
.searchable(text: vs.binding(get: \.searchText, send: Action.searchTextChanged))
}
}
} In attempting to debug the problem, I tried to create and manage the binding explicitly, but it still shows the same problem. Also, neither of the two print statements gets executed. // An attempt to explicitly create and manage a binding to the search text fails.
// The search bar is still "frozen".
struct View2: SwiftUI.View {
private let store: StoreOf<ItemsListScene>
@ObservedObject var vs: ViewStoreOf<ItemsListScene>
@Binding var searchText: String
init(store: StoreOf<ItemsListScene>) {
self.store = store
let vs = ViewStoreOf<ItemsListScene>(store, observe: { $0 })
self._searchText = .init(
get: {
print("getter called")
return vs.searchText
},
set: {
print("setter called: \($0)")
vs.send(.searchTextChanged($0))
}
)
self._vs = .init(initialValue: vs)
}
var body: some SwiftUI.View {
List {
Section {
ForEach(vs.searchResults) { item in
Text(item.name)
}
}
}
.searchable(text: $searchText)
}
} Of course, I also looked at what a vanilla SwiftUI implementation would do and, in that case, the search bar works fine but then I don't know how to connect the // This makes the search bar "usable" but then how do I connect the
// @State searchText property to the view store?
struct View3: SwiftUI.View {
private let store: StoreOf<ItemsListScene>
@SwiftUI.State private var searchText = ""
init(store: StoreOf<ItemsListScene>) {
self.store = store
}
var body: some SwiftUI.View {
WithViewStore(store, observe: \.searchResults) { vs in
List {
Section {
ForEach(vs.state) { item in
Text(item.name)
}
}
}
}
.searchable(text: $searchText)
}
} Is it something that I am doing wrong or is it a bug in either TCA or SwiftUI? I can't tell so any help would be greatly appreciated. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
Hey @wltrup! struct State: Equatable {
var itemsA = ItemsArray(uniqueElements: Mocks.items)
var items: ItemsListScene.State {
get { .init(items: itemsA) }
set { /* empty - the items tab does not change the array of items */ }
}
} The struct State: Equatable {
var itemsA = ItemsArray(uniqueElements: Mocks.items)
private var _items: ItemListScene.State = .init(items: [])
var items: ItemsListScene.State {
get {
var state = _items
state.items = self.itemsA
return state
}
set {
self._items = newValue
}
}
} I hope it'll help solving your issue! |
Beta Was this translation helpful? Give feedback.
Hey @wltrup!
The problem comes from here:
The
set
shouldn't be noop: thesearchText
value you just set in the child feature is discarded, and you return a brand newItemsListScene.State
with this field empty for the next round. It means that the search field is empty, so SwiftUI deletes the character you just entered and you get the impression that the field is stuck.When deriving states with internal properties (like
searchText
), you need to make…