Nested If statements inside ForEach loop - swiftui

I have created a custom list. Based on the conditions, some objects don't have names. Alternatively, some objects might have more than 1 name. So, the names in my custom list are optional. I want to print out all the names and I used the following codes. The code is working but it is too heavy. Sometimes, the Xcode throws "The compiler is unable to type-check this expression in a reasonable time"
My DataArray Example:
import SwiftUI
struct DataArray: Identifiable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
public struct ListDataArray {
static var dot = [
DataArray(number: 1,
cities: "Baltimore"
name1: "A",
name2: "B"),
DataArray(number: 2,
cities: "Frederick"),
DataArray(number: 3,
cities: "Catonsville"
name1: "Aa",
name2: "Bb",
name3: "Cc",
name4: "Dd"),
]
}
import SwiftUI
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach (datas, id: \.id) { data in
if (data.name1 !=nil) {
HStack {
Image(systemName: "1.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(data.name1 ?? "")
Spacer()
}
}
if (data.name2 !=nil) {
HStack {
Image(systemName: "2.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(data.name2 ?? "")
Spacer()
}
}
if (data.name3 !=nil) {
HStack {
Image(systemName: "3.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(data.name3 ?? "")
Spacer()
}
}
if (data.name4 !=nil) {
HStack {
Image(systemName: "4.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(data.name4 ?? "")
Spacer()
}
}
}
}
}
}
}
So, for the second approach, I used nested ForEach loop like
ForEach (data, id: \.id) { data in
ForEach ([data.name1, data.name2, data.name3, data.name4], id: .\self) { d in
if (d !=nil) {
HStack {
Image(systemName: "1.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(d ?? "")
Spacer()
}
}
The issue about this nested ForEach approach is that the LazyVStack always crashes when I scroll to the bottom of the pages. Also, I don't know how to update the system images. I can use regular VStack but these arrays will be a lot so I need LazyVStack. So, please help me to do a better way to shorten the if statements. Please please don't comment if you don't know or if you are not sure what you are doing. I posted similar questions four to five times here already. They all ended up with useless comments and they are worthless for a beginner level who really try to learn something new.

Here's one solution, using my solution mentioned in the comments, using if let = ... to do optional binding (read more on that concept here https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html):
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach (datas, id: \.id) { data in
if let name1 = data.name1 {
HStack {
Image(systemName: "1.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(name1)
Spacer()
}
}
if let name2 = data.name2 {
HStack {
Image(systemName: "2.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(name2)
Spacer()
}
}
if let name3 = data.name3 {
HStack {
Image(systemName: "3.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(name3)
Spacer()
}
}
if let name4 = data.name4 {
HStack {
Image(systemName: "4.circle.fill")
.font(.title2)
.foregroundColor(.red)
Text(name4)
Spacer()
}
}
}
}
}
}
}
Here's a further refactored version that splits the inner view into a separate function so that the code isn't repeated:
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
#ViewBuilder func segment(imageName: String, text: String) -> some View {
HStack {
HStack {
Image(systemName: imageName)
.font(.title2)
.foregroundColor(.red)
Text(text)
Spacer()
}
}
}
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach (datas, id: \.id) { data in
if let name1 = data.name1 {
segment(imageName: "1.circle.fill", text: name1)
}
if let name2 = data.name2 {
segment(imageName: "2.circle.fill", text: name2)
}
if let name3 = data.name3 {
segment(imageName: "3.circle.fill", text: name3)
}
if let name4 = data.name4 {
segment(imageName: "4.circle.fill", text: name4)
}
}
}
}
}
}
I've found that when parsing SwiftUI, the compiler has trouble evaluating what would seem like simple boolean expressions. In other words, your original code should actually work, but the compiler has trouble parsing it.

Related

Binding two ForEach loop to update each item cell

