SwiftUI update observedObject in Cell View of list - swiftui

I tried to change the value of the observedObject. The value will changed but not permanently. When I scroll down and scroll back, the value will changed back to original value. I guess is the problem of #State in the cell view of list. But I cant figure out what the solution to change the data permanently in observedObject. So what should I do to change the data permanently by using the data cell.
struct ContentView: View {
#ObservedObject var dataClass = DataClass()
var body: some View {
List(dataClass.data){ item in
DataCell(item: item)
}
}
}
struct DataCell: View {
#State var item : DataModal
var body: some View{
HStack{
Text("\(item.num!)")
Text("\(item.val!)").onTapGesture {
item.val = 1
}
}
.padding(.vertical, 100)
}
}
struct DataModal : Identifiable {
var id = UUID()
var num : Int?
var val : Int?
}
class DataClass : ObservableObject {
#Published var data = [DataModal]()
init(){
load()
}
func load(){
data = [DataModal(num: 1, val: 1),DataModal(num: 2, val: 2),DataModal(num: 3, val: 3),DataModal(num: 4, val: 4),DataModal(num: 5, val: 5),DataModal(num: 6, val: 6),DataModal(num: 7, val: 7),DataModal(num: 8, val: 8),DataModal(num: 9, val: 9),DataModal(num: 10, val: 10),DataModal(num: 11, val: 11),DataModal(num: 12, val: 12),DataModal(num: 13, val: 13)]
}
}

You pass a copy of DataModal, because it is a value type. In this scenario you need to pass binding to original value inside dataClass, like
struct ContentView: View {
#ObservedObject var dataClass = DataClass()
var body: some View {
List(dataClass.data.indices, id: \.self){ index in
DataCell(item: $dataClass.data[index])
}
}
}
struct DataCell: View {
#Binding var item : DataModal
var body: some View{
HStack{
Text("\(item.num!)")
Text("\(item.val!)").onTapGesture {
item.val = 1
}
}
.padding(.vertical, 100)
}
}

Related

iOS 15: Navigation link popping out, again

In the last few months, many developers have reported NavigationLinks to unexpectedly pop out and some workarounds have been published, including adding another empty link and adding .navigationViewStyle(StackNavigationViewStyle()) to the navigation view.
Here, I would like to demonstrate another situation under which a NavigationLink unexpectedly pops out:
When there are two levels of child views, i.e. parentView > childLevel1 > childLevel2, and childLevel2 modifies childLevel1, then, after going back from level 2 to level 1, level 1 pops out and parentView is shown.
I have filed a bug report but not heard from apple since. None of the known workarounds seem to work. Does someone have an idea what to make of this? Just wait for iOS 15.1?
Below is my code (iPhone app). In the parent view, there is a list of persons from which orders are taken. In childLevel1, all orders from a particular person are shown. Each order can be modified by clicking on it, which leads to childLevel2. In childLevel2, several options are available (here only one is shown for the sake of brevity), which is the reason why the user is supposed to leave childLevel2 via "< Back".
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var paId: Int
var n: Int // used only in the real code
}
class Data : ObservableObject {
init() {
pastries = [
Pastry(id: 0, name: "Prezel"),
Pastry(id: 1, name: "Donut"),
Pastry(id: 2, name: "bagel"),
Pastry(id: 3, name: "cheese cake"),
]
persons = [
Person(id: 0, name: "Alice", orders: [Order(paId: 1, n: 1)]),
Person(id: 1, name: "Bob", orders: [Order(paId: 2, n: 1), Order(paId: 3, n: 1)])
]
activePersonsIds = [0, 1]
}
#Published var activePersonsIds: [Int] = []
#Published var persons: [Person] = []
#Published var pastries: [Pastry]
#Published var latestOrder = Order(paId: 0, n: 1)
lazy var pastryName: (Int) -> String = { (paId: Int) -> String in
if self.pastries.first(where: { $0.id == paId }) == nil {
return "undefined pastryId " + String(paId)
}
return self.pastries.first(where: { $0.id == paId })!.name
}
var activePersons : [Person] {
return activePersonsIds.compactMap {id in persons.first(where: {$0.id == id})}
}
}
#main
struct Bretzel_ProApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
#StateObject var data = Data()
var body: some View {
TabView1(data: data)
// in the real code, there are more tabs
}
}
struct TabView1: View {
#StateObject var data: Data
var body: some View {
NavigationView {
List {
ForEach(data.activePersons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
VStack (alignment: .leading) {
Text(person.name)
}
}
)
}
}
.listStyle(PlainListStyle())
.navigationTitle("Orders")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
List() {
ForEach (0...p.orders.count-1, id: \.self) { loop in
Section(header:
HStack() {
Text("BESTELLUNG " + String(loop+1))
}
) {
EPSubview1(data: data, psId: psId, loop: loop)
}
}
}.navigationTitle(p.name)
.listStyle(InsetGroupedListStyle())
}
}
struct EPSubview1: View {
#ObservedObject var data: Data
var psId: Int
var loop: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
let o1: Order = p.orders[loop]
NavigationLink(
destination: SelectPastry(data: data)
.onAppear() {
data.latestOrder.paId = o1.paId
}
.onDisappear() {
data.persons[pindex].orders[loop].paId = data.latestOrder.paId
},
label: {
VStack(alignment: .leading) {
Text(String(o1.n) + " x " + data.pastryName(o1.paId))
}
}
)
}
}
struct SelectPastry: View {
#ObservedObject var data : Data
var body: some View {
VStack {
List {
ForEach(data.pastries, id: \.self) {pastry in
Button(action: {
data.latestOrder.paId = pastry.id
}) {
Text(pastry.name)
.foregroundColor(data.latestOrder.paId == pastry.id ? .primary : .secondary)
}
}
}.listStyle(PlainListStyle())
}
}
}
The problem is your ForEach. Despite that fact that Person conforms to Identifiable, you're using \.self to identify the data. Because of that, every time an aspect of the Person changes, so does the value of self.
Instead, just use this form, which uses the id vended by Identifiable:
ForEach(data.activePersons) { person in
Which is equivalent to:
ForEach(data.activePersons, id: \.id) { person in

The StateObject in SwiftUI doesn't preserve states after the view disappeared

I would like StateObject preserve state even after its view disappeared. But I failed. Here is my code:
import SwiftUI
struct ContentView: View {
#State var b: Bool = false
var body: some View {
VStack {
Toggle(isOn: $b, label: {Text("Show Button")})
if b {A()} else {Text("hi")}
}
}
}
struct A: View {
#StateObject var o: Obj = Obj()
var body: some View {
Button {o.i += 1} label: {Text("i == \(o.i.description)")}
}
}
class Obj: ObservableObject {
#Published var i: Int = 1
}
After the Toggle switched, the state o.i was reseted to 1. How can I preserve the state?
try this:
struct ContentView: View {
#State var b: Bool = false
#StateObject var o: Obj = Obj()
var body: some View {
VStack {
Toggle(isOn: $b, label: {Text("Show Button")})
if b {A(o: o)} else {Text("hi")}
}
}
}
struct A: View {
#ObservedObject var o: Obj
var body: some View {
Button {o.i += 1} label: {Text("i == \(o.i.description)")}
}
}

how to deal with swiftui #State optional unwrap

in swiftui, I have a state variable count ,which is optional, in the sheet present ,I unwrap the optional and show Detailview, but it seems never hit there.
any idea how why not hit there?
it seems never hit
DetailView(count: num)
import SwiftUI
struct ContentView: View {
#State var showDetailView = false
#State var count : Int?
var testArr = [1,2,3,4,5]
var body: some View {
NavigationView {
List(testArr.indices){ indice in
Text("row num \(indice)")
.onTapGesture{
self.showDetailView = true
self.count = 5
}
}
.sheet(isPresented: self.$showDetailView) {
if let num = self.count{
//never hit here
DetailView(count: num)
}
}
.navigationBarTitle("Your Reading")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailView: View {
var count: Int
var body: some View {
if count == 5 {
Text("5555")
} else {
EmptyView()
}
}
}
The issue is not the optional unwrapping.
The issue is that you are executing update to count while the body is being computed. To overcome this use my solution of:
swiftui state variable count lost what it stored
Let me know if that solution does not work for you.
You could use this workaround (not recommended), if you really have to use #State var count.
DispatchQueue.main.async {
self.count = 5
}
You can use sheet(item:content:) instead of sheet(isPresented:content:):
struct ContentView: View {
#State var count: Int?
var testArr = [1, 2, 3, 4, 5]
var body: some View {
NavigationView {
List(testArr.indices) { index in
Text("row num \(index)")
.onTapGesture {
self.count = 5
}
}
.sheet(item: $count) { count in
DetailView(count: count)
}
.navigationBarTitle("Your Reading")
}
}
}
Note that item must conform to Identifiable, so you need to either use your own struct or create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}

Delete entire list in SwiftUI

Is there a way to delete all the list items in SwiftUI?
I'm using a ForEach() inside a List() and I want to have a clear all button to remove all the items from the list, is there a way to do it?
struct SwiftUIView: View {
#State var filters : [filter] = [filter(name: "new"), filter(name: "old"), filter(name: "some")]
#State var afterFilters : [someFilter] = []
var body: some View {
List{
ForEach(0..<self.filters.count, id:\.self){ i in
filterRepresent(string: self.$afterFilters[i].filter.name, isOn: self.$afterFilters[i].isOn)
}
}.onAppear {
for filter in self.filters {
self.afterFilters.append(someFilter(filter: filter))
}
}
}
}
struct filterRepresent : View {
#Binding var string : String
#Binding var isOn : Bool
var body : some View {
HStack{
Text(string)
Toggle("",isOn: $isOn)
}
}
}
struct filter {
var name : String
var isOn : Bool
init(name: String){
self.name = name
self.isOn = false
}
}
struct someFilter : Identifiable{
var id : Int
var filter : filter
var isOn : Bool
init(filter : filter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isOn
}
}
As you can see, in the example above, I'm using a #Binding to change the data I store based on the Toggle state, I want to have a button that deletes the entire list (in the real app the data to the list is uploaded from a server side into a temp array just like in the above) when I do it with .removeall() I get thrown with "out of index" error.
The button I use :
Button(action: {
self.afterFilters.removeAll()
}, label: {
Text("Clear all").font(Font.custom("Quicksand-Medium", size: 15))
})
The error I'm getting:
Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.13/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
You have to clean up model and view will be refreshed automatically.
Here is a simple demo:
struct DemoCleanUpList: View {
#State private var persons = ["Person 1", "Person 2", "Person 3"]
var body: some View {
VStack {
Button("CleanUp") { self.persons.removeAll() }
List {
ForEach(persons, id: \.self) { person in
Text(person)
}
}
}
}
}

SwiftUI can not update class data updates

I came across a situation that you use class data as your data source, and display them in a swiftUI list view, when you update your data source, the swiftUI list view won't be updated, what can we do to make the class data updates interactive with swiftUI?
see code blow:
I define the environment object :
import Foundation
import Combine
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
data.objectWillChange.send()
}
self.objectWillChange.send()
}
}
to display each data in a Row View:
import SwiftUI
struct RowView: View {
#State var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(data: RowData(title: "text", count: 1))
}
}
class RowData: ObservableObject {
var title: String = ""
var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
in content view, display the data in a list view, I would like to refresh all the view updates when click update button. the button triggers the update methods to update the class data value from data source.
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var shouldUpdate:Bool = false
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.shouldUpdate.toggle()
self.localData.removeAll()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
}
}
Well... I don't see the reason to have localData, but, anyway, here is modified code that works.
Tested with Xcode 12 / iOS 14
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
}
self.objectWillChange.send()
}
}
struct RowView: View {
#ObservedObject var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
class RowData: ObservableObject {
#Published var title: String = ""
#Published var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
.onAppear {
self.data.fetch()
self.localData = self.data.datalist
}
}
}