How to redraw a child view in SwiftUI? - swiftui

I have a ContentView that has a state variable "count". When its value is changed, the number of stars in the child view should be updated.
struct ContentView: View {
#State var count: Int = 5
var body: some View {
VStack {
Stepper("Count", value: $count, in: 0...10)
StarView(count: $count)
}
.padding()
}
}
struct StarView: View {
#Binding var count: Int
var body: some View {
HStack {
ForEach(0..<count) { i in
Image(systemName: "star")
}
}
}
}
I know why the number of stars are not changed in the child view, but I don't know how to fix it because the child view is in a package that I cannot modify. How can I achieve my goal only by changing the ContentView?

You are using the incorrect ForEach initializer, because you aren't explicitly specifying an id. This initializer is only for constant data, AKA a constant range - which this data isn't since count changes.
The documentation for the initializer you are currently using:
It's important that the id of a data element doesn't change unless you
replace the data element with a new data element that has a new
identity. If the id of a data element changes, the content view
generated from that data element loses any current state and animations.
Explicitly set the id like so, by adding id: \.self:
struct StarView: View {
#Binding var count: Int
var body: some View {
HStack {
ForEach(0 ..< count, id: \.self) { _ in
Image(systemName: "star")
}
}
}
}
Similar answer here.

Related

Get selected item in SwiftUI list without using a navigation link

I'm writing a SwiftUI Mac app that is similar to a kanban board. The app has three lists: Todo, Doing, and Done. At the bottom of each list is a button to move a task to another list. For example the todo list has a Start Doing button. Selecting a task from the todo list and clicking the button should move the task from the todo list to the doing list.
Every SwiftUI list selection example I have seen uses a navigation link. Selecting a list item takes you to another view. But I don't want to want to navigate to another view when selecting a list item. I want the selected task so I can change its status and move it to the correct list when clicking the button.
Here's the code for one of my lists.
struct TodoList: View {
// The board has an array of tasks.
#Binding var board: KanbanBoard
#State private var selection: Task? = nil
#State private var showAddSheet = false
var body: some View {
VStack {
Text("Todo")
.font(.title)
List(todoTasks, selection: $selection) { task in
Text(task.title)
}
HStack {
Button(action: { showAddSheet = true }, label: {
Label("Add", systemImage: "plus.square")
})
Spacer()
Button(action: { selection?.status = .doing}, label: {
Label("Start Doing", systemImage: "play.circle")
})
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskView(board: $board)
}
}
var todoTasks: [Task] {
// Task conforms to Identifiable.
// A task has a status that is an enum: todo, doing, or done.
return board.tasks.filter { $0.status == .todo}
}
}
When I click on a list item, it is not selected.
How do I get the selected item from the list without using a navigation link?
Workaround
Tamas Sengel's answer led me to a workaround. Give each list item a Start Doing button so I don't have to track the selection.
List(todoTasks, id: \.self) { task in
HStack {
Text(task.title)
Button {
task.status = .doing
} label: {
Text("Start Doing")
}
}
}
The workaround helps for my specific case. But I'm going to keep the question open in hopes of an answer that provides a better alternative to using a button for people who want a way to get the selected list item.
Use a Button in the List and in the action, set a #State variable to the current list item.
#State var currentTask: Task?
List(todoTasks, id: \.self) { task in
Button {
currentTask = task
} label: {
Text(task.title)
}
}
Use .environment(\.editMode, .constant(.active)) to turn on selecting capability.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// This is a walk-around: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(multiSelection)
})
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Put your 3 List with same data array but filtering by status on each one something like:
task.filter({ $0.status == .toDo })
Then on your row add the modifier .onTapGesture be sure to cover all the available space.
Inside the code block introduce your logic or func to change the item status. changeTaskStatus(item: task)

SwiftUI state discarded when scrolling items in a list

I'm very new to Swift and SwiftUI so apologies for the very basic question. I must be misunderstanding something about the SwiftUI lifecycle and it's interaction with #State.
I've have a list, and when you click on the row, it increments a counter. If I click on some row items to increment the counter, scroll down, and scroll back up again - the state is reset to 0 again. Can anyone point out where I'm going wrong? Many thanks.
struct TestView : View {
#State private var listItems:[String] = (0..<50).map { String($0) }
var body: some View {
List(listItems, id: \.self) { listItem in
TestViewRow(item: listItem)
}
}
}
struct TestViewRow: View {
var item: String
#State private var count = 0
var body: some View {
HStack {
Button(item, action: {
self.count += 1
})
Text(String(self.count))
Spacer()
}
}
}
Items in a List are potentially lazily-loaded, depending on the os (macOS vs iOS), length of the list, number of items on the screen, etc.
If a list item is loaded and then its state is changed, that state is not reassigned to the item if that item has been since unloaded/reloaded into the List.
Instead of storing #State on each List row, you could move the state to the parent view, which wouldn't be unloaded:
struct ContentView : View {
#State private var listItems:[(item:String,count:Int)] = (0..<50).map { (item:String($0),count:0) }
var body: some View {
List(Array(listItems.enumerated()), id: \.0) { (index,item) in
TestViewRow(item: item.item, count: $listItems[index].count)
}
}
}
struct TestViewRow: View {
var item: String
#Binding var count : Int
var body: some View {
HStack {
Button(item, action: {
count += 1
})
Text(String(count))
Spacer()
}
}
}

SwiftUI - Crash in Refresh after OnDelete

With the code below I have one row in the second section of a form. When the onDelete executes, I can see (via breakpoints) that it successfully removes the record from the array. The body gets refreshed as it's a state variable and then it crashes with a Fatal error: Index out of range: file. The crash occurs when it tries to do the ForEach again which at this point should show nothing since the array is empty.
struct BuyerCheckout: View {
#State private var subTotal:Double!
#State private var tax:Double!
#State private var fees = [FeeModel(type: "", amount: 0.00)]
var body: some View {
NavigationView {
Form {
Section(header: Text("Gross Amounts")) {
HStack {
Text("Subtotal: $")
TextField("Purchase Subtotal", value: $subTotal, formatter: NumberFormatter.currency)
.keyboardType(.decimalPad)
}
HStack {
Text("Tax: $")
TextField("Purchase Tax", value: $tax, formatter: NumberFormatter.currency)
.keyboardType(.decimalPad)
}
}
Section(header: Text("Other Fees")) {
ForEach(fees.indices, id: \.self) {
FeeCell(fee: self.$fees[$0])
}
.onDelete(perform: removeRows)
}
}.gesture(DragGesture().onChanged { _ in
UIApplication.shared.windows.forEach { $0.endEditing(false) }
})
.padding()
.navigationBarTitle("Checkout")
.onAppear {
UITableViewCell.appearance().selectionStyle = .none
}
}
}
I am also doing the ForEach with indexes this way as the variable represents a binding for the FeeCell. I cannot pass a binding with the ForEach iterator of the array. Not sure this is relevant, but I thought I'd mention.

SwiftUI, unique stepper for items in a list view

I'm wanting to display a list of an array fetched from an NSManagedObject.
Then in that list have a stepper for each item that will eventually same the incremented amount to a property of the NSManagedObject.
My code so far:
struct ExtraIncomeAdviceView: View {
var incomes:FetchedResults<Income>
var savingsGoals:FetchedResults<SavingsGoal>
var presentationModeAddIncomeView: Binding<PresentationMode>
#Environment(\.presentationMode) var presentationModeAdvice: Binding<PresentationMode>
#State private var increment:Int = 0
var body: some View {
VStack{
Text("So you got an extra £\(String(format: "%0.2f",getLatestExtraIncome()))?").font(.headline)
VStack{
HStack{
Text("For your goals, set aside:").font(.footnote).foregroundColor(.gray)
Spacer()
}
ForEach(self.savingsGoals) { goal in
HStack{
Spacer()
Stepper("Add to \(goal.name!): \(goal.progressAmount!)", onIncrement: {
self.increment += 1
print("Adding to age")
}, onDecrement: {
self.increment -= 1
print("Subtracting from age")
})
}
}
}.padding()
.padding(.vertical, 40)
Button(action: {
self.presentationModeAdvice.wrappedValue.dismiss()
self.presentationModeAddIncomeView.wrappedValue.dismiss()
}) {
Text("Got it!")
.foregroundColor(.white)
.padding(15)
.background(Color .orange)
.cornerRadius(40)
}
}.padding()
}
}
The problem with this is that every goal thats displayed in the ForEach reads and writes to the same variable. I'm stuck on how to somehow dynamically create new variables to read and write to for each item in the array.
In your case, I would make a custom view struct that is called within the ForEach named GoalRow() or something applicable that contains the your HStack, Stepper, and local increment variable. Then, you can handle the stepped value appropriately within the GoalRow() struct and clean up your code a bit.
ForEach(self.savingsGoals) { goal in
GoalRow(goal: goal)
}
and then within GoalRow:
struct GoalRow: View {
var goal: YourObject
#State private var increment: Int = 0
var body: some View {
HStack(alignment: .leading, spacing: 20) {
Stepper("Add to \(goal.name!): \(goal.progressAmount!)", onIncrement: {
self.increment += 1
// edit your proposed progress amount here
print("Adding to age")
}, onDecrement: {
self.increment -= 1
// edit your proposed progress amount here
print("Subtracting from age")
})
}
Apple mentioned this in their tutorial. See the example for LandmarkRow in this tutorial Building Lists and Navigation

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.