This is my second post and I need your help as much as possible. I am creating a favorite button on my parent view and detail view. I need both buttons to work correspondent to each other. When I marked favorite on the ForEach loop of my parent view, I want to show the item is favorited in my detail view. Also, I can unfavorite or favorite from my detail view vice vasa. It is really hard for me to figure out how to bind those two ForEach loops. Below I provide an example of my codes. If you want to test with my full code, you can access it here: Making favorite button from several layers and binding two list using EnvironmentObject
struct Data: Identifiable {
let id = UUID()
let number: Int
var name1: String
let name2: String
}
public struct DataList {
static var dot = [
Data(number: 1,
name1: "Pasian Phatna",
name2: "Praise God, from whom All Blessings Flow"),
Data(number: 2,
name1: "Itna Kumpi, Ka Tuu-Cing Pa",
name2: "The King of Love My Shephaerd Is (Dominus Regit Me)"),
Data(number: 3,
name1: "Kumpipa Bia Un",
name2: "O Worship the King"),
Data(number: 4,
name1: "Pa Tung Min Than'na Om Hen",
name2: "Gloria Patri (1st Tune)"),
Data(number: 5,
name1: "Pa Tung Min Than'na Om Hen",
name2: "Gloria Patri (2nd Tune)")
]
}
struct ParentView: View {
#State var datas: [Data] = DataList.dot
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(spacing: 5) {
ForEach (datas, id: \.id) { data in
MainData(data: data)
Divider()
.padding(.all)
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct MainData: View {
#State var data: Data
#State var selectedFavoriteSong: Bool = false
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.number)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name1)
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
Text(data.name2)
.font(.title3)
.fontWeight(.medium)
.foregroundColor(.secondary)
.italic()
}
.padding(.horizontal)
.multilineTextAlignment(.center)
}
}
Please consider, the Search() below will pop up when I tapped the search icon (which is not presented here). My point is the Search() is not directly connect to the ParentView() but the DetailView() is embedded in the Search().
struct Search: View {
#State var datas: [Data] = DataList.dot
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach (datas, id: \.id) { data in
NavigationLink(
destination: DetailView(data: data),
label: {
Text("Search")
})
}
}.padding(.horizontal)
}
}
}
}
struct DetailView: View {
#State var data: Data
#State var selectedFavoriteSong: Bool = false
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.name1)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name2)
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
}
.padding(.horizontal)
.multilineTextAlignment(.center)
Spacer()
}
}
So, I want to connect the parent view and the detail view with some kind of binding property. But there is impossible to connect these two. I can store
#State var selectedFavoriteSong: Bool = false
inside the EnvironmentObject. But when I click favorite, all the items inside the ForEach loop are selected. Please help me on this issue. If you need a full code, the above link will direct to my first post. Thank you.
I'd suggest storing all of your data in an ObservableObject that is owned by the parent view and then can get passed into subviews (either explicitly or via an EnvironmentObject):
class DataSource : ObservableObject {
#Published var data : [Data] = DataList.dot
#Published var favoritedItems: Set<UUID> = []
func favoriteBinding(forID id: UUID) -> Binding<Bool> {
.init {
self.favoritedItems.contains(id)
} set: { newValue in
if newValue {
self.favoritedItems.insert(id)
} else {
self.favoritedItems.remove(id)
}
}
}
}
For example:
struct ParentView : View {
#StateObject var dataSource = DataSource()
var body: some View {
VStack {
Search(dataSource: dataSource)
}
}
}
Note that the data source stores a list of IDs that have been favorited. It uses a custom binding that can pass the boolean value down to a detail view:
struct Search: View {
#ObservedObject var dataSource : DataSource
var body: some View {
NavigationView {
ScrollView (.vertical, showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach (dataSource.data, id: \.id) { data in
NavigationLink(
destination: DetailView(data: data,
selectedFavoriteSong: dataSource.favoriteBinding(forID: data.id)),
label: {
Text(data.name1)
})
}
}.padding(.horizontal)
}
}
}
}
struct DetailView: View {
var data : Data
#Binding var selectedFavoriteSong : Bool
var body: some View {
HStack {
Button(action: {
self.selectedFavoriteSong.toggle()
}, label: {
if self.selectedFavoriteSong {
Image(systemName: "suit.heart.fill")
.foregroundColor(.red)
.padding(.horizontal)
} else {
Image(systemName: "suit.heart")
.padding(.horizontal)
}
})
Spacer()
Text("\(data.name1)")
Spacer()
}
.padding(.top)
VStack {
Text(data.name2 ?? "")
.font(.title2.smallCaps())
.fontWeight(.bold)
.foregroundColor(.primary)
}
.padding(.horizontal)
.multilineTextAlignment(.center)
Spacer()
}
}

Nesting ForEach loop and LazyVStack error

