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)
}
}
Related
Whenever a message is received from the backend, the app should automatically scroll to the beginning of the message. I cannot find a way for this to work. I've tried the stackoverflow solutions here, here and here to no avail.
Here's my code
struct ContentView: View {
#State private var inputText = ""
#State private var messages: [Message] = []
#State private var showThumbsDownSheet = false
#State private var feedbackText = ""
#State private var showThanksForFeedback = false
var body: some View {
VStack {
NavigationView {
ScrollViewReader { scrollView in
List {
ForEach(messages, id: \.id) { message in
Text(message.text)
if !message.isFromUser && message.isReplyFromBackEnd {
HStack {
ThumbsUpButton(message: message)
Spacer()
ThumbsDownButton(message: message, showThumbsDownSheet: $showThumbsDownSheet, reviewText: $feedbackText)
}
}
}
}
.onChange(of: messages) { value in
scrollView.scrollTo(self.messages.count - 1)
}
}
.navigationTitle("Ask Tais")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
Button(action: { exit(0) }) {
Label("Quit app", systemImage: "")
}
}
label: {
Label("Add", systemImage: "ellipsis.circle")
}
}
}
}
.onAppear {
// Add a "Hello" message to the list of messages when the app is first opened
self.messages.append(Message(id: UUID(), text: "Hello!", isFromUser: false, isReplyFromBackEnd: false, responseId: "0"))
}
.frame(maxHeight: .infinity)
HStack {
TextField("What do you want to say?", text: $inputText)
Button(action: {
self.messages.append(Message(id: UUID(), text: self.inputText, isFromUser: true, isReplyFromBackEnd: false, responseId:"0"))
sendRequest(with: self.inputText) { responseText, id in
self.messages.append(Message(id: UUID(), text: responseText, isFromUser: false, isReplyFromBackEnd: true, responseId: id))
}
self.inputText = ""
}) {
Text("Ask Tais")
}
}
.padding()
}
}
}
ScrollViewReader and scrollTo does not work on List with changing content. You have to use ScrollView instead of List and build your own list design.
Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
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()
}
}
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
})
i would like to remove the space between my back button ("Rezept hinzufügen") and my navigationbarTitle ("Suche")... I cant figure out why this space is there so i need your swarm intelligence. :)
What did i try?
Working with a ZStack, maybe there is NavigationView from before which is doin this bug
Add Spacer()
Recreate the whole view
Now I stuck...
I think the easiest way is to show you my problem with an video...
Here you can see my problem
Here is my code...
import SwiftUI
extension UIApplication
{
func endEditing(_force : Bool)
{
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(_force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier
{
var gesture = DragGesture().onChanged{_ in UIApplication.shared.endEditing(_force: true)}
func body(content: Content) -> some View
{
content.gesture(gesture)
}
}
extension View
{
func resignKeyboardOnDragGesture() -> some View
{
return modifier(ResignKeyboardOnDragGesture())
}
}
/*
Zutaten pflegen Button, zum hinzufügen von Zutaten zu einem Rezept.
**/
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 {
NavigationView
{
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)
//Gefilterte Liste der Namen aus meinem Array
List {
ForEach(myArray.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self)
{
searchText in Text(searchText)
}
}
.navigationBarTitle(Text("Suche"))
.resignKeyboardOnDragGesture()
}
}
}
}
Thanks for your help!
:-)
Just remove redundant NavigationView - it is needed only one in same view hierarchy, and obviously there is already some in parent view
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 {
NavigationView // << remove this one !!
{