Searchable and Refreshable View in SwiftUI is glitching on ObservableObject changes - ios

I have a SwiftUI List which should both have a search bar and enable pull to refresh.
In theory, this is very simple. However, there is a search bar bug whenever I update any #Published properties in the .refreshable { } code.
My view looks like this:
struct MyView: View {
#State var text = ""
var body: some View {
NavigationView {
List {
Text("hello")
}
.refreshable {
model.prop = "world"
}
}
.searchable(text: $text)
}
}
And my model looks like this:
class Model: ObservableObject {
#Published var prop = ""
}
I've tried putting the update in an async block, but it did not solve the problem:
DispatchQueue.main.async {
model.prop = "world"
}
Any non-ObservableObject-mutating code inside of the .refreshable { } block works perfectly fine.
Am I doing something wrong?

Related

writing to Binding inside a Button action triggers onDisappear

I have a struct:
struct Demo {
struct Data {
var value: Int
}
var data: [Data]
}
and then in some view I have:
struct SomeView: View {
#Binding var demo: Demo
var body: some View {
Button(action: {
demo.data[validIndex].value = 5
}) {
Image(systemName: "plus.circle")
.resizable()
.frame(width: 50.0, height: 50.0, alignment: .center)
}
}
}
assume the var data: [Data] has more than 5 elements in it, and we're just trying to update an existing value when we press this button in SomeView. If I use SomeView like this:
struct DisappearView: View {
#Binding var demo: Demo
var body: some View {
ZStack {
// other views/shapes, etc
SomeView(demo: $demo)
}
.onAppear {
// do some stuff
}
.onDisappear {
// do some stuff
}
}
}
The data comes from:
class DemoStore: ObservableObject {
#Published var demos: [Demo] = []
// assume this loads correctly and demos is populated
}
The access to DemoStore and demos getting passed around looks like this:
import SwiftUI
#main
struct App: App {
#StateObject private var store = DemoStore()
var body: some Scene {
WindowGroup {
NavigationView {
DemosView(demos: $store.demos) { }
}
}
}
}
struct DemosView: View {
#Binding var demos: [Demo]
var body: some View {
List($demos) { $demo in
NavigationLink(destination: DisappearView(demo: $demo)) {
}
}
}
}
the write line, demo.data[validIndex].value = 5 upon button press triggers the .onDisappear in the parent View, and I cannot figure out why. I've used the debugger to step through and execute the code line by line, the array update seems to work when I write it as an expression in the debugger, but if I resume the code it boots out of the view and triggers .onDisappear. If I comment out the write line, .onDisappear isn't triggered when I press the button.

LazyVStack Initializes all views when one changes SwiftUI

I have a LazyVStack which I would like to only update one view and not have all others on screen reload. With more complex cells this causes a big performance hit. I have included sample code
import SwiftUI
struct ContentView: View {
#State var items = [String]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(self.items, id: \.self) { item in
Button {
if let index = self.items.firstIndex(where: {$0 == item}) {
self.items[index] = "changed \(index)"
}
} label: {
cell(text: item)
}
}
}
}
.onAppear {
for _ in 0...200 {
self.items.append(NSUUID().uuidString)
}
}
}
}
struct cell: View {
let text: String
init(text: String) {
self.text = text
print("init cell", text)
}
var body: some View {
Text(text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you can see even when only changing 1 cell the init gets called for every cell. Is there anyway to avoid this?
Here is a working code, there is some points to mention, View in SwiftUI would get initialized here and there or anytime SwiftUI thinks it needed! But the body of View would get computed if really some value in body changed. It is planed to work like that, there is some exceptions as well. Like body get computed even the values that used in the body were as before with no change, I do not want inter to that topic! But in your example and in your issue, we want SwiftUI renders only the changed View, for this goal the down code works well without issue, but as you can see I used VStack, if we change VStack to LazyVStack, SwiftUI would renders some extra view due its undercover codes, and if you scroll to down and then to up, it would forget all rendered view and data in memory and it will try to render the old rendered views, so it is the nature of LazyVStack, we cannot do much about it. Apple want LazyVStack be Lazy. But you can see that LazyVStack would not render all views, but some of them that needs to works. we cannot say or know how much views get rendered in Lazy way, but for sure not all of them.
let initializingArray: () -> [String] = {
var items: [String] = [String]()
for _ in 0...200 { items.append(UUID().uuidString) }
return items
}
struct ContentView: View {
#State var items: [String] = initializingArray()
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Button(action: {
if let index = self.items.firstIndex(where: { $0 == item }) {
items[index] = "changed \(index)"
}
}, label: {
ItemView(item: item)
})
}
}
}
}
}
struct ItemView: View {
let item: String
var body: some View {
print("Rendering row for:", item)
return Text(item)
}
}

SwiftUI 3 .searchable with Firestore

I want to use the new .searchable() modifier with Firebase Firestore (or some other server), like the "Search" tab in the App Store App. I just found tutorials on how to filter local lists/arrays with .searchable().
I know how to send a search request to Firestore using the .onSubmit() modifier. My question is how to show another View when the .onSubmit() is called or when a searchCompletion is clicked.
So if the search bar is empty, I want SuggestionsView() to show but if the user searched for something there should be SearchResultView() under the search bar, like in the App Store:
I tried it with this code but this doesn't give the App-Store effect.
struct SearchView: View {
#State var searchText = ""
var body: some View {
NavigationView {
if searchText.isEmpty {
SuggestionsView()
.searchable(text: $searchText)
.navigationTitle("Search")
}
else {
SearchResultView()
.searchable(text: $searchText)
}
}
}
}
struct SuggestionsView: View {
var body: some View {
List {
Text("Suggestions")
}
.listStyle(PlainListStyle())
}
}
struct SearchResultView: View {
var body: some View {
List {
Text("Results")
}
.listStyle(InsetGroupedListStyle())
}
}
Move searchbar to the NavigationView so it is not affected by the if else statement.
import SwiftUI
struct SearchView: View {
#State var searchText = ""
var body: some View {
if #available(iOS 15.0, *) {
NavigationView {
if searchText.isEmpty {
SuggestionsView()
.navigationTitle("Search")
} else {
SearchResultView()
}
}
.searchable(text: $searchText)
} else {
// Fallback on earlier versions
}
}
}

ObservableObject is updating all views and causing menus to close in SwiftUI

I have this class which calls an API every 3 seconds (using a timer) and saves the result in the #Published array named items:
public class ApiAdapter: ObservableObject {
#Published var items = [Item]()
init() {
api = Api(...)
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in
api.getItems() { items in
if let _items = items {
self.items = _items
}
}
}
}
}
The ApiAdapter is used throughout the entire app so I decided to make it an #EnvironmentObject.
In the main view of my app I'm accessing the apiAdapter.items array for displaying it in a list like so:
struct MainListView: View {
#EnvironmentObject var apiAdapter: ApiAdapter
var body: some View {
NavigationView {
List {
ForEach(apiAdapter.items, id: \.hash) { i in
Text(i.name)
...
}
}
.navigationBarItems(trailing:
Menu {
Picker(selection: $category, label: Text("Filter")) {
Text("Category 1").tag(1)
Text("Category 2").tag(2)
Text("Category 3").tag(3)
}
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.imageScale(.large)
}
}
}
}
In MainListView I have also some Menus in the navigationBar which get closed every time the apiAdapter updates the items variable, so it makes it hard to use them.
How is it possible to update only the list and not the entire view? or even better how is it possible to update only the items that changed in the last api call?
Remove the EnvironmentObject from your MainListView. Create a new View for the List only like this:
struct ListView : View {
#EnvironmentObject var apiAdapter: ApiAdapter
var body : some View {
List {
ForEach(apiAdapter.items, id: \.hash) { i in
Text(i.name)
}
}
}
}
Now your subview only updates, while your main view remains the same and the Menu remains opened.
And your MainListView as follows:
struct MainListView: View {
var body: some View {
NavigationView {
ListView() //<< here comes your ListView which refreshes independent
.navigationBarItems(trailing:

Views do not update inside the ForEach in SwiftUI

I'm using a ForEach to parse a list of models and create a view for each of them, Each view contains a Button and a Text, the Button toggles a visibility state which should hide the text and change the Button's title (Invisible/Visible).
struct ContentView: View {
var colors: [MyColor] = [MyColor(val: "Blue"), MyColor(val: "Yellow"), MyColor(val: "Red")]
var body: some View {
ForEach(colors, id: \.uuid) { color in
ButtonColorView(color: color.val)
}
}
}
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
if visible {
return AnyView( HStack {
Button("Invisible") {
self.visible.toggle()
}
Text(color)
})
} else {
return AnyView(
Button("Visible") {
self.visible.toggle()
}
)
}
}
}
class MyColor: Identifiable {
let uuid = UUID()
let val: String
init(val: String) {
self.val = val
}
}
Unfortunately it's not working, the views inside the ForEach do not change when the Button is pressed. I replaced the Foreach with ButtonColorView(color: colors[0].val) and it seems to work, so I'd say the problem is at ForEach.
I also tried breakpoints in ButtonColorView and it seems the view is called when the Button is triggered returning the right view, anyways the view does not update on screen.
So, am I using the ForEach in a wrong way ?
This problem occurs in a more complex app, but I tried to extract it in this small example. To summarize it: I need ButtonColorView to return different Views depending of its state (visibility in this case)
PS: I'm using Xcode 11 Beta 6
You are using ForEach correctly. I think it's the if statement within ButtonColorView's body that's causing problems. Try this:
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
HStack {
Button(visible ? "Invisible" : "Visible") {
self.visible.toggle()
}
if visible {
Text(color)
}
}
}
}
You can also try something like this:
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
HStack {
if visible {
HStack {
Button("Invisible") {
self.visible.toggle()
}
Text(color)
}
} else {
Button("Visible") {
self.visible.toggle()
}
}
}
}
}

Resources