I have encountered two issues below my codes. They should be a simple fix but I couldn't figure them out by myself.
The first issue is I cannot pass my nested ForEach loop to change the system images. The reason I used for nested ForEach loop is the Xcode always throw the error "Expression was too complex to be solved in reasonable time". Here is the original link for this problem.
Nested If Statement in ForEach loop
import SwiftUI
struct HymnLyrics: View {
let verseImages: [String] = ["2.circle.fill", "3.circle.fill", "4.circle.fill", "5.circle.fill"]
var lyrics: [Lyric] = LyricList.hymnLa
#AppStorage("fontSizeIndex") var fontSizeIndex = Int("Medium") ?? 18
#AppStorage("fontIndex") var fontIndex: String = ""
#AppStorage("showHVNumbers") var showHVNumbers: Bool = true
#AppStorage("scrollToIndex") var scrollToIndex: Int?
var body: some View {
ScrollView {
ScrollViewReader { proxy in
LazyVStack(spacing: 10) {//LazyVStack is crash
ForEach (lyrics, id: \.id) { lyric in
Group {
HStack {
if showHVNumbers {
Image(systemName: "1.circle.fill")
.font(.title2)
.foregroundColor(.red)
}
Text(lyric.verse1)
.font(Font.custom(fontIndex, size: CGFloat(fontSizeIndex)))
Spacer()
}
ForEach ([lyric.verse2, lyric.verse3, lyric.verse4, lyric.verse5], id: \.self) { verseCount in
if (verseCount != nil) {
HStack {
if showHVNumbers {
Image(systemName: verseImages[verseCount])// Here is the error, it simply not works.
.font(.title2)
.foregroundColor(.red)
}
Text(verseCount ?? "")
.font(Font.custom(fontIndex, size: CGFloat(fontSizeIndex)))
Spacer()
}
}
}
}
.foregroundColor(.primary)
.padding(.horizontal, 10.0)
.padding(.bottom)
Divider()
}
.onChange(of: scrollToIndex, perform: { value in
proxy.scrollTo(value, anchor: .top)
})
}
}
}
}
}
The second issue is the app crashed when I scroll down to the bottom. When I replace LazyVStack with regular VStact, it works. But I want to use LazyVStack in this case. Please guide me for both of the issues. Thanks you in advance.

Optimize list searches

How can I optimize the searches in the list. I have two thousand records. I don't want to do a search through NSPredicate, because I want to pass what is in the field through a function that cleans up the numbers and reduces the letters, before comparing. Can you somehow give a delay so that it does not search immediately but after some time or if the user finishes typing. I also heard about something like Combine, but I have no idea how to use it.
Songbook List
import CoreData
import SwiftUI
struct SongbookView: View {
#State var searchText: String = ""
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
SearchBar(text: $searchText)
Spacer()
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
.listStyle(InsetListStyle())
.animation(.default)
}
.padding(.top, 10)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Śpiewnik")
.font(.system(size: 20))
.bold()
}
}
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
Search Bar
import SwiftUI
struct SearchBar: View {
#Binding var text: String
#State var isEditing = false
var body: some View {
HStack {
TextField("Szukaj ...", text: $text)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 8)
}
}
}
)
.padding(.horizontal, 10)
.onTapGesture {
self.isEditing = true
}
if isEditing {
Button(action: {
self.isEditing = false
self.text = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}) {
Text("Anuluj")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
.animation(.default)
}
}
}
}
whenever you change the text in your SearchBar (that is every character you type),
the SongbookView is updated
because you are using a binding for text. What you want is to do the update
only once when you press return. There are many ways to do this. A quick way to do this and keep your binding setup, is:
struct SearchBar: View {
#Binding var text: String
#State var txt: String = "" // <--- here a temp var
#State var isEditing = false
var body: some View {
HStack {
TextField("Szukaj ...", text: $txt) // <--- here
.onSubmit {
text = txt // <--- here only update on return press
}
.padding(7)
....
.onAppear {
txt = text // <--- here if needed
}
If you are using ios-14, use
TextField("Szukaj ...", text: $txt, onCommit: { // <--- here
text = txt // <--- here
})

SWIFTUI - creating a ForEach with Searchbar, text and button

I am building a list which should have more than 100 elements (in the future) but the alignment seems not to work..
Currently i have this...
I think it looks pretty nice atm - but the problem is, when you clicked an element (it doesnt matter which one) every element will be clicked... I tested this by print command.
The searchbar works pretty fine, except from hiding the head - but that is not the problem right now..
When you search for an item, the correct item will be displayed and by clicking on it, the correct command appears...
I am looking for my mistake, but i cant find it.. Maybe you guys can help me :-)
This is my code...
struct RecipeIngredientsView: View {
let myArray = ["Dennis", "Tessa", "Peter", "Anna", "Tessa", "Klaus", "Xyan", "Zuhau", "Clown", "Brot", "Bauer"]
#State private var searchText = ""
#State private var showCancelButton: Bool = false
var body: some View {
VStack
{
HStack
{
HStack
{
Image(systemName: "magnifyingglass")
TextField("Suche", text: $searchText, onEditingChanged: { isEditing in self.showCancelButton = true}, onCommit: {
print("onCommit")
}).foregroundColor(.primary)
Button(action: {
self.searchText = searchText
}){
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.secondarySystemBackground))
.cornerRadius(10.0)
if showCancelButton {
Button("Abbrechen")
{
UIApplication.shared.endEditing(_force: true)
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
.padding(.horizontal)
.navigationBarHidden(showCancelButton)
List {
VStack{
ForEach(myArray.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self)
{
searchText in VStack {
CardView(searchText: searchText)
}
}
Spacer()
}
}
.navigationBarTitle(Text("Suche"))
.resignKeyboardOnDragGesture()
}
}
}
struct CardView : View
{
#State var searchText = ""
var body : some View
{
HStack{
Text(searchText)
Spacer()
Button(action: {print("\(searchText) btn pressed!")})
{
Image(systemName: "circle")
.frame(width:25, height:25)
.clipShape(Circle())
}
}
}
}
Thank you guys & merry x-mas!:-)
This happened because of :
List {
VStack{
//.... ForEach loop
}
}
Like that the list contains just one element , which is the VStack
To avoid this your code should be like that :
List {
ForEach(myArray.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self)
{ searchText in
CardView(searchText: searchText)
}
}

How to create a custom delete button without using the slide to delete that comes with swiftUI I am not using list, just using a foreach loop

Since, the onDelete and onMove are features of List/form I cannot use them when I have custom interfaces without them. I have used a VStack inside a ForEach. I am quite new to swiftUI and unsure on how I can implement custom code for onDelete and onMove.
Here's my code:
struct Trying: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
numbers.remove(atOffsets: offsets)
}
}
The way it works right now:
Here is a simple demo of possible approach to implement custom delete (of course with move it would be more complicated due to drag/drop, but idea is the same). Tested with Xcode 12 / iOS 14.
struct DemoCustomDelete: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
.overlay(
DeleteButton(number: number, numbers: $numbers, onDelete: removeRows)
, alignment: .topTrailing)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
withAnimation {
numbers.remove(atOffsets: offsets)
}
}
}
struct DeleteButton: View {
#Environment(\.editMode) var editMode
let number: Int
#Binding var numbers: [Int]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
Based on #Asperi's answer, I just generalized it to accept any Equatable sequence.
struct DeleteButton<T>: View where T: Equatable {
#Environment(\.editMode) var editMode
let number: T
#Binding var numbers: [T]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
I recently had the need to delete a row and I couldn't use a LIST. Instead I had a scroll view... But I was able to implement the edit to simulate the same onDelete behavior as if it was a list. Initially I couldn't get my code to work. It wasn't until I closely examined my implementation and experimented that I stumbled on why mine worked. I'm coding for an iPad so my NavigationView uses,
.navigationViewStyle(StackNavigationViewStyle())
Once I added this to the struct's NavigationView, when you click on the EditButton it activates editMode?.wrappedValue to .active / .inactive
Below is my implementation for the code sample above...
struct Trying: View {
#State var num: Int = 0
#Environment(\.editMode) var editMode
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack {
ForEach(numbers, id: \.self) { number in
HStack {
if editMode?.wrappedValue == .active {
Button(action: { num = number
removeRows(numbr: num)
}, label: {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
})
} // END IF editMode?wrappedValue == .active
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.red)
}
}
// .onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
// FOR SOME REASON THIS ALLOWS THE EditButton() to activate editMode without a LIST being present.
.navigationViewStyle(StackNavigationViewStyle())
}
func removeRows(numbr: Int) {
print("removing \(numbr)")
}
}
It looks like: