Flickering with .searchable List - swiftui

For the example, you need to add LoremSwiftum with SPM.
I'm found that when using the .init(_:id:rowContent:) constructor of List, there is flickering of the row items in the search field.
Video of what it looks like
import SwiftUI
import LoremSwiftum
let words = Array(Set(Lorem.words(3000).components(separatedBy: " ")))
struct ContentView: View {
#State var searchText = ""
var searchedWords: [String] {
searchText.isEmpty ? words : Array(words.filter { $0.localizedCaseInsensitiveContains(searchText) }.prefix(50))
}
var body: some View {
NavigationView {
List(searchedWords, id:\.self) { word in
HStack {
Rectangle().frame(width: 50, height: 50).foregroundColor(.red)
Text(word)
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
}
Using .indices makes the flicker go away, ie:
List(searchedWords.indices, id:\.self) { i in
let word = words[I]
...
}
although as I understand it, using .indices can cause problems when the items change.
Using .id(UUID()) on the List also makes the flickering go away, although this has some problems as well.
So, how can I use the correct constructor of List and not have awful flickering of the items when searching?

Try avoiding computed properties for lists identifiable at least.
I had next object for list item:
struct Message: Identifiable, Equatable, Codable {
var id: String { UUID().uuidString }
let title: String
let message: String
}
It was flickering very similar to your example.
Changing structure to next one solved issue for me.
struct Message: Identifiable, Equatable, Codable {
let id: String
let title: String
let message: String
init(title: String, message: String) {
self.id = UUID().uuidString
self.title = title
self.message = message
}
}

I am using the following code and I don't experience the flicker.
struct ContentView: View {
#State private var words = Array(Set(Lorem.words(3000).components(separatedBy: " ")))
#State private var searchText: String = ""
#State private var filteredWords: [String] = []
var body: some View {
NavigationStack {
List(filteredWords, id: \.self) { word in
HStack {
Rectangle().frame(width: 50, height: 50).foregroundColor(.red)
Text(word)
}
}.searchable(text: $searchText)
.onChange(of: searchText) { search in
filteredWords = words.filter({ $0.starts(with: search.lowercased())})
}
.onAppear {
filteredWords = words
}
}
}
}

Related

The searchable modifier on tvOS, when inside a NavigationView, doesn't allow refocusing the search bar

When using a NavigationView and a ScrollView with searchable, as soon as you focus a item in the LazyVGrid the search bar collapses the keyboard, and it's no longer possible to re-focus the search bar to change the query.
It doesn't matter if the .searchable modifier is applied to the ScrollView or the NavigationView.
The more I look at it, the more it appears to be a SwiftUI bug on tvOS, but I would still like to find a workaround, if possible.
Sample code which reproduces the problem:
import SwiftUI
struct ContentView: View {
private var fruits = ["Apples", "Pears", "Oranges", "Plums", "Pineapples", "Bananas"]
#State private var items: [String]
#State private var searchText: String = ""
init() {
self.items = fruits
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 300))], spacing: 40) {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(text: item)) {
Text(item)
}
}
}
}
.searchable(text: $searchText)
.onChange(of: searchText) { query in
if query.isEmpty {
items = fruits
} else {
items = fruits.filter { $0.contains(query) }
}
}
}
}
}
struct DetailView: View {
let text: String
var body: some View {
Text(text)
}
}
Gif illustrating the problem:

SwiftUI List messed up after delete action on iOS 15

It seems that there is a problem in SwiftUI with List and deleting items. The items in the list and data get out of sync.
This is the code sample that reproduces the problem:
import SwiftUI
struct ContentView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
}
}
.animation(.easeInOut, value: popupShown)
}
}
struct MainListView: View {
#State var texts = (0...10).map(String.init)
func delete(at positions: IndexSet) {
positions.forEach { texts.remove(at: $0) }
}
var body: some View {
List {
ForEach(texts, id: \.self) { Text($0) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
If you perform a delete action on the first row and scroll to the last row, the data and list contents are not in sync anymore.
This is only happening when animation is attached to it. Removing .animation(.easeInOut, value: popupShown) workarounds the issue.
This code sample works as expected on iOS 14 and doesn't work on iOS 15.
Is there a workaround for this problem other then removing animation?
It isn't the animation(). The clue was seeing It appears that having the .animation outside of the conditional causes the problem. Moving it to the view itself corrected it to some extent. However, there is a problem with this ForEach construct: ForEach(texts, id: \.self). As soon as you start deleting elements of your array, the UI gets confused as to what to show where. You should ALWAYS use an Identifiable element in a ForEach. See the example code below:
struct ListDeleteView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
.animation(.easeInOut, value: popupShown)
}
}
}
}
struct MainListView: View {
#State var texts = (0...10).map({ TextMessage(message: $0.description) })
func delete(at positions: IndexSet) {
texts.remove(atOffsets: positions)
}
var body: some View {
List {
ForEach(texts) { Text($0.message) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
struct TextMessage: Identifiable {
let id = UUID()
let message: String
}

SwiftUI State var array not updating child views

For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.

SwiftUI .sheet Unexpectedly found nil while unwrapping an Optional value

The Problem
The following example highlights my issue better than I can explain it. I explicitly give an optional variable a value before presenting a sheet. This sheet, which requires a non-optional variable to init, doesn't register the value and says it is nil. I can't understand why this would be if I only ever call the sheet after the optional has been given a value. Any help would be greatly appreciated. Thanks!
What I have tried
In the example I replaced:
.sheet(isPresented: $showModalView, content: {
EditBookView(book: editingBook!) //Fatal error here
})
with:
.sheet(isPresented: $showModalView, content: {
if let book = editingBook {
EditBookView(book: book)
}
})
However, this just shows an empty sheet (implying that editingBook is empty). But, interestingly when I close this empty sheet and select another item in the list, the view appears as intended.
Reproducible example
import SwiftUI
struct Book: Identifiable {
var id: UUID
var title: String
init(title: String){
self.title = title
self.id = UUID()
}
}
struct ContentView: View {
#State var books = [Book]()
#State var showModalView = false
#State var editingBook: Book? = nil
var body: some View {
List{
ForEach(books){ book in
VStack(alignment: .leading){
Text(book.title)
.font(Font.title.bold())
Text("id: \(book.id.uuidString)")
.foregroundColor(.gray)
Button(action: {
editingBook = book
showModalView = true
}){
Text("Edit")
.foregroundColor(.accentColor)
}
.buttonStyle(PlainButtonStyle())
.padding(.top)
}
}
}
.padding()
.onAppear{
for i in 0...50 {
books.append(Book(title: "Book #\(i)"))
}
}
.sheet(isPresented: $showModalView, content: {
EditBookView(book: editingBook!) //Fatal error here
})
}
}
struct EditBookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit:
enum SheetChoice: Hashable, Identifiable {
case addContentView
case editContentView
var id: SheetChoice { self }
}
...
.sheet(item: $sheetChoice){ item in
switch item {
case .addContentView:
AddContentView()
.environmentObject(model)
case .editContentView:
//if let selectedContent = selectedContent {
ContentEditorView(book: selectedContent!, editingFromDetailView: false)
.environmentObject(model)
//}
}
}
Make sure you also use editingBook inside your body (not only sheet building block).
SwiftUI tracks which State variables are used in its body. When it’s not used, you might come into this weird situations when your body is called with ignored changes to that state variable.
So basically add this line at the beginning of your body:
var body: some View {
_ = editingBook
return <your view>
}
Alternatively, you can use this .sheet modifier version:
https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)
Following the answer from #msmialko, I suspect this is a compiler problem.
_ = self.<your_variable>
inside body solves the problem.
One possible workaround is moving out the sheet content into another View, and pass the Binding to the #Stete to it:
struct Book: Identifiable {
var id: UUID
var title: String
init(title: String){
self.title = title
self.id = UUID()
}
}
enum SheetChoice: Hashable, Identifiable {
case addContentView
case editContentView
var id: SheetChoice { self }
}
class MyModel: ObservableObject {
}
struct ContentView: View {
#State var books = [Book]()
#State var selectedContent: Book? = nil
#State var sheetChoice: SheetChoice? = nil
#StateObject var model = MyModel()
var body: some View {
List{
ForEach(books){ book in
VStack(alignment: .leading){
Text(book.title)
.font(Font.title.bold())
Text("id: \(book.id.uuidString)")
.foregroundColor(.gray)
Button(action: {
selectedContent = book
sheetChoice = .editContentView
}){
Text("Edit")
.foregroundColor(.accentColor)
}
.buttonStyle(PlainButtonStyle())
.padding(.top)
}
}
}
.padding()
.onAppear{
for i in 0...50 {
books.append(Book(title: "Book #\(i)"))
}
}
.sheet(item: $sheetChoice){
item in
SheetContentView(item: item, selectedContent: $selectedContent)
.environmentObject(model)
}
}
}
struct SheetContentView: View {
var item: SheetChoice
var selectedContent: Binding<Book?>
var body: some View {
switch item {
case .addContentView:
AddContentView()
case .editContentView:
ContentEditorView(book: selectedContent.wrappedValue!,
editingFromDetailView: false)
}
}
}
struct ContentEditorView: View {
var book: Book
var editingFromDetailView: Bool
var body: some View {
Text(book.title)
}
}
struct AddContentView: View {
var body: some View {
Text("AddContentView")
}
}

Conditionally Text in SwiftUI depending on Array value

I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.