I have a NavigationSplitView with a Picker that shows 2 list on a MainView. When you select an item, the DetailView is update. The problem is that when you change the picker selection the DetailView not always show a value.
I want to keep the DetailView until the user select other value.
This a little example code that shows the problem.
import SwiftUI
struct Model1: Identifiable, Hashable {
let id = UUID()
let title: String
let description: String
}
struct Model2: Identifiable, Hashable {
let id = UUID()
let name: String
let address: String
}
struct CustomData: Identifiable {
var id = UUID()
var docs: [Model1]
var lites: [Model2]
init(docs: [Model1], lites: [Model2]) {
self.docs = docs
self.lites = lites
}
}
#MainActor
final class TestViewModel: ObservableObject {
#Published var data: CustomData?
func loadData() {
//FetchData
let docs = [Model1(title: "title1", description: "desc1"), Model1(title: "title2", description: "desc2"), Model1(title: "title3", description: "desc3")]
let lites = [Model2(name: "value1", address: "desc1"), Model2(name: "value2", address: "desc2"), Model2(name: "value3", address: "desc3")]
self.data = CustomData(docs: docs, lites: lites)
}
func lites() -> [Model2] {
return data?.lites ?? []
}
func docs() -> [Model1] {
return data?.docs ?? []
}
}
struct TestView2: View {
#State private var subpage = 0
#ObservedObject var viewModel = TestViewModel()
var body: some View {
NavigationSplitView {
VStack {
Picker("Select option", selection: $subpage) {
Text("Lites").tag(0)
Text("Docs").tag(1)
}
.pickerStyle(.segmented)
//
VStack() {
switch(subpage){
case 0: //List of Lites
List(viewModel.lites()) { lite in
NavigationLink(value: lite) {
HStack (alignment: .top) {
Text(lite.name)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.headline))
}
}
}
.navigationDestination(for: Model2.self) { lite in
DetailView2(path: lite.name)
}
case 1:
List(viewModel.docs()) { doc in
NavigationLink(value: doc) {
HStack (alignment: .top) {
Text(doc.title)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.subheadline))
}
}
}
.navigationDestination(for: Model1.self) { doc in
DetailView2(path: doc.title)
}
default:
Text("No docs")
}
}
.accentColor(AppColors.LightBlue)
.padding(.top, 10)
.scrollContentBackground(.hidden)
.background(AppColors.LightGray)
}
}
detail: {
DetailView2(path: "Example Text")
}
.onAppear(){
viewModel.loadData()
}
}
}
struct DetailView2: View {
var path: String
var body: some View {
VStack(spacing: .infinity) {
Text(path)
}
.navigationBarHidden(true)
}
}
struct TestView2_Previews: PreviewProvider {
static var previews: some View {
TestView2()
.previewInterfaceOrientation(.landscapeRight)
.previewDevice("iPad (9th generation)")
}
}
You need to add a destination navigationDestination to your VStack like this
import SwiftUI
struct Model1: Identifiable, Hashable {
let id = UUID()
let title: String
let description: String
}
struct Model2: Identifiable, Hashable {
let id = UUID()
let name: String
let address: String
}
struct CustomData: Identifiable {
var id = UUID()
var docs: [Model1]
var lites: [Model2]
init(docs: [Model1], lites: [Model2]) {
self.docs = docs
self.lites = lites
}
}
#MainActor
final class TestViewModel: ObservableObject {
#Published var data: CustomData?
func loadData() {
//FetchData
let docs = [Model1(title: "title1", description: "desc1"), Model1(title: "title2", description: "desc2"), Model1(title: "title3", description: "desc3")]
let lites = [Model2(name: "value1", address: "desc1"), Model2(name: "value2", address: "desc2"), Model2(name: "value3", address: "desc3")]
self.data = CustomData(docs: docs, lites: lites)
}
func lites() -> [Model2] {
return data?.lites ?? []
}
func docs() -> [Model1] {
return data?.docs ?? []
}
}
#available(iOS 16.0, *)
struct TestView2: View {
#State private var subpage = 0
#ObservedObject var viewModel = TestViewModel()
var body: some View {
NavigationSplitView {
VStack {
Picker("Select option", selection: $subpage) {
Text("Lites").tag(0)
Text("Docs").tag(1)
}
.pickerStyle(.segmented)
//
VStack() {
switch(subpage){
case 0: //List of Lites
List(viewModel.lites()) { lite in
NavigationLink(value: lite) {
HStack (alignment: .top) {
Text(lite.name)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.headline))
}
}
}
case 1:
List(viewModel.docs()) { doc in
NavigationLink(value: doc) {
HStack (alignment: .top) {
Text(doc.title)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.subheadline))
}
}
}
default:
Text("No docs")
}
}
.accentColor(.blue)
.padding(.top, 10)
.scrollContentBackground(.hidden)
.background(.gray)
.navigationDestination(for: Model1.self) { doc in
DetailView2(path: doc.title)
}
.navigationDestination(for: Model2.self) { lite in
DetailView2(path: lite.name)
}
}
}
detail: {
DetailView2(path: "Example Text")
}
.onAppear(){
viewModel.loadData()
}
}
}
struct DetailView2: View {
var path: String
var body: some View {
VStack(spacing: .infinity) {
Text(path)
}
.navigationBarHidden(true)
}
}
#available(iOS 16.0, *)
struct TestView2_Previews: PreviewProvider {
static var previews: some View {
TestView2()
.previewInterfaceOrientation(.landscapeRight)
.previewDevice("iPad (9th generation)")
}
}
Related
[Pic 1 AS IS]
[Pic 2 TO BE]
Hi there,
I am just starting to learn Swift an I would like my app users to build their own list of items (first level) where each item again contains a list of items (second level). Important is that each of the individually created lists in the second level is like no other of the individually created lists. (see picture)
Is anyone aware of which approach I need to take to solve this?
I am myself able to build the list within the list within the NavigationView, but how can I make each list individual?
Here is my code:
struct ItemModel: Hashable {
let name: String
}
struct ProductModel: Hashable {
let productname: String
}
class ListViewModel: ObservableObject {
#Published var items: [ItemModel] = []
}
class ProductlistViewModel: ObservableObject {
#Published var products: [ProductModel] = []
}
struct ContentView: View {
#StateObject private var vm = ListViewModel()
#StateObject private var pvm = ProductlistViewModel()
#State var firstPlusButtonPressed: Bool = false
#State var secondPlusButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView() //The DetailView below
.navigationTitle(item.name)
.navigationBarItems(
trailing:
Button(action: {
secondPlusButtonPressed.toggle()
}, label: {
NavigationLink {
AddProduct() //AddProduct below
} label: {
Image(systemName: "plus")
}
})
)
} label: {
Text(item.name)
}
}
}
.navigationBarItems(
trailing:
Button(action: { firstPlusButtonPressed.toggle()
}, label: {
NavigationLink {
AddItem() //AddItem below
} label: { Image(systemName: "plus")
}
})
)
}
.environmentObject(vm)
.environmentObject(pvm)
}
}
struct AddItem: View {
#State var textFieldText: String = ""
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var vm: ListViewModel
var body: some View {
NavigationView {
VStack {
TextField("Add an item...", text: $textFieldText)
Button(action: {
vm.addItem(text: textFieldText)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("SAVE")
})
}
}
}
}
struct DetailView: View {
#StateObject private var pvm = ProductlistViewModel()
#Environment(\.editMode) var editMode
var body: some View {
NavigationView {
List {
ForEach(pvm.products, id: \.self) { product in
Text(product.productname)
}
}
}
.environmentObject(pvm)
}
}
struct AddProduct: View {
#State var textFieldText: String = ""
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var pvm: ProductlistViewModel
var body: some View {
NavigationView {
VStack {
TextField("Add a product", text: $textFieldText)
Button(action: {
pvm.addProduct(text: textFieldText)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("SAVE")
})
}
}
}
}
This is going to be long but here it goes. The issue is the whole ViewModel setup. You detail view now is only using the product view model, you need to rethink your approach.
But what makes the whole thing "complicated" is the 2 different types, Item and Product which you seem to want to combine into one list and use the same subviews for them both.
In swift you have protocol that allows this, protocols require struct and class "conformance".
//Protocols are needed so you can use reuse views
protocol ObjectModelProtocol: Hashable, Identifiable{
var id: UUID {get}
var name: String {get set}
init(name: String)
}
//Protocols are needed so you can use reuse views
protocol ListModelProtocol: Hashable, Identifiable{
associatedtype O : ObjectModelProtocol
var id: UUID {get}
var name: String {get set}
//Keep the individual items with the list
var items: [O] {get set}
init(name: String, items: [O])
}
extension ListModelProtocol{
mutating func addItem(name: String) {
items.append(O(name: name))
}
}
Then your models start looking something like this. Notice the conformance to the protocols.
//Replaces the ListViewModel
struct ListItemModel: ListModelProtocol{
let id: UUID
var name: String
var items: [ItemModel]
init(name: String, items: [ItemModel]){
self.id = .init()
self.name = name
self.items = items
}
}
//Replaces the ProductlistViewModel
struct ListProductModel: ListModelProtocol{
let id: UUID
var name: String
var items: [ProductModel]
init(name: String, items: [ProductModel]){
self.id = .init()
self.name = name
self.items = items
}
}
//Uniform objects, can be specialized but start uniform
struct ItemModel: ObjectModelProtocol {
let id: UUID
var name: String
init(name: String){
self.id = .init()
self.name = name
}
}
//Uniform objects, can be specialized but start uniform
struct ProductModel: ObjectModelProtocol {
let id: UUID
var name: String
init(name: String){
self.id = .init()
self.name = name
}
}
class ModelStore: ObservableObject{
#Published var items: [ListItemModel] = [ListItemModel(name: "fruits", items: [.init(name: "peach"), .init(name: "banana"), .init(name: "apple")])]
#Published var products: [ListProductModel] = [ListProductModel(name: "vegetable", items: [.init(name: "tomatoes"), .init(name: "paprika"), .init(name: "zucchini")])]
}
Now your views can look something like this
struct ComboView: View {
#StateObject private var store = ModelStore()
#State var firstPlusButtonPressed: Bool = false
#State var secondPlusButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
//The next part will address this
ItemLoop(list: $store.items)
ItemLoop(list: $store.products)
}
.toolbar(content: {
ToolbarItem {
AddList(store: store)
}
})
}
}
}
struct ItemLoop<LM: ListModelProtocol>: View {
#Binding var list: [LM]
var body: some View{
ForEach($list, id: \.id) { $item in
NavigationLink {
DetailView<LM>(itemList: $item)
.navigationTitle(item.name)
.toolbar {
NavigationLink {
AddItem<LM>( item: $item)
} label: {
Image(systemName: "plus")
}
}
} label: {
Text(item.name)
}
}
}
}
struct AddList: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var store: ModelStore
var body: some View {
Menu {
Button("add item"){
store.items.append(ListItemModel(name: "new item", items: []))
}
Button("add product"){
store.products.append(ListProductModel(name: "new product", items: []))
}
} label: {
Image(systemName: "plus")
}
}
}
struct AddItem<LM>: View where LM : ListModelProtocol {
#State var textFieldText: String = ""
#Environment(\.presentationMode) var presentationMode
#Binding var item: LM
var body: some View {
VStack {
TextField("Add an item...", text: $textFieldText)
Button(action: {
item.addItem(name: textFieldText)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("SAVE")
})
}
}
}
struct DetailView<LM>: View where LM : ListModelProtocol{
#Environment(\.editMode) var editMode
#Binding var itemList: LM
var body: some View {
VStack{
TextField("name", text: $itemList.name)
.textFieldStyle(.roundedBorder)
List (itemList.items, id:\.id) { item in
Text(item.name)
}
}
.navigationTitle(itemList.name)
.toolbar {
NavigationLink {
AddItem(item: $itemList)
} label: {
Image(systemName: "plus")
}
}
}
}
If you notice the List in the ComboView you will notice that the items and products are separated into 2 loop. That is because SwiftUI requires concrete types for most views, view modifiers and wrappers.
You can have a list of [any ListModelProtocol] but at some point you will have to convert from an existential to a concrete type. In your case the ForEach in de DetailView requires a concrete type.
class ModelStore: ObservableObject{
#Published var both: [any ListModelProtocol] = [
ListProductModel(name: "vegetable", items: [.init(name: "tomatoes"), .init(name: "paprika"), .init(name: "zucchini")]),
ListItemModel(name: "fruits", items: [.init(name: "peach"), .init(name: "banana"), .init(name: "apple")])
]
}
struct ComboView: View {
#StateObject private var store = ModelStore()
#State var firstPlusButtonPressed: Bool = false
#State var secondPlusButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
ConcreteItemLoop(list: $store.both)
}
.toolbar(content: {
ToolbarItem {
AddList(store: store)
}
})
}
}
}
struct ConcreteItemLoop: View {
#Binding var list: [any ListModelProtocol]
var body: some View{
ForEach($list, id: \.id) { $item in
NavigationLink {
if let concrete: Binding<ListItemModel> = getConcrete(existential: $item){
DetailView(itemList: concrete)
} else if let concrete: Binding<ListProductModel> = getConcrete(existential: $item){
DetailView(itemList: concrete)
}else{
Text("unknown type")
}
} label: {
Text(item.name)
}
}
}
func getConcrete<T>(existential: Binding<any ListModelProtocol>) -> Binding<T>? where T : ListModelProtocol{
if existential.wrappedValue is T{
return Binding {
existential.wrappedValue as! T
} set: { newValue in
existential.wrappedValue = newValue
}
}else{
return nil
}
}
}
struct AddList: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var store: ModelStore
var body: some View {
Menu {
Button("add item"){
store.both.append(ListItemModel(name: "new item", items: []))
}
Button("add product"){
store.both.append(ListProductModel(name: "new product", items: []))
}
} label: {
Image(systemName: "plus")
}
}
}
I know its long but this all compiles so you should be able to put it in a project and disect it.
Also, at the end of all of this you can create specific views for the model type.
struct DetailView<LM>: View where LM : ListModelProtocol{
#Environment(\.editMode) var editMode
#Binding var itemList: LM
var body: some View {
VStack{
TextField("name", text: $itemList.name)
.textFieldStyle(.roundedBorder)
List (itemList.items, id:\.id) { item in
VStack{
switch item{
case let i as ItemModel:
ItemModelView(item: i)
case let p as ProductModel:
Text("\(p.name) is product")
default:
Text("\(item.name) is unknown")
}
}
}
}
.navigationTitle(itemList.name)
.toolbar {
NavigationLink {
AddItem(item: $itemList)
} label: {
Image(systemName: "plus")
}
}
}
}
struct ItemModelView: View{
let item: ItemModel
var body: some View{
VStack{
Text("\(item.name) is item")
Image(systemName: "person")
}
}
}
I'm building a picker component and am passing a Binding two layers deep. The issue is that I need to detect it's .onChange on the PickerListView, however, it never gets triggered.
import SwiftUI
public struct SectionItems<T: Hashable>: Hashable {
let header: String
public var items: [T]
public init(header: String, items: [T]) {
self.header = header
self.items = items
}
}
struct PickerListView<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#Binding var selected: T
#Binding var sections: [SectionItems<T>]
init(selected: Binding<T>, sections: Binding<[SectionItems<T>]>) {
self._selected = selected
self._sections = sections
}
var body: some View {
List {
ForEach(sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
selected = item
dismiss()
}) {
HStack {
Text(String(describing: item))
Spacer()
if selected == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.onChange(of: sections) { _ in
print("PickerListView: \(sections.count)") // Doesn't run
}
}
}
import SwiftUI
public struct PickerView<T: Hashable>: View {
#Binding var selected: T
#Binding var sections: [SectionItems<T>]
public var body: some View {
NavigationView {
NavigationLink(
String(describing: selected),
destination: PickerListView(
selected: $selected,
sections: $sections
)
)
}
.onChange(of: sections) { _ in
print("PickerView: \(sections.count)") // Runs
}
}
}
struct ContentView: View {
#State private var selected = "33"
#State private var sections = [
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
]
var body: some View {
VStack {
PickerView(selected: $selected, sections: $sections)
Button("Add Value", action: {
sections[0].items.append("\(Int.random(in: 1...100))")
})
}
.onChange(of: sections) { _ in
print("ContentView: \(sections.count)") // Runs
}
}
}
You can try a different approach, instead of passing multiple items across many Views you can combine all the variables in a single ObservableObject
Then you can use didSet to make any changes you would make with onChange
///Keeps all the code needed for several views in one spot
class SectionViewModel<T: Hashable>: ObservableObject{
#Published var selected : T
#Published var sections : [SectionItems<T>]{
didSet{
//Use didSet instead of onChange
print("\(type(of: self)) :: \(#function) :: \(sections.count)")
print("\(type(of: self)) :: \(#function) :: section[0]items count: \(sections[0].items.count)")
}
}
init(selected : T, sections : [SectionItems<T>]){
self.selected = selected
self.sections = sections
}
}
Your Views would look something like
struct PickerListView<T: Hashable>: View {
#ObservedObject var vm: SectionViewModel<T>
#Environment(\.dismiss) var dismiss
var body: some View {
List {
ForEach(vm.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
vm.selected = item
dismiss()
}) {
HStack {
Text(String(describing: item))
Spacer()
if vm.selected == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
//Use didset for any changes
// .onChange(of: vm.sections) { _ in
// print("PickerListView: \(vm.sections.count)") // Doesn't run
// }
}
}
public struct PickerView<T: Hashable>: View {
#ObservedObject var vm: SectionViewModel<T>
public var body: some View {
//Navigation View should be at the top
//NavigationView {
NavigationLink(
String(describing: vm.selected),
destination: PickerListView<T>(vm: vm)
)
// }
//Use did set for any actions
// .onChange(of: vm.sections) { _ in
// print("PickerView: \(vm.sections.count)") // Runs
// print("PickerView section[0]items count: \(vm.sections[0].items.count)") // Runs
// }
}
}
struct SampleDeepView: View {
#StateObject var vm: SectionViewModel = .init(selected: "33", sections: [
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
])
var body: some View {
NavigationView{
VStack {
PickerView(vm: vm)
//Adding an item not a section
Button("Add Value", action: {
vm.sections[0].items.append("\(Int.random(in: 1...100))")
})
//Adding a section
Button("Add Section", action: {
vm.sections.append(
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
)
})
}
}
//Use didset for any actions
// .onChange(of: vm.sections) { _ in
// print("ContentView section[0]items count: \(vm.sections[0].items.count)") // Runs
// }
}
}
having a problem with updating data in ObservedObject
here's the data MRE:
struct Property: Identifiable, Codable, Hashable {
var id = UUID()
var name : String = ""
var meters : [Meter] = [Meter(name: "electricity"), Meter(name: "water")]
}
struct Meter: Identifiable, Hashable, Codable {
var id = UUID()
var name : String = ""
}
class PropertyData: ObservableObject {
#Published var properties: [Property]
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(properties) {
UserDefaults.standard.set(encoded, forKey: "PropertyData")
}
}
init() {
if let properties = UserDefaults.standard.data(forKey: "PropertyData") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Property].self, from: properties) {
self.properties = decoded
return
}
}
self.properties = [
Property(name: "Saks /1", meters: [Meter(name: "electricity")]),
Property(name: "Saks /2", meters: [Meter(name: "electricity"), Meter(name: "water")]),
]
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var data = PropertyData()
var body: some View {
NavigationView {
List {
ForEach(data.properties, id:\.self) {property in
NavigationLink(destination: NavigationLinkView(data: data, property: property)) {
Text(property.name)
}
}
}
}
}
}
struct NavigationLinkView: View {
#ObservedObject var data : PropertyData
var property:Property
var body: some View {
TabView {
MetersView(data: data, property: property)
.tabItem{
VStack {
Image(systemName: "scroll")
Text("Utitity")
}
}
}
}
}
struct MetersView: View {
#ObservedObject var data: PropertyData
#State private var metersPage = 0
#State var property : Property
#State private var addMeters = false
var body: some View {
VStack {
HStack {
Picker("Meters", selection: $metersPage) {
ForEach (0 ..< property.meters.count, id:\.self) {index in
Text(property.meters[index].name)
}
}
.pickerStyle(SegmentedPickerStyle())
Button{
print(property.meters)
addMeters.toggle()
} label: {
Image(systemName: "gear")
}
}.padding()
Spacer()
}.sheet(isPresented: $addMeters){AddMetersView(data: data, property: $property)}
}
}
struct AddMetersView: View {
#ObservedObject var data : PropertyData
#Binding var property : Property
#State var newMeter: String = ""
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form{
Section {
TextField("Add another meter", text: $newMeter)
.autocapitalization(.none)
Button{
if newMeter != "" {
property.meters.append(Meter(name: newMeter))
data.save()
print(property.meters.count)
} } label: {
Text("Add a meter")
}
}
ForEach(0..<property.meters.count, id:\ .self) {index in
Text(property.meters[index].name)
}
Section() {
Button("That's enough"){
print(property.meters)
presentationMode.wrappedValue.dismiss()}
}
}
}
}
}
I cannot understand, why the meters, that I add on AddMetersView, do update the Meters page, but then just goes away as soon as I go to the ContentView.
Plus, in my app, if I add a property, it does stay, and it persists, but not the Meters
This is similar to your previous question. Change #State var property : Property to
It is all about the connections. You break them in a few places along the way I made some changes and put comments along the line.
import SwiftUI
struct MetersParentView: View {
//Change to StateObject
#StateObject var data = PropertyData()
var body: some View {
NavigationView {
List {
ForEach($data.properties, id:\.id) {$property in
NavigationLink(destination: NavigationLinkView(data: data, property: $property)) {
Text(property.name)
}
}
}
}
}
}
struct NavigationLinkView: View {
#ObservedObject var data : PropertyData
//Make Binding there is no connection without it
#Binding var property : Property
var body: some View {
TabView {
MetersView(data: data, property: $property)
.tabItem{
VStack {
Image(systemName: "scroll")
Text("Utitity")
}
}
}
}
}
struct MetersView: View {
#ObservedObject var data: PropertyData
#State var selectedMeter: Meter = .init()
//Change to Binding
//State is a source of truth, it breaks the connection
#Binding var property : Property
#State private var addMeters = false
var body: some View {
VStack {
HStack {
Picker("Meters", selection: $selectedMeter) {
//Use object not index
ForEach(property.meters, id:\.id) {meter in
//Tag adjustment
Text(meter.name).tag(meter as Meter)
}
}.onAppear(perform: {
selectedMeter = property.meters.first ?? Meter()
})
.pickerStyle(SegmentedPickerStyle())
Button{
print(property.meters)
addMeters.toggle()
} label: {
Image(systemName: "gear")
}
}.padding()
Spacer()
}.sheet(isPresented: $addMeters){
AddMetersView(data: data, property: $property)
}
}
}
struct AddMetersView: View {
#ObservedObject var data : PropertyData
#Binding var property : Property
#State var newMeter: String = ""
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form{
Section {
TextField("Add another meter", text: $newMeter)
.autocapitalization(.none)
Button{
if newMeter != "" {
print(property.meters.count)
property.meters.append(Meter(name: newMeter))
print(property.meters.count)
data.save()
print(property.meters.count)
}
} label: {
Text("Add a meter")
}
}
//Dont use index
ForEach(property.meters, id:\ .id) {meter in
Text(meter.name)
}
Section() {
Button("That's enough"){
print(property.meters)
presentationMode.wrappedValue.dismiss()}
}
}
}
}
}
struct MetersParentView_Previews: PreviewProvider {
static var previews: some View {
MetersParentView()
}
}
I have two models:
struct Category: Identifiable {
var id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
}
and:
struct ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked = false
}
with:
class Checklist: ObservableObject {
#Published var items = [Category]()
func deleteListItem(whichElement: IndexSet) {
items.remove(atOffsets: whichElement)
}
func moveListItem(whichElement: IndexSet, destination: Int) {
items.move(fromOffsets: whichElement, toOffset: destination)
}
}
I try to implement tap on row to check and uncheck cheklist item in tableView with sections and rows, but I cannot get how this can be released. My code:
struct ChecklistView: View {
#EnvironmentObject var checklist: Checklist
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}
.background(Color.white)
.onTapGesture {
if let matchingIndex =
category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].isChecked.toggle()
}
}
}
}
}
.onDelete(perform: checklist.deleteListItem)
.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: { self.newChecklistItemViewIsVisible = true }) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
NewChecklistItemView(checklist: self.checklist)
}
}
}
I get error with this code on line with category.items[matchingIndex].isChecked.toggle():
Cannot use mutating member on immutable value: 'category' is a 'let' constant
How I can get to ChecklistItem and make it check and uncheck on tap.
import SwiftUI
//Change to class and add NSObject structs are immutable
class Category: NSObject, Identifiable {
let id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
//Now you need an init
init(title: String , number: Int, items: [ChecklistItem]) {
self.title = title
self.number = number
self.items = items
}
}
//Change to class and add NSObject structs are immutable
class ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked: Bool = false
//Now you need an init
init(name: String) {
self.name = name
}
}
class Checklist: ObservableObject {
#Published var items = [Category]()
}
struct ChecklistView: View {
//Can be an #EnvironmentObject if the #ObservedObject comes from a higher View
#ObservedObject var checklist: Checklist = Checklist()
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
Button(action: {
print(item.isChecked.description)
item.isChecked.toggle()
//Something to trigger the view to refresh will not be necessary if using something like #FetchRequest or after you somehow notify `checklist.items` that there is a change
checklist.objectWillChange.send()
}) {
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}//HStack
//White is incompatible with Text Color in Dark Mode
.background(Color.gray)
}//Button
}//ForEach
}//Section
}//ForEach
//Methods not provided
//.onDelete(perform: checklist.deleteListItem)
//.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: {
self.newChecklistItemViewIsVisible = true
//Code to Add Samples
checklist.items.append(Category(title: "Test", number: Int.random(in: 0...100), items: [ChecklistItem(name: "Test")]))
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
//Pass as an #EnvironmentObject
NewChecklistItemView().environmentObject(checklist)
}
}
}
struct NewChecklistItemView: View {
#EnvironmentObject var checklist: Checklist
var body: some View {
Text(checklist.items.count.description)
}
}
struct ChecklistView_Previews: PreviewProvider {
static var previews: some View {
//When the #ObservedObject comes from a higher View remove comment below
ChecklistView()//.environmentObject(Checklist())
}
}
The reason you are getting that error is because structs are immutable. You should use method marked with "mutating" inside desired struct. Something like
if let matchingIndex = category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].toggleItem()
}
and inside your struct:
mutating func toggleItem() {
self.isChecked.toggle()
}
But i would recommend you to use #State instead, because what you are trying to do is straight forward related to how you represent your view. And later, when user is willing to do something with that selection you send that data to your model
I have a problem when I enter a character within a TextField (Within ExerciseSetView), I have to re-click the text box to make it so I can enter another character. If I remove the bindings from the Textfield I can enter text fluidly.
I think it has something to do with my presenter class and the updateSet function recreating a set instance because I have to replace some values two levels deep within an array.
//
// ContentView.swift
// test
//
//
import SwiftUI
import Combine
import CoreData
class WorkoutExerciseSetVM: Hashable, ObservableObject {
#Published public var id: Int
#Published public var reps: String
#Published public var weight: String
init(id: Int, reps: String, weight: String) {
self.id = id
self.reps = reps
self.weight = weight
}
static func ==(lhs: WorkoutExerciseSetVM, rhs: WorkoutExerciseSetVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutExerciseVM: Hashable, ObservableObject {
#Published public var id: UUID
#Published public var name: String
#Published public var sets: [WorkoutExerciseSetVM]
init(id: UUID, name: String, sets: [WorkoutExerciseSetVM]) {
self.id = id
self.name = name
self.sets = sets
}
static func ==(lhs: WorkoutExerciseVM, rhs: WorkoutExerciseVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutVM: Hashable, ObservableObject {
#Published public var id = UUID()
#Published public var name: String
#Published public var exercises: [WorkoutExerciseVM]
#Published public var started: Date? = Date()
#Published public var completed: Date? = Date()
init(id: UUID, name: String, exercises: [WorkoutExerciseVM], started: Date?, completed: Date?) {
self.id = id
self.name = name
self.exercises = exercises
self.started = started
self.completed = completed
}
static func ==(lhs: WorkoutVM, rhs: WorkoutVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutPresenter: ObservableObject {
#Published public var id: UUID
#Published public var exercises: [WorkoutExerciseVM]
#Published public var name: String
#Published public var started: Date?
#Published public var completed: Date?
init(routine: WorkoutVM) {
self.id = UUID()
self.name = routine.name
self.started = Date()
self.completed = nil
self.exercises = routine.exercises.map{ exercise in
return WorkoutExerciseVM(
id: UUID(),
name: exercise.name,
sets: [
WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0")
]
)
}
}
func removeExercise(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.filter{$0.id.uuidString != exerciseId}
}
func addSet(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.map {
if ($0.id.uuidString == exerciseId) {
if ($0.sets.count == 0) {
$0.sets.append(WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0"))
}
if let lastSet = $0.sets.last {
$0.sets.append(WorkoutExerciseSetVM(id: lastSet.id + 1, reps: lastSet.reps, weight: lastSet.weight))
}
}
return $0
}
}
func updateSet(id: UUID, set: WorkoutExerciseSetVM) {
let exerciseId = id.uuidString
self.exercises = self.exercises.map{
if $0.id.uuidString == exerciseId {
$0.sets = $0.sets.map{(oldExerciseSet) -> WorkoutExerciseSetVM in
if oldExerciseSet.id == set.id {
return set
}
return oldExerciseSet
}
return $0
}
return $0;
}
}
func removeSet(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.map{(exercise) -> WorkoutExerciseVM in
if exercise.id.uuidString == exerciseId {
let newExercise = exercise
if newExercise.sets.count > 1 {
newExercise.sets.removeLast()
}
return newExercise
}
return exercise;
}
}
}
struct ContentView: View {
var body: some View {
VStack {
WorkoutView(presenter: WorkoutPresenter(routine: WorkoutVM(id: UUID(), name: "Test", exercises: [WorkoutExerciseVM(id: UUID(), name: "Exercise", sets: [WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0")])], started: nil, completed: nil)))
}
}
}
struct WorkoutView: View {
#ObservedObject var presenter: WorkoutPresenter
var body: some View {
return GeometryReader { geo in
ZStack {
VStack {
ScrollView {
ForEach(self.presenter.exercises, id: \.self) { exercise in
ExerciseView(presenter: self.presenter, exercise: exercise)
}
}
}
}
}
}
}
struct ExerciseView: View {
#ObservedObject var presenter: WorkoutPresenter
var exercise: WorkoutExerciseVM
var body: some View {
VStack {
VStack(alignment: .leading) {
VStack {
VStack {
ForEach(exercise.sets, id: \.self) { exerciseSet in
ExerciseSetView(
set: exerciseSet,
onUpdate: { newExerciseSet in
self.presenter.updateSet(id: self.exercise.id, set: newExerciseSet)
}
)
}
}
}
}
HStack {
Button(action: {
self.presenter.addSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "plus")
Text("Add Set")
}
}
Button(action: {
self.presenter.removeSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "minus")
Text("Remove Set")
}
}
}
}
}
}
struct ExerciseSetView: View {
var set: WorkoutExerciseSetVM
var onUpdate: (_ set: WorkoutExerciseSetVM) -> Void
var body: some View {
let repBinding = Binding(
get: {
String(self.set.reps)
},
set: {
if ($0 as String?) != nil {
self.onUpdate(WorkoutExerciseSetVM(id: self.set.id, reps: $0 , weight: self.set.weight))
}
}
)
let weightBinding = Binding(
get: {
String(self.set.weight)
},
set: {
if ($0 as String?) != nil {
self.onUpdate(WorkoutExerciseSetVM(id: self.set.id, reps: self.set.reps, weight: $0 ))
}
}
)
return HStack {
Spacer()
// textfield that isn't working
TextField("", text: repBinding)
Spacer()
// textfield that isn't working
TextField("", text: weightBinding)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Your code has some fundamental errors. Please research about ObservableObject and Published values before going into production with this code. Otherwise, it would be quite hard to deal with this code later.
I have updated your views and that seems to work. You are not using ObservableObject as they should be used. Just pass ObservableObjectss around let them do the bindings for you instead of setting custom bindings.
struct ExerciseView: View {
#ObservedObject var presenter: WorkoutPresenter
#ObservedObject var exercise: WorkoutExerciseVM
var body: some View {
VStack {
VStack(alignment: .leading) {
ForEach(exercise.sets, id: \.self) { exerciseSet in
ExerciseSetView(set: exerciseSet)
}
}
HStack {
Button(action: {
self.presenter.addSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "plus")
Text("Add Set")
}
}
Button(action: {
self.presenter.removeSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "minus")
Text("Remove Set")
}
}
}
}
}
}
struct ExerciseSetView: View {
#ObservedObject var set: WorkoutExerciseSetVM
var body: some View {
HStack {
Spacer()
TextField("", text: $set.reps)
Spacer()
TextField("", text: $set.weight)
}
}
}
Let me know if this works for you.