swiftui+combine: why isFavoriteO changed when scroll the LazyVGrid? - swiftui

I have a LazyVGrid, every item with is favorite button. and use combine to debounce user input($isFavoriteI), when isFavoriteO changed, then modify the items.
it works fine, but when i scroll the list, log will print: "X, isFavorite changed as false/true)", what cause isFavoriteO changed and why? because of item reusing in list? how to avoid it?
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
import SwiftUI
import Combine
struct Item {
var index: Int
var favorite: Bool
}
var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
// Item(index: 8, favorite: true),
// Item(index: 9, favorite: false),
// Item(index: 10, favorite: true),
// Item(index: 11, favorite: false),
// Item(index: 12, favorite: true),
// Item(index: 13, favorite: false),
// Item(index: 14, favorite: true),
// Item(index: 15, favorite: false),
// Item(index: 16, favorite: true),
// Item(index: 17, favorite: false),
// Item(index: 18, favorite: true),
// Item(index: 19, favorite: false),
]
struct ViewModelInListTestView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(items, id: \.index) { item in
ItemView(item: item)
}
}
}.navigationTitle("ViewModel In List")
}
}
struct ItemView: View {
let item: Item
#ObservedObject var viewModel: ViewModel
init(item: Item) {
print("ItemView.init, \(item.index)")
self.item = item
self.viewModel = ViewModel(item: item)
}
var body: some View {
HStack {
Text("index \(item.index)")
Spacer()
Image(systemName: viewModel.isFavoriteI ? "heart.fill" : "heart")
.foregroundColor(viewModel.isFavoriteI ? .red : .white)
.padding()
.onTapGesture { onFavoriteTapped() }
.onChange(of: viewModel.isFavoriteO) { isFavorite in
setFavorite(isFavorite)
}
}
.frame(width: 200, height: 150)
.background(Color.gray)
}
func onFavoriteTapped() {
viewModel.isFavoriteI.toggle()
}
func setFavorite(_ isFavorite: Bool) {
print("index \(item.index), isFavorite changed as \(isFavorite)")
items[item.index].favorite = isFavorite
}
class ViewModel: ObservableObject {
#Published var isFavoriteI: Bool = false
#Published var isFavoriteO: Bool = false
private var subscriptions: Set<AnyCancellable> = []
init(item: Item) {
print("ViewModel.init, \(item.index)")
let isFavorite = item.favorite
isFavoriteI = isFavorite; isFavoriteO = isFavorite
$isFavoriteI
.print("index \(item.index) isFavoriteI:")
.dropFirst()
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.eraseToAnyPublisher()
.print("index \(item.index) isFavoriteO:")
.receive(on: DispatchQueue.main)
.assign(to: \.isFavoriteO, on: self)
.store(in: &subscriptions)
}
}
}
update # 4.15
according to #Cenk Bilgen, i re-write the code, but strange thing happened. print("set favorite as (favorite)") will not present if adding removeDuplicates. why?
import SwiftUI
import Combine
struct Item: Identifiable {
var index: Int
var favorite: Bool
var id: Int { index }
}
class Model: ObservableObject {
#Published var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
]
}
struct ViewModelInListTestView: View {
#StateObject var model = Model()
var body: some View {
print("ViewModelInListTestView refreshing"); return
ScrollView(showsIndicators: false) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(model.items.indices) { index in
ItemView(item: model.items[index])
.environmentObject(model)
}
}
}.navigationTitle("ViewModel In List")
}
struct ItemView: View {
#EnvironmentObject var model: Model
let item: Item
#State private var updateFavourite = PassthroughSubject<Bool, Never>()
#State private var favorite: Bool = false
init(item: Item) {
self.item = item
self._favorite = State(initialValue: item.favorite)
}
var body: some View {
print("ItemView \(item.index) refreshing"); return
HStack {
Text("index \(item.index)")
Spacer()
Image(systemName: favorite ? "heart.fill" : "heart")
.foregroundColor(favorite ? .red : .white)
.padding()
.onTapGesture {
favorite.toggle()
updateFavourite.send(favorite)
}
.onReceive(
updateFavourite
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
// .removeDuplicates() <------ HERE
// .eraseToAnyPublisher()
) { favorite in
print("set favorite as \(favorite)")
model.items[item.index].favorite = favorite
}
}
.frame(width: 200, height: 150)
.background(Color.gray)
}
}
}

struct Item: Identifiable {
var index: Int
var favorite: Bool
var id: Int { index }
}
class Model: ObservableObject {
#Published var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
]
}
struct SimplerView: View {
#StateObject var model = Model()
var body: some View {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(items.indices) { index in
ItemView(item: $model.items[index])
.environmentObject(model)
}
}
}.navigationTitle("ViewModel In List")
}
struct ItemView: View {
#EnvironmentObject var model: Model
#Binding var item: Item
#State private var updateFavourite = PassthroughSubject<Bool, Never>()
var body: some View {
HStack {
Text("index \(item.index)")
Spacer()
Image(systemName: item.favorite ? "heart.fill" : "heart")
.foregroundColor(item.favorite ? .red : .white)
.padding()
.onTapGesture {
updateFavourite.send(item.favorite)
}
.onReceive(updateFavourite
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)) { value in
item.favorite = !value
}
}
.frame(width: 200, height: 150)
.background(Color.gray)
}
}
}

I don't really understand how isFavoriteI and isFavoriteO work, but you
could try this:
in ItemView remove the
.onChange(of: viewModel.isFavoriteO) { isFavorite in
setFavorite(isFavorite)
}
and change:
func onFavoriteTapped() {
viewModel.isFavoriteI.toggle()
print("\(item.index), isFavorite changed as \(viewModel.isFavoriteI)")
items[item.index].favorite = viewModel.isFavoriteI
}

The best of doing this is in this way in down code, SwiftUI would stop unnecessary render, and it will render if it needs!
You had some issue that you should use id for your items, plus combine does not work well in this case, so use better and easier way in down:
import SwiftUI
struct ContentView: View {
#StateObject var itemModel: ItemModel = sharedItemModel
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(Array(itemModel.items.enumerated()), id:\.element.id) { (offset, element) in
ItemView(index: offset, favorite: element.favorite)
}
}
}
Button("append new random element") { itemModel.items.append(Item(favorite: Bool.random())) }
.padding()
}
}
struct ItemView: View {
let index: Int
let favorite: Bool
init(index: Int, favorite: Bool) {
self.index = index
self.favorite = favorite
}
var body: some View {
print("rendering item: " + index.description)
return HStack {
Text("index " + index.description)
.bold()
.padding()
Spacer()
Image(systemName: favorite ? "heart.fill" : "heart")
.foregroundColor(Color.red)
.padding()
.onTapGesture { sharedItemModel.items[index].favorite.toggle() }
}
.frame(width: 200, height: 150)
.background(Color.gray)
.cornerRadius(10.0)
}
}
struct Item: Identifiable {
let id: UUID = UUID()
var favorite: Bool
}
class ItemModel: ObservableObject {
#Published var items: [Item] = [Item]()
}
let sharedItemModel: ItemModel = ItemModel()

Related

How can I change button status in SwiftUI?

I am trying to change the color programmatically of some buttons in SwiftUI.
The buttons are stored in a LazyVGrid. Each button is built via another view (ButtonCell).
I'm using a #State in the ButtonCell view to check the button state.
If I click on the single button, his own state changes correctly, just modifying the #State var of the ButtonCell view. If I try to do the same from the ContentView nothing is happening.
This is my whole ContentView (and ButtonCell) view struct:
struct ContentView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<10) { number in
ButtonCell(value: number + 1)
}
}
}
Button(action: {
ButtonCell(value: 0, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
struct ButtonCell: View {
var value: Int
#State var isEnabled:Bool = false
var body: some View {
Button(action: {
print (value)
print (isEnabled)
isEnabled = true
}) {
Rectangle()
.foregroundColor(isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(value)").foregroundColor(.white)
)
}
}
}
}
How I may change the color of a button in the LazyVGrid by clicking the "Change isEnabled state" button?
You need a different approach here. Currently you try to change the State of ButtonCell from the outside. State variables should always be private and therefore should not be changed from outside. You should swap the state and parameters of ButtonCell into a ViewModel. The ViewModels then are stored in the parent View (ContentView) and then you can change the ViewModels and the child views automatically update. Here is a example for a ViewModel:
final class ButtonCellViewModel: ObservableObject {
#Published var isEnabled: Bool = false
let value: Int
init(value: Int) {
self.value = value
}
}
Then store the ViewModels in the ContentView:
struct ContentView: View {
let buttonViewModels = [ButtonCellViewModel(value: 0), ButtonCellViewModel(value: 1), ButtonCellViewModel(value: 2)]
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<3) { index in
ButtonCell(viewModel: buttonViewModels[index])
}
}
}
Button(action: {
buttonViewModels[0].isEnabled.toggle()
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
}
And implement the ObservedObject approach in ButtonCell.
struct ButtonCell: View {
#ObservedObject var viewModel: ButtonCellViewModel
var body: some View {
Button(action: {
print (viewModel.value)
print (viewModel.isEnabled)
viewModel.isEnabled = true
}) {
Rectangle()
.foregroundColor(viewModel.isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(viewModel.value)").foregroundColor(.white)
)
}
}
}

How do I have private state variables for a childview in swiftui?

So I am creating this app where a user chooses the multiplication table he wishes to practice on, and the number of questions on the first view, then, by pressing a button,
it goes on to the next view which passes this data, which will then create a bank of questions for him to do
There is an error saying i cannot pass a private variable, why is this so? I have attached my code for reference
import SwiftUI
struct ContentView: View {
#State private var multiplicationTable = 1
#State private var amountQuestions = 1
let multiplicationTables = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let amountQuestionss = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var body: some View {
NavigationView {
VStack {
Picker(selection: $multiplicationTable, label: Text("multiplicationTable")) {
ForEach(0 ..< 10) {num in
Text("\(multiplicationTables[num])")
}
}
.padding()
.padding()
Text("Choose number of Questions")
Picker("Select number of questions", selection: $amountQuestions) {
ForEach(0 ..< 10) {num in
Text("\(amountQuestionss[num])")
}
}
NavigationLink(destination: kek(One: multiplicationTable, Two: amountQuestions) .navigationBarHidden(true)) {
Button ("GO") {
}
.padding(50)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
.navigationTitle("Choose Multiplication Table")
.navigationBarTitleDisplayMode(.inline)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
this is the first view, here's the second view
import SwiftUI
struct kek: View {
let One : Int
let Two : Int
#State var Question : String
#State var answer = ""
#State var Three = 0
#State var Four = 0
func nextQuestion(){
Three = One
Four = Int.random(in: 0...10)
Question = "\(Three) * \(Four)"
}
var body: some View {
VStack {
Text("Question: What is \(Question)?")
Form {
Section {
TextField("Amount", text: $answer)
.keyboardType(.decimalPad)
}
}
Button ("Next") {
}
.padding(50)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
}
struct kek_Previews: PreviewProvider {
static var previews: some View {
kek(One: 1, Two: 2)
}
}
I'm not sure where you're getting the error as it is ok to pass a variable into another view. I modified your code to illustrate what I believe you're trying to achieve.
struct ContentView: View {
#State private var multiplicationTable = 1
#State private var amountQuestions = 1
let multiplicationTables = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let numberOfQuestions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var body: some View {
NavigationView {
VStack {
Picker("Multiplication Table", selection: $multiplicationTable) {
// There was an "off by one" issue in the kek View which I resolved by using the value of the array itself for the selection. Not the location in the array.
ForEach(multiplicationTables, id: \.self) { num in
Text("\(num)")
}
}
.pickerStyle(.segmented)
.padding([.top, .bottom], 20)
/*
Don't use
.padding()
.padding()
Instead you can specify how large you'd like the padding to be around the view.
*/
Text("Choose number of Questions")
Picker("Select number of questions", selection: $amountQuestions) {
ForEach(0 ..< numberOfQuestions.count) {num in
Text("\(numberOfQuestions[num])")
}
}
NavigationLink {
kek(one: multiplicationTable, two: amountQuestions)
} label: {
// A NavigationLink is clickable by default, so you don't need to place a button inside of it.
// Here I used Text and formatted it just as you had with your Button.
Text("Go")
.padding(50)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
.navigationTitle("Choose Multiplication Table")
.navigationBarTitleDisplayMode(.inline)
}
}
}
}
struct kek: View {
let one: Int
let two: Int // note: It doesn't look like you're actually using this property. Consider removing it.
#State private var answer = ""
#State private var randomNumber = 0
#State private var three = 0
#State private var question = ""
func nextQuestion(){
three = one
randomNumber = Int.random(in: 0...10)
question = "\(three) * \(randomNumber)"
}
var body: some View {
VStack {
Text("Question: What is \(question)?")
Form {
Section {
TextField("Amount", text: $answer)
.keyboardType(.decimalPad)
}
}
Button ("Next") {
nextQuestion()
}
.padding(50)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
}

Deleting List Item With A Binding Boolean Value - SwiftUI

In my app, I have a screen where the user can switch filters (of a map) on and off using the Swift Toggle, and also, delete them (with Edit Button). I use a Binding Boolean variable to connect the toggles so that I can react to each change, here is the code:
//The filter data model
class DSFilter{
var filterId : Int
var filterTitle : String
var isActive : Bool
init(filterId : Int, filterTitle : String, isActive : Bool){
self.filterId = filterId
self.filterTitle = filterTitle
self.isActive = isActive
}
}
//The data model representable
struct filterItem : View {
#Binding var filter : DSFilter
#Binding var isOn : Bool
#State var image = Image("defPerson")
var body : some View {
HStack {
image.resizable().frame(width: 45, height: 45).clipShape(Circle())
VStack(alignment: .leading, spacing: 8) {
Text(filter.filterTitle).font(Font.custom("Quicksand-Bold",size: 15))
Text(filter.filterTitle).font(Font.custom("Quicksand-Medium",size: 13)).foregroundColor(.gray)
}.padding(.leading)
Spacer()
HStack{
Toggle("",isOn: $isOn).padding(.trailing, 5).onReceive([self.isOn].publisher.first()) { (value) in
self.filter.isActive = self.isOn
}.frame(width: 30)
}
}.frame(height: 60)
.onAppear(){
self.isOn = self.filter.isActive
}
}
}
//The view where the user has the control
struct FilterView: View {
#State var otherFilters : [addedFilter] = []
#Binding var isActive : Bool
var body: some View {
VStack{
Capsule().fill(Color.black).frame(width: 50, height: 5).padding()
ZStack{
HStack{
Spacer()
Text("Filters").font(Font.custom("Quicksand-Bold", size: 20))
Spacer()
}.padding()
HStack{
Spacer()
EditButton()
}.padding()
}
List{
ForEach(0..<self.otherFilters.count, id:\.self){ i in
filterItem(filter: self.$otherFilters[i].filter, isOn: self.$otherFilters[i].isOn)
}.onDelete(perform:removeRows)
}
Spacer()
}
}
func removeRows(at offsets: IndexSet) {
print("deleting")
print("deleted filter!")
self.otherFilters.remove(at: Array(offsets)[0]) //I use it like so because you can only delete 1 at a time anyway
}
}
//The class I use to bind the isOn value so I can react to change
struct addedFilter : Identifiable{
var id : Int
var filter : DSFilter
var isOn : Bool
init(filter : DSFilter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isActive
}
}
When I use the delete as it is right now I get the following error (Xcode 12.0):
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
2020-10-06 17:42:12.567235+0300 Hynt[5207:528572] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
What do I need to change so I can delete the items? Should I take a different approach to the Binding problem with the Toggle?
Hi instead of keeping three different array to manage addedFilter, DSFilter, isOn. why don't you try the following one single model
class FFilter: Identifiable {
var id: Int
var addedFilter: addedFilter
var dsFilter: DSFilter
var isOn: Bool
init(addedFilter: addedFilter, dsFilter: DSFilter, isOn: Bool) {
self.id = addedFilter.id
self.addedFilter = addedFilter
self.dsFilter = dsFilter
self.isOn = isOn
}
}
And ViewModel change code like this:
class ViewModel: ObservableObject {
#Published var superFilters: [FFilter] = [
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true)
),
dsFilter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false)
),
dsFilter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false),
isOn: false),
FFilter(
addedFilter: addedFilter(filter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true)),
dsFilter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true)
),
dsFilter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false)
),
dsFilter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false),
isOn: false)
]}
then change your list code
List {
ForEach(viewModel.superFilters) { filter in
filterItem(filter: .constant(filter.dsFilter), isOn: .constant(filter.isOn))
}.onDelete(perform: removeRows(at:)) }
and removeRows method:
func removeRows(at offsets: IndexSet) {
print("deleting at \(offsets)")
self.viewModel.superFilters.remove(atOffsets: offsets)}
I hope it will work. let me know if you still face any issues
I suggest you to change your line in removeRows() function from
self.otherFilters.remove(at: Array(offsets)[0]
to
self.otherFilters.remove(atOffsets: offsets)
So, after a lot of try and fail I came to the answer.
The problem seems to be in the binding part - the compiler can't handle a change in the array size if I bind an element from it so I did the following:
First, I change the filterItem struct so the filter property is #State and not #Binding :
struct filterItem : View {
#State var filter : DSFilter
#Binding var isOn : Bool
Secondly, I have changed the addedFilter struct to the following:
struct addedFilter : Identifiable{
var id : Int
var isOn : Bool
init(filter : DSFilter){
self.id = filter.filterId
self.isOn = filter.isActive
}
}
And the ForEach loop to the following:
ForEach(0..<self.normOthFilters.count, id:\.self){ i in
filterItem(filter: self.normOthFilters[i], isOn: self.$otherFilters[i].isOn)
}.onDelete(perform:removeRows)
To the main view I have added :
#State var normDefFilters : [DSFilter] = []
#State var normOthFilters : [DSFilter] = []
And finally the onDelete function to the following:
func removeRows(at offsets: IndexSet) {
print("deleting")
print("deleted filter!")
self.normOthFilters.remove(atOffsets: offsets)
}
In conclution I am not changing the value of the binded value and so I am not receiving any errors!

SwiftUI: cannot delete Row in List

i have a small swiftUI programm in Xcode which let me create and delete Users in a list with a stepper to count points of the users.
everything works fine (adding users, renaming users, stepper counting) unless the deletion of the user.
it throws an error:
Fatal error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444 2020-05-23 12:06:22.854920+0200 Counter[21328:1125981] Fatal
error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444
Here is the code:
import SwiftUI
struct ContentView : View {
#State var isEditing = false
#State var stepperWerte = [3, 5, 7, 9]
#State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(0..<editText.count, id: \.self) {
spieler in HStack {
if self.editText.indices.contains(spieler) {
Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
TextField("", text: self.$editText[spieler], onEditingChanged: {_ in }, onCommit: {self.saveText(id: spieler, Text: self.editText[spieler])} )
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
}
}
}
.onDelete(perform: spielerLoeschen)
.frame(width: nil, height: nil, alignment: .trailing)
}
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
stepperWerte.remove(atOffsets: offsets)
editText.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
stepperWerte.append(startLeben)
editText.append(startName)
}
func saveText(id: Int, Text: String) {
editText[id] = Text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
(ignore the "if" after the HStack, it has no real effect and those extra prints in the last Text to show the index and the total count)
if i dump the arrays (stepperWerte and editText) they are removed the right way -> the player selected for deletion will be removed correctly from the two arrays.
if i change
TextField("", text: self.$editText[spieler]
to
TextField("", text: self.$editText[0]
it works (unless naturally it displays the first player in all rows and i got the same error after deleting all the players (=rows))
any help would be great - thank you!
According to #Asperi i have changed my code to the following:
import SwiftUI
struct BetterTextField : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
}
}
struct ContentView : View {
#State var isEditing = false
#State var stepperWerte = [3, 5, 7, 9]
#State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(0..<editText.count, id: \.self) {
spieler in HStack {
if self.editText.indices.contains(spieler) {
Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
BetterTextField(container: self.$editText, index: self.editText.firstIndex(of: self.editText[spieler])!, text: self.editText[spieler])
Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
}
}
}
.onDelete(perform: spielerLoeschen)
.frame(width: nil, height: nil, alignment: .trailing)
}
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
stepperWerte.remove(atOffsets: offsets)
editText.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
stepperWerte.append(startLeben)
editText.append(startName)
}
func saveText(id: Int, Text: String) {
editText[id] = Text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
... and it works - thank you!
but:
is this a bug in SwiftUI or intentional?
The problem is that you are not using your items directly in the ForEach loop. Consider using structs for your data as objects and make them identifiable.
struct Player : Identifiable {
let id = UUID()
var stepperWerte: Int
var editText : String
}
struct ContentView : View {
#State var isEditing = false
#State var players = [Player(stepperWerte: 3, editText: "Player 1"), Player(stepperWerte: 5, editText: "Player 2"), Player(stepperWerte: 7, editText: "Player 3"), Player(stepperWerte: 9, editText: "Player 4")]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(self.players) { player in
SecondView(player: player)
}
.onDelete(perform: spielerLoeschen)
}
.frame(width: nil, height: nil, alignment: .trailing)
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
players.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
players.insert(Player(stepperWerte: 4, editText: "Neuer Player"), at: 0)
}
}
struct SecondView : View {
var player : Player
#State var stepperWerte : Int
#State var name : String
init(player : Player)
{
self._stepperWerte = State(initialValue: player.stepperWerte)
self._name = State(initialValue: player.editText)
self.player = player
}
var body: some View
{
Stepper(value: self.$stepperWerte, in: -1...10, step: 1, label: {
TextField("", text: self.$name)
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(player.stepperWerte)")
})
}
}
I created a struct Player, and then an array of many Players. In the ForEach you can directly use players as Player confirms to Identifiable protocol. This is way easier as you can access a player object in your ForEach loop and you do not have to access everything with indices. In the deleting function you just delete the object out of the array or add something new to it. Deleting now works fine.
I have removed some code from the list row, just to reproduce it easier, just if you are wondering.

NavigationView + TextField Background

I'm currently learning SwiftUI and building a todo list app. On the ContentView screen I've got a NavigationView and a button that pops up an "add new task" textfield into the list. I suspect this is not the correct way to implement this but when the textfield shows up the background color doesn't persist. For the life of me I can't figure out how to set the background color. If I move the textfield outside the NavigationView I can set the background but when the NavigationView shifts to make space for the textfield I get a bunch of black screen flicker. Any thoughts on how I can set the background color on the textfield when added to the list or fix the screen flicker when I move it out? Appreciate the help.
import SwiftUI
import UIKit
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]) var listItems: FetchedResults<ToDoItem>
#State private var newToDoItem = ""
#State private var showNewTask = false
#State var isEditing = false
#State var showTaskView = false
#State var bottomState = CGSize.zero
#State var showFull = false
#State var deleteButton = false
//this removes the lines in the list view
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
UIScrollView.appearance().backgroundColor = .clear
//UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack{
VStack{
TitleView()
NavigationView {
List {
if showNewTask {
HStack{
TextField("New task", text: self.$newToDoItem, onEditingChanged: { (changed) in
}) {
print("onCommit")
self.addTask(taskTitle: self.newToDoItem)
self.saveTasks()
self.showNewTask.toggle()
self.newToDoItem = ""
}
.font(Font.system(size: 18, weight: .bold))
.foregroundColor(Color("Text"))
Button(action: {
self.newToDoItem = ""
self.showNewTask.toggle()
}) {
Image(systemName: "xmark.circle").foregroundColor(Color("button"))
.font(Font.system(size: 18, weight: .bold))
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.background(Color("addNewTask"))
.cornerRadius(10.0)
}
ForEach(listItems, id: \.self) {item in
HStack {
Button(action: {
item.isComplete = true
self.saveTasks()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
self.deleteTaskTest(item: item)
}
}) {
if (item.isComplete) {
Image(systemName: "checkmark.circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color(#colorLiteral(red: 0.1616941956, green: 0.9244045403, blue: 0.1405039469, alpha: 1)))
.padding(.trailing, 4)
} else {
Image(systemName: "circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color("button"))
.padding(.trailing, 4)
}
}
.buttonStyle(PlainButtonStyle())
ToDoItemView(title: item.title, createdAt: "\(item.createdAt)")
.onTapGesture {
self.showTaskView.toggle()
}
.onLongPressGesture(minimumDuration: 0.1) {
self.isEditing.toggle()
print("this is a long press test")
}
}
.listRowBackground(Color("background"))
}
.onMove(perform: moveItem)
.onDelete(perform: deleteTask)
}
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
.navigationBarTitle(Text("ToDay"), displayMode: .large)
.navigationBarHidden(true)
.background(Color("background"))
}
//ADD A NEW TASK BUTTON
HStack {
Spacer()
Button(action: {
self.showNewTask.toggle()
}) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .bold))
.frame(width: 36, height: 36)
.background(Color("button"))
.foregroundColor(.white)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
.padding()
}
.blur(radius: showTaskView ? 20 : 0)
.animation(.default)
.padding(.top, 30)
//BOTTOM CARD VIEW
TaskView()
.offset(x: 0, y: showTaskView ? 360 : 1000)
.offset(y: bottomState.height)
.animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.5))
.gesture(
DragGesture().onChanged { value in
self.bottomState = value.translation
if self.showFull {
self.bottomState.height += -300
}
if self.bottomState.height < -300 {
self.bottomState.height = -300
}
} .onEnded { value in
if self.bottomState.height > 50 {
self.showTaskView = false
}
if (self.bottomState.height < -100 && !self.showFull) || (self.bottomState.height < -250 && self.showFull){
self.bottomState.height = -300
self.showFull = true
} else {
self.bottomState = .zero
self.showFull = false
}
}
)
}
.background(Color("background").edgesIgnoringSafeArea(.all))
}
Finally got it to work. For whatever reason reworking the stacks fixed it.
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]) var listItems: FetchedResults<ToDoItem>
#State private var showCancelButton: Bool = false
#State private var newToDoItem = ""
#State private var showNewTask = false
#State var isEditing = false
#State var showTaskView = false
#State var bottomState = CGSize.zero
#State var showFull = false
#State var deleteButton = false
var itemName = ""
init() {
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack {
VStack {
NavigationView {
VStack {
TitleView()
.padding(.top, 20)
.background(Color("background"))
// Enter new task view
if showNewTask {
HStack {
HStack {
TextField("New task", text: self.$newToDoItem, onEditingChanged: { (changed) in
}) {
self.addTask(taskTitle: self.newToDoItem)
self.saveTasks()
self.showNewTask.toggle()
self.newToDoItem = ""
}
.font(Font.system(size: 18, weight: .bold))
.foregroundColor(Color("Text"))
Button(action: {
self.newToDoItem = ""
self.showNewTask.toggle()
}) {
Image(systemName: "xmark.circle").foregroundColor(Color("button"))
.font(Font.system(size: 18, weight: .bold))
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.background(Color("addNewTask"))
.cornerRadius(10.0)
}
.background(Color("background"))
.padding(.horizontal)
}
List {
ForEach(listItems, id: \.self) {item in
HStack {
Button(action: {
item.isComplete = true
self.saveTasks()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
self.deleteTaskTest(item: item)
}
}) {
if (item.isComplete) {
Image(systemName: "checkmark.circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color(#colorLiteral(red: 0.1616941956, green: 0.9244045403, blue: 0.1405039469, alpha: 1)))
.padding(.trailing, 4)
} else {
Image(systemName: "circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color("button"))
.padding(.trailing, 4)
}
}
.buttonStyle(PlainButtonStyle())
ToDoItemView(title: item.title, createdAt: "\(item.createdAt)")
.onTapGesture {
//item.title = self.itemName
self.showTaskView.toggle()
}
.onLongPressGesture(minimumDuration: 0.1) {
self.isEditing.toggle()
print("this is a long press test")
}
}
.listRowBackground(Color("background"))
}
.onMove(perform: moveItem)
.onDelete(perform: deleteTask)
}
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
.navigationBarTitle(Text("ToDay"), displayMode: .large)
.navigationBarHidden(true)
.background(Color("background"))
}
.background(Color("background").edgesIgnoringSafeArea(.all))
}
HStack {
Spacer()
Button(action: {
//withAnimation(){
self.showNewTask.toggle()
//}
}) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .bold))
.frame(width: 36, height: 36)
.background(Color("button"))
.foregroundColor(.white)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
.padding()
}
.blur(radius: showTaskView ? 20 : 0)
//BOTTOM CARD VIEW
TaskView()
.offset(x: 0, y: showTaskView ? 360 : 1000)
.offset(y: bottomState.height)
.animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.5))
.gesture(
DragGesture().onChanged { value in
self.bottomState = value.translation
if self.showFull {
self.bottomState.height += -300
}
if self.bottomState.height < -300 {
self.bottomState.height = -300
}
} .onEnded { value in
if self.bottomState.height > 50 {
self.showTaskView = false
}
if (self.bottomState.height < -100 && !self.showFull) || (self.bottomState.height < -250 && self.showFull){
self.bottomState.height = -300
self.showFull = true
} else {
self.bottomState = .zero
self.showFull = false
}
}
)
}
.animation(.default)
.background(Color("background").edgesIgnoringSafeArea(.all))
}
func moveItem(indexSet: IndexSet, destination: Int){
let source = indexSet.first!
if source < destination {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = listItems[source].order
while startIndex <= endIndex {
listItems[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
listItems[source].order = startOrder
} else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = listItems[destination].order + 1
let newOrder = listItems[destination].order
while startIndex <= endIndex {
listItems[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
listItems[source].order = newOrder
}
saveTasks()
self.isEditing.toggle()
}
func deleteTask(indexSet: IndexSet){
let source = indexSet.first!
let listItem = listItems[source]
//self.deleteButton.toggle()
managedObjectContext.delete(listItem)
saveTasks()
}
func deleteTaskTest(item: ToDoItem){
managedObjectContext.delete(item)
saveTasks()
}
func addTask(taskTitle: String) {
let newTask = ToDoItem(context: managedObjectContext)
newTask.title = taskTitle
newTask.order = (listItems.last?.order ?? 0) + 1
newTask.createdAt = Date()
}
func saveTasks() {
do {
try managedObjectContext.save()
} catch {
print(error)
}
}