I would like to create a Settings view using SwiftUI. I mainly took the official example from Apple about SwiftUI to realize my code. The settings view should have a toggle to whether display or not my favorites items.
For now I have a landmarks list and a settings view.
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var imageName: String
var title: String
var isFavorite: Bool
var description: String
enum CodingKeys: String, CodingKey {
case id, imageName, title, description
}
}
final class UserData: ObservableObject {
#Published var showFavoriteOnly: Bool = false
#Published var items: [Landmark] = landmarkData
#Published var showProfile: Bool = false
}
struct ItemList: View {
#EnvironmentObject var userData: UserData
#State var trailing: Bool = false
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
NavigationView {
List {
VStack {
CircleBadgeView(text: String(landmarkData.count), thickness: 2)
Text("Tutorials available")
}.frame(minWidth:0, maxWidth: .infinity)
ForEach(userData.items) { landmark in
if !self.userData.showFavoriteOnly || landmark.isFavorite {
ZStack {
Image(landmark.imageName)
.resizable()
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.overlay(ImageOverlay(text: landmark.title), alignment: .bottomTrailing)
Text(String(landmark.isFavorite))
NavigationLink(destination: TutorialDetailView(landmark: landmark)) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
}
}
}.navigationBarTitle("Tutorials")
.navigationBarItems(trailing: trailingItem())
}
}
}
extension ItemList {
func trailingItem () -> some View {
return HStack {
if userData.showProfile {
NavigationLink(destination: ProfileView()) {
Image(systemName: "person.circle")
.imageScale(.large)
.accessibility(label: Text("Profile"))
}
}
NavigationLink(destination: SettingsView().environmentObject(userData)) {
Image(systemName: "gear")
.imageScale(.large)
.accessibility(label: Text("Settings"))
}
}
}
}
As you can see my SettingsView is accessible from navigationBarItems of my NavigationView. I don't know if it's the problem or not but when I put the Toggle inside the ListView it works as expected. But now when I trigger the toggle to enable only favorite my application crash instantly.
I've tried to trigger the Show profile toggle from SettingsView and it works.
struct SettingsView: View {
#EnvironmentObject var userData: UserData
var body: some View {
Form {
Section(header: Text("General")) {
Toggle(isOn: $userData.showProfile) {
Text("Show profile")
}
Toggle(isOn: $userData.showFavoriteOnly) {
Text("Favorites only")
}
}
Section(header: Text("UI")) {
Toggle(isOn: .constant(false)) {
Text("Dark mode")
}
NavigationLink(destination: Text("third")) {
Text("Third navigation")
}
}
}.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
}
In brief, the crash appears in my SettingsView when I trigger the Show only favorite Toggle and then I try to go back to the previous view which is ItemListView
The only information I can get about the error is Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
You can find the whole project on my GitHub : https://github.com/Hurobaki/swiftui-tutorial
Some help would be really appreciated :)
Here is a minimal version of your example code, that works:
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var imageName: String
var title: String
var isFavorite: Bool
var description: String
}
final class UserData: ObservableObject {
#Published var showFavoriteOnly: Bool = false
#Published var items: [Landmark] = [
Landmark(id: 1, imageName: "a", title: "a", isFavorite: true, description: "A"),
Landmark(id: 2, imageName: "b", title: "b", isFavorite: false, description: "B")
]
}
struct ContentView: View {
#EnvironmentObject var userData: UserData
var body: some View {
NavigationView {
List(userData.items.filter { !userData.showFavoriteOnly || $0.isFavorite }) { landmark in
Text(String(landmark.isFavorite))
}
.navigationBarTitle("Tutorials")
.navigationBarItems(trailing: trailingItem())
}
}
func trailingItem () -> some View {
return HStack {
NavigationLink(destination: SettingsView()) {
Text("Settings")
}
}
}
}
struct SettingsView: View {
#EnvironmentObject var userData: UserData
var body: some View {
Form {
Section(header: Text("General")) {
Toggle(isOn: $userData.showFavoriteOnly) {
Text("Favorites only")
}
}
}.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
}
Related
I am building an app for iOS and Mac Catalyst and have been able to code most of the
experience that I want except for functions that use swipe to delete in iOS.
The view includes multiple sections, each with a List and ForEach closure. I want to
be able to add the EditButton() function to the header of each section and have it
apply only to that section's List.
I can
add an EditButton() function to gain this functionality, however,
so far I have only been able to make that work for the entire screen, not for the
individual sections.
I have tried refactoring the code for each section into functions and into structs
(as shown below). In all cases the EditButton() activates the delete icons for ALL
list rows, not just the section with the button.
I have also tried placing the EditButton() inside the section in the VStack. No difference.
Here's a simple example with the latest code attempt:
struct ContentView: View {
#State private var selectedItem: String?
#State private var items = ["One", "Two", "Three", "Four", "Five"]
#State private var fruits = ["Apple", "Orange", "Pear", "Lemon", "Grape"]
var body: some View {
NavigationSplitView {
ItemSection(selectedItem: $selectedItem, items: $items)
FruitSection(selectedItem: $selectedItem, fruits: $fruits)
} detail: {
if let selectedItem {
ItemDetailView(selectedItem: selectedItem)
} else {
EmptyView()
}
}//nav
}//body
}//struct
struct ItemSection: View {
#Binding var selectedItem: String?
#Binding var items: [String]
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}
.onDelete { items.remove(atOffsets: $0) }
}
.listStyle(PlainListStyle())
}//v
.padding()
} header: {
HStack {
Text("Section for Items")
Spacer()
//uncomment when you have it working
//#if targetEnvironment(macCatalyst)
EditButton()
//#endif
}//h
.padding(.horizontal, 10)
}//section and header
}//body
}//item section
struct FruitSection: View {
#Binding var selectedItem: String?
#Binding var fruits: [String]
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(fruits, id: \.self) { fruit in
NavigationLink(value: fruit) {
Text(fruit)
}
}
.onDelete { fruits.remove(atOffsets: $0) }
}
.listStyle(PlainListStyle())
}//v
.padding()
} header: {
HStack {
Text("Section for Fruits")
Spacer()
}//h
.padding(.horizontal, 10)
}//section fruit
}//body
}//fruit section
struct ItemDetailView: View {
var selectedItem: String
var body: some View {
VStack {
Text(selectedItem)
Text("This is the DetailView")
}
}
}
Any guidance would be appreciated. Xcode 14.0.1 iOS 16
import SwiftUI
struct ContentView: View {
#State private var selectedItem: String?
#State private var items = ["One", "Two", "Three", "Four", "Five"]
#State private var fruits = ["Apple", "Orange", "Pear", "Lemon", "Grape"]
var body: some View {
NavigationSplitView {
ItemSection(selectedItem: $selectedItem, items: $items)
FruitSection(selectedItem: $selectedItem, fruits: $fruits)
} detail: {
if let selectedItem {
ItemDetailView(selectedItem: selectedItem)
} else {
EmptyView()
}
}//nav
}//body
}//struct
struct ItemSection: View {
#Binding var selectedItem: String?
#Binding var items: [String]
#State var isEditMode = false // this is what you need
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}
.onDelete { items.remove(atOffsets: $0) }
}
.environment(\.editMode, isEditMode ? .constant(.active) : .constant(.inactive)) // and set this
.listStyle(PlainListStyle())
}//v
.padding()
} header: {
HStack {
Text("Section for Items")
Spacer()
//uncomment when you have it working
//#if targetEnvironment(macCatalyst)
Button { // you also need to set EditButton() -> Button()
withAnimation {
isEditMode.toggle()
}
} label: {
Text(isEditMode ? "Done" : "Edit")
}
//#endif
}//h
.padding(.horizontal, 10)
}//section and header
}//body
}//item section
struct FruitSection: View {
#Binding var selectedItem: String?
#Binding var fruits: [String]
#State var isEditMode = false // same as this section
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(fruits, id: \.self) { fruit in
NavigationLink(value: fruit) {
Text(fruit)
}
}
.onDelete { fruits.remove(atOffsets: $0) }
}
.environment(\.editMode, isEditMode ? .constant(.active) : .constant(.inactive))
.listStyle(PlainListStyle())
}//v
.padding()
} header: {
HStack {
Text("Section for Fruits")
Spacer()
Button {
withAnimation {
isEditMode.toggle()
}
} label: {
Text(isEditMode ? "Done" : "Edit")
}
}//h
.padding(.horizontal, 10)
}//section fruit
}//body
}//fruit section
struct ItemDetailView: View {
var selectedItem: String
var body: some View {
VStack {
Text(selectedItem)
Text("This is the DetailView")
}
}
}
Here's a more general & simplified approach using PreferenceKey:
struct EditModeViewModifier: ViewModifier {
var forceEditing: Bool?
#State var isEditing = false
func body(content: Content) -> some View {
content
.onPreferenceChange(IsEditingPrefrenceKey.self) { newValue in
withAnimation {
isEditing = newValue
}
}.environment(\.editMode, .constant((forceEditing ?? isEditing) ? .active: .inactive))
}
}
extension View {
func editMode(_ editing: Bool? = nil) -> some View {
modifier(EditModeViewModifier(forceEditing: editing))
}
}
struct EditingButton: View {
#State var isEditing = false
var body: some View {
Button(action: {
isEditing.toggle()
}) {
Text(isEditing ? "Done" : "Edit")
}.preference(key: IsEditingPrefrenceKey.self, value: isEditing)
}
}
struct IsEditingPrefrenceKey: PreferenceKey {
static var defaultValue = false
static func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue()
}
}
You use EditingButton instead of EditButton, & use .editMode() at then end of your View. Then your sections become something like this:
struct ItemSection: View {
#Binding var selectedItem: String?
#Binding var items: [String]
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}.onDelete { items.remove(atOffsets: $0) }
}.listStyle(PlainListStyle())
}.padding()
} header: {
HStack {
Text("Section for Items")
Spacer()
//uncomment when you have it working
//#if targetEnvironment(macCatalyst)
EditingButton()
//#endif
}.padding(.horizontal, 10)
}.editMode()
}
}
struct FruitSection: View {
#Binding var selectedItem: String?
#Binding var fruits: [String]
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(fruits, id: \.self) { fruit in
NavigationLink(value: fruit) {
Text(fruit)
}
}.onDelete { fruits.remove(atOffsets: $0) }
}.listStyle(PlainListStyle())
}.padding()
} header: {
HStack {
Text("Section for Fruits")
Spacer()
EditingButton()
}.padding(.horizontal, 10)
}.editMode()
}
}
A more concise version of Timmy's answer uses this reusable code:
import SwiftUI
struct EditingButton: View {
#Binding var isEditing: Bool
var body: some View {
Button(isEditing ? "Done" : "Edit", action: changeEditing)
.preference(key: IsEditingPrefrenceKey.self, value: isEditing)
}
func changeEditing() { withAnimation { isEditing.toggle() } }
struct IsEditingPrefrenceKey: PreferenceKey {
static var defaultValue = false
static func reduce(value: inout Bool, nextValue: () -> Bool) { value = nextValue() }
}
}// EditingButton
extension View {
func editMode(_ editing: Bool) -> some View {
environment(\.editMode, editing ? .constant(.active) : .constant(.inactive))
}
}
Then the ItemSection is like this:
struct ItemSection: View {
#Binding var selectedItem: String?
#Binding var items: [String]
#State var isEditMode = false // this is what you need
var body: some View {
Section {
VStack {
List(selection: $selectedItem) {
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}
.onDelete { items.remove(atOffsets: $0) }
.onMove(perform: move)
}
.editMode(isEditMode)
.listStyle(PlainListStyle())
}//v
.padding()
} header: {
HStack {
Text("Section for Items")
Spacer()
EditingButton(isEditing: $isEditMode)
}//h
.padding(.horizontal, 10)
}//section and header
}// body
private func move(indexes: IndexSet, dest: Int) {
print("Move item from indexset \(indexSetList(indexes)) to index \(dest)")
items.move(fromOffsets: indexes, toOffset: dest)
}// move
}//item section
func indexSetList(_ indexes: IndexSet) -> String {
guard !indexes.isEmpty else { return "none"}
return Array(indexes).map(String.init).joined(separator: " ")
}
I have allowed drag and drop reordering to make it do what I was interested in as well.
I have a child view that updates an EnvironmentObject that then causes the child view to pop back to its parent view. I am creating an app that uses similar "Like" functionality from this tutorial: https://www.hackingwithswift.com/books/ios-swiftui/letting-the-user-mark-favorites
Every time the like button is clicked and the EnvironmentObject likes object is updated, the view pops to the previous (ProductGridView) view instead of staying on the child view (ProductDetailView).
struct ContentView: View {
#State private var tabSelection = 0
#ObservedObject var products = Products()
#ObservedObject var favorites = Favorites()
var body: some View {
VStack {
TabView(selection: $tabSelection) {
NavigationView{
ProductGridView()
}
.tabItem { Image(systemName: "megaphone")
Text("Products")
}.tag(0)
.environmentObject(products)
.environmentObject(favorites)
}
struct ProductGridView: View {
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
#EnvironmentObject var products: Products
#EnvironmentObject var favorites: Favorites
var body: some View {
VStack{
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 20) {
ForEach(products.products, content: {
product in
NavigationLink(destination: ProductDetailView(product: product)) {
ProductCellView(product: product)
.padding(.horizontal, 10)
}
})
}
}.onAppear() {
self.products.fetchData()
}
}
struct ProductDetailView: View {
let product: Product
#EnvironmentObject var favorites: Favorites
var body: some View {
ScrollView(showsIndicators: false) {
VStack{
ProductImageView(product: product)
Button(action: {
if favorites.contains(product) {
favorites.remove(product) //Updating here causes issue
} else {
favorites.add(product) //Updating here causes issue
}
}) {
if favorites.contains(product){
Image(systemName: "heart.fill")
}
else{
Image(systemName: "heart")
}
}
}
class Favorite : Identifiable, Encodable {
var id = UUID()
var name: String
...
class Favorites: ObservableObject {
#Published private var products: [String]?
...
struct Product: Identifiable{
let id = UUID()
let productname: String
...
It looks like your fetch data call may be causing the issue.
struct YourContentView: View {
#State private var tabSelection = 0
#ObservedObject var products = Products()
#ObservedObject var favorites = Favorites()
var body: some View {
VStack {
TabView(selection: $tabSelection) {
NavigationView{
ProductGridView()
}
.tabItem { Image(systemName: "megaphone")
Text("Products")
}.tag(0)
.environmentObject(products)
.environmentObject(favorites)
}
}
}
}
struct ProductDetailView: View {
let product: Product
#EnvironmentObject var favorites: Favorites
var body: some View {
ScrollView(showsIndicators: false) {
VStack{
Image(systemName: "photo")
.resizable()
.scaledToFit()
Button {
if favorites.products?.contains(product) ?? false {
favorites.remove(product) //Updating here causes issue
} else {
favorites.add(product) //Updating here causes issue
}
} label: {
if favorites.products?.contains(product) ?? false{
Image(systemName: "heart.fill")
}
else{
Image(systemName: "heart")
}
}
}
}
}
}
struct ProductCellView: View {
#State var product: Product
var body: some View {
HStack {
Image(systemName: "photo")
Text(product.productname)
}
}
}
struct ProductGridView: View {
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
#EnvironmentObject var products: Products
#EnvironmentObject var favorites: Favorites
var body: some View {
VStack{
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 20) {
ForEach(products.products, content: {
product in
NavigationLink(destination: ProductDetailView(product: product)) {
ProductCellView(product: product)
.padding(.horizontal, 10)
}
})
}
}.onAppear() {
self.products.fetchData()
}
}
}
}
class Favorites: ObservableObject {
#Published var products: [Product]? = []
init() {}
func remove(_ product: Product) {
products?.removeAll(where: { $0.id == product.id })
}
func add(_ product: Product) {
products?.append(product)
}
}
class Products: ObservableObject, Identifiable {
#Published var products: [Product]
init() {
products = [.init(productname: "ApplePie"), .init( productname: "Cheeseburger")]
}
func fetchData() {
// uncommenting this code will cause product grid view to reload because it relies on products
// products = [.init(productname: "ApplePie"), .init( productname: "Cheeseburger")]
}
}
struct Product: Identifiable, Equatable {
let id = UUID()
let productname: String
public static func ==(lhs: Product, rhs: Product) -> Bool {
lhs.id == rhs.id
}
}
I have 2 Views in my Swift Project and when I click on the Button on the secondView, I want to update the List in the First View. I don't know how to do it! If I use a static variable in my MainView and then edit this variable from the secondView, it works, but it won't update. And if I don't use static and instead use #State, it would update, but I can't access it from my secondView.
Here is the Code below:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
MainView()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
var arrayList: [CreateListItems] = []
init() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(arrayList) { x in
Text("\(x.name)")
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
func updateList() {
//Code that should remove "FirstName" from the List in MainView
}
var body: some View {
return VStack {
Button(action: {
updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}
struct CreateListItems: Identifiable {
var id: UUID = UUID()
var name: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can share it using #State and #Binding if you put
struct ContentView: View {
#State var arrayList: [CreateListItems] = []
struct MainView: View {
#Binding var arrayList: [CreateListItems]
struct UpdateOtherViewFromHere: View {
#Binding var arrayList: [CreateListItems]
or you use the MVVM pattern and store the list in an ObservableObject and use #StateObject/#ObservedObject (source) and use #EnvironmentObject(connection) to share it between your Views.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
class ParentViewModel: ObservableObject{
#Published var arrayList: [CreateListItems] = []
init(){
addSamples()
}
func addSamples() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
func updateList() {
let a = CreateListItems(name: "\(arrayList.count + 1) Name!")
arrayList.append(a)
}
}
struct ParentView: View {
#StateObject var vm: ParentViewModel = ParentViewModel()
var body: some View {
TabView {
MainView().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(vm.arrayList) { x in
Text(x.name)
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
Button(action: {
vm.updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}
In my navigation, I want to be able to go from ContentView -> ModelListView -> ModelEditView OR ModelAddView.
Got this working, my issue now being that when I hit the Back button from ModelAddView, the intermediate view is omitted and it pops back to ContentView; a behaviour that
ModelEditView does not have.
There's a reason for that I guess – how can I get back to ModelListView when dismissing ModelAddView?
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List{
NavigationLink(
destination: ModelListView(),
label: {
Text("1. Model")
})
Text("2. Model")
Text("3. Model")
}
.padding()
.navigationTitle("Test App")
}
}
}
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing:
NavigationLink(
destination: ModelAddView(modelViewModel: $modelViewModel), label: {
Image(systemName: "plus")
})
)
}
}
struct ModelEditView: View {
#Binding var model: Model
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelAddView: View {
#Binding var modelViewModel: ModelViewModel
#State var model = Model(id: UUID(), titel: "")
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelViewModel {
var modelValues: [Model]
init() {
self.modelValues = [ //mock data
Model(id: UUID(), titel: "Foo"),
Model(id: UUID(), titel: "Bar"),
Model(id: UUID(), titel: "Buzz")
]
}
}
struct Model: Identifiable, Equatable {
let id: UUID
var titel: String
}
Currently placing a NavigationLink in the .navigationBarItems may cause some issues.
A possible solution is to move the NavigationLink to the view body and only toggle a variable in the navigation bar button:
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
#State var isAddLinkActive = false // add a `#State` variable
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
}
)
}
.background( // move the `NavigationLink` to the `body`
NavigationLink(destination: ModelAddView(modelViewModel: $modelViewModel), isActive: $isAddLinkActive) {
EmptyView()
}
.hidden()
)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: trailingButton)
}
// use a Button to activate the `NavigationLink`
var trailingButton: some View {
Button(action: {
self.isAddLinkActive = true
}) {
Image(systemName: "plus")
}
}
}
Newbie here.
My problem simplified:
I have a Person struct consisting of 2 strings - first and last name.
An initial array with a few persons (ex. "Bob" "Smith", "Joe" "Johnson", etc.)
A list view showing each member.
Clicking on a row in the list shows a detail view - call it "person card" view - which shows the first name and last name.
I then have a modal view to edit these variables.
Currently the Save button on the modal only closes the modal. However, because I am using bindings on the modal view to the values on the "person card" view, the "person card" view is updated with the changed data when the modal closes.
The list view though still shows the original value(s) and not the updated data (as I expect). I know that I have to add as method to the save function but I'm not sure what. I know how to insert and append to an array but I can't find an update array method.
FYI - The data model I am using is a "store" instance of a class that is an ObservableObject. I have that variable declared as an EnvironmentObject on each view.
Here is the code as requested:
struct PatientData: Identifiable
{
let id = UUID()
var patientName: String
var age: String
}
let patientDataArray: [PatientData] =
[
PatientData(patientName: "Charles Brown", age: "68"),
PatientData(patientName: "Jim Morrison", age: "36"),
]
final class PatientDataController: ObservableObject
{
#Published var patients = patientDataArray
{
struct PatientList: View
{
#EnvironmentObject var patientDataController: PatientDataController
#State private var showModalSheet = false
var body: some View
{
NavigationView
{
List
{
ForEach(patientDataController.patients)
{ patientData in NavigationLink(destination: PatientInfoCard(patientData: patientData))
{ PatientListCell(patientData: patientData) }
}
.onMove(perform: move)
.onDelete(perform: delete)
.navigationBarTitle(Text("Patient List"))
}
struct PatientInfoCard: View
{
#EnvironmentObject var patientDataController: PatientDataController
#State var patientData: PatientData
#State private var showModalSheet = false
var body: some View
{
VStack(alignment: .leading, spacing: 8)
{ // Change to patientDataArray???
Text(patientData.patientName)
.font(.largeTitle)
BasicInfo(patientData: patientData)
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding()
// Can't push Edit button more than once
.navigationBarItems(trailing: Button(action:
{self.showModalSheet = true})
{Text("Edit")})
.sheet(isPresented: $showModalSheet)
{
EditPatientModal(patientData: self.$patientData, showModalSheet: self.$showModalSheet)
.environmentObject(self.patientDataController)
}
}
}
struct EditPatientModal: View
{
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var patientDataController: PatientDataController
#Binding var patientData: PatientData
#Binding var showModalSheet: Bool
var body: some View
{
NavigationView
{
VStack(alignment: .leading)
{
Text("Name")
.font(.headline)
TextField("enter name", text: $patientData.patientName)
Text("Age")
.font(.headline)
TextField("enter age", text: $patientData.age)
}
.navigationBarTitle(Text("Edit Patient"), displayMode: .inline)
.navigationBarItems(
leading: Button("Cancel")
{ self.cancel() },
trailing: Button("Save")
{ self.save() } )
}
}
private func save()
{
self.presentationMode.wrappedValue.dismiss()
}
Here is my updated code:
class PatientData: ObservableObject, Identifiable
{
let id = UUID()
#Published var patientName = ""
#Published var age = ""
init(patientName: String, age: String)
{
self.patientName = patientName
self.age = age
}
}
let patientDataArray: [PatientData] =
[
PatientData(patientName: "Charles Brown", age: "68"),
PatientData(patientName: "Jim Morrison", age: "36")
]
final class PatientDataController: ObservableObject
{
#Published var patients = patientDataArray
}
struct PatientList: View
{
#EnvironmentObject var patientDataController: PatientDataController
#EnvironmentObject var patientData: PatientData
#State private var showModalSheet = false
var body: some View
{
NavigationView
{
List
{
ForEach(self.patientDataController.patients.indices)
{ idx in
NavigationLink(destination: PatientInfoCard(patientData: self.$patientDataController.patients[idx]))
/*Cannot convert value of type 'Binding<PatientData>' to expected argument type 'PatientData'*/ <-- My one error message; in NavigationLink
{ PatientListCell(patientData: self.$patientDataController.patients[idx]) }
}
.onMove(perform: move)
.onDelete(perform: delete)
.navigationBarTitle(Text("Patient List"))
}
.navigationBarItems(leading: EditButton())
struct PatientInfoCard: View
{
#EnvironmentObject var patientDataController: PatientDataController
#Binding var patientData: PatientData
#State private var showModalSheet = false
var body: some View
{
VStack(alignment: .leading, spacing: 8)
{
Text(patientData.patientName)
.font(.largeTitle)
BasicInfo(patientData: patientData)
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding()
.navigationBarItems(trailing: Button(action:
{self.showModalSheet = true})
{Text("Edit")})
.sheet(isPresented: $showModalSheet)
{
EditPatientModal(patientData: self.$patientData, showModalSheet: self.$showModalSheet)
.environmentObject(self.patientDataController)
}
}
}
struct BasicInfo: View
{
#EnvironmentObject var patientDataController: PatientDataController
#State var patientData: PatientData
var patientDataIndex: Int
{
patientDataController.patients.firstIndex(where: { $0.id == patientData.id })!
}
var body: some View
{
VStack(alignment: .leading, spacing: 8)
{
Text("Age:")
.font(.headline)
Text(patientData.age)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
struct EditPatientModal: View
{
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var patientDataController: PatientDataController
#Binding var patientData: PatientData
#Binding var showModalSheet: Bool
var body: some View
{
NavigationView
{
VStack(alignment: .leading)
{
Text("Name")
.font(.headline)
TextField("enter name", text: $patientData.patientName)
Text("Age")
.font(.headline)
TextField("enter age", text: $patientData.age)
}
.navigationBarTitle(Text("Edit Patient"), displayMode: .inline)
.navigationBarItems(
leading: Button("Cancel")
{ self.cancel() },
trailing: Button("Save")
{ self.save() } )
}
}
private func save()
{
self.presentationMode.wrappedValue.dismiss()
}
You are missing 2 things in your code.
Your struct needs to be ObservableObject otherwise any changes happen to it will not get effected and in order for it to be ObservableObject it has to be a class so first change:
class PatientData: ObservableObject, Identifiable
{
let id = UUID()
#Published var patientName: String
#Published var age: String
init(patientName: String, age: String) {
self.patientName = patientName
self.age = age
}
}
I understand you have an environmentObject which is publishing, but it's only publishing changes to the array, meaning adding or removing items but not to individual patientData objects.
2nd thing to change is in your forEach loop you need pass Patient as a Bind and in order to do that you have to loop through indices and then access the data through Bind
NavigationView
{
if(self.patientDataController.patients.count > 0) {
List {
ForEach(self.patientDataController.patients.enumerated().map({$0}), id:\.element.id) { idx, patient in
NavigationLink(destination: PatientInfoCard(patientData: self.$patientDataController.patients[idx])) {
Text(patient.patientName)
}
}
}
.navigationBarItems(leading: EditButton())
} else {
Text("List is empty")
}
}
Let us know if this doesn't work