I have a list of expandable items, which I made following along this post: https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-expanding-lists
Here is the code:
struct Examples: View {
private struct Example: Identifiable {
let id = UUID()
let string: String
var items: [Example]?
}
private let exampleItems: [Example] = [
Example(string: "One",
items: [Example(string: "1")]),
Example(string: "Two",
items: [Example(string: "2")]),
Example(string: "Three",
items: [Example(string: "3")]),
]
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
List(exampleItems, children: \.items) { row in
Text(row.string)
}
}
}
}
And I'd like to add a bit of haptic feedback each time a row is expanded or minimised. In UIKit you'd just pop this in tableView(_:didSelectRowAt:), and that would be that.
However I can't figure out the SwiftUI equivalent, and all my attempts to make things tappable or add buttons etc. break the main action of expanding the list row.
How does one perform an action upon a row being expanded/minimised in SwiftUI?
Related
I'm trying to make a mixed picker in swift ui. Means: first part is made of values from a constant, second part is made of core data entity values.
My Code so far
Structs
struct StandardArten: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Constants {
static let standardKontakte = [
StandardArten(name: "Hausarzt")
]
}
picker
Picker("Kategorie", selection: $kategorie) {
Text("Bitte wählen...").tag(Optional<UUID>(nil))
ForEach(Constants.standardKontakte, id: \.id) { kat in
Text(kat.name).tag(kat.id)
}
Divider()
ForEach(adressKategorien, id:\.id) { kat in
Text(kat.name ?? "ohne Titel").tag(kat.id)
}
}
The picker is displayed properly (see image), but selecting "Hausarzt" the picker "jumps back" to the old value.
So what am I doing wrong here?
Additional information:
$kategorie is declared as #State var kategorie: UUID?
struct ContentView: View {
var array = ["Fire", "Water", "Earth", "Air", "Emotion"]
#State var randomList = [String]()
var body: some View {
VStack {
List (randomList, id: \.self) { ranList in
Text(ranList)
}
Button("Add to list") {
let ranIndexNumber = Int.random(in: 0...array.count-1)
randomList.append(array[ranIndexNumber])
}
Hi everyone, I am new to the coding and to stack overflow website, I am learning SwiftUI atm and this code above was a "challenge" which is; as I tap the button, the code adds random items from array list above, to the list. I managed to write the code, but I wanted to add another function to the button;
When tapped, in addition to adding items to the list, I want it to remove all items if item count in the list exceed 10. How am I suppose to do that, I don't know/not yet learned a way to count the items in a list to use in an IF statement.
Thanks for the help in advance
P.S. if there is any way to add my code with right colors while posting, I would be grateful if you shortly describe how to do it.Thanks
struct ContentView: View {
var array = ["Fire", "Water", "Earth", "Air", "Emotion"]
#State var randomList = [String]()
var body: some View {
VStack {
List (randomList, id: \.self) { ranList in
Text(ranList)
}
Button("Add to list") {
let ranIndexNumber = Int.random(in: 0...array.count-1)
if randomList.count >= 10 {
randomList = [array[ranIndexNumber]]
} else {
randomList.append(array[ranIndexNumber])
}
}
When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.
When I delete an element of an array using onDelete(), it removes the correct item in the data but removes the last item on the UI. I saw this answer but I am already using what it recommends. Any advice?
struct ContentView: View {
#State var items = ["One", "Two", "Three"]
var body: some View {
Form{
ForEach(items.indices, id:\.self){ itemIndex in
let item = self.items[itemIndex]
EditorView(container: self.$items, index: itemIndex, text: item)
}.onDelete(perform: { indexSet in
self.items.remove(atOffsets: indexSet)
})
}
}
}
Here is the EditorView struct:
struct EditorView : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("Type response here", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
Your ForEach is looping over the indices of your array, so that is what SwiftUI is using to identify them. When you delete an item, you have one fewer index for the array, so SwiftUI interprets that as the last one has been deleted.
To do this correctly, you should be looping over a list of Identifiable items that have a unique id. Here I've created a struct called MyItem which holds the original String and a uniquely generated id. I use .map(MyItem.init) to convert the items into a [MyItem].
Also, your code needs the index in the loop, so loop over Array(items.enumerated()) which will give you an array of (offset, element) tuples. Then tell SwiftUI to use \.element.id as the id.
Note that EditorView now takes an Array of MyItem.
With these changes, SwiftUI will be able to identify the item you have deleted from the list and update the UI correctly.
struct MyItem: Identifiable {
var name: String
let id = UUID()
}
struct ContentView: View {
#State var items = ["One", "Two", "Three"].map(MyItem.init)
var body: some View {
Form{
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
EditorView(container: self.$items, index: index, text: item.name)
}.onDelete(perform: { indexSet in
self.items.remove(atOffsets: indexSet)
})
}
}
}
struct EditorView : View {
var container: Binding<[MyItem]>
var index: Int
#State var text: String
var body: some View {
TextField("Type response here", text: self.$text, onCommit: {
self.container.wrappedValue[self.index].name = self.text
})
}
}
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.