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
Related
I am sorry im not sure that the title makes sense but if u read, im sure u will understand my problem. I have declared a variable #State var timetext: Int32 in the file CreatingWorkout, with a textfield TextField("5000, 100 etc", value: $timetext, formatter: NumberFormatter()) When i go to the createWorkoutView file and try to present it with the sheet, it wants me to give a value to timetext. However, when i provide a value with the textfield it stays constantly value given when calling with the sheet. I will attach a video here for you to see.
CreatingWorkout.swift :
struct CreatingWorkout: View {
#State var workoutTitle: String
#State var desc: String
#State var timetext: Int32
#State private var iconColor = Color.black
#State var displayWorkout: String = ""
#Environment(\.dismiss) var dismiss
#Environment(\.managedObjectContext) private var viewContext
private func saveWorkout() {
do {
let workout = Workout(context: viewContext)
workout.title = workoutTitle
workout.time = timetext
workout.icon = displayWorkout
workout.descriptionn = desc
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
CreateWorkout.swift :
import SwiftUI
struct CreateWorkoutView: View {
#State private var showingCreateWorkout = false
#State var timetext: Int32 = 0
var body: some View {
NavigationView {
VStack {
VStack(alignment: .center, spacing: 20) {
Text("Fitzy")
.font(.largeTitle)
.fontWeight(.bold)
Text("Create your first Workout")
.font(.title3)
.foregroundColor(.gray)
Button {
showingCreateWorkout.toggle()
} label: {
Image(systemName: "plus")
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(Color("AccentColor"))
.frame(width: 50, height: 50)
)
}.sheet(isPresented: $showingCreateWorkout) {
CreatingWorkout(workoutTitle: "", desc: "", timetext: timetext)
}
}
}.navigationTitle("Create Workout")
}
}
}
struct CreateWorkoutView_Previews: PreviewProvider {
static var previews: some View {
CreateWorkoutView()
}
}
https://drive.google.com/file/d/1qQDQmap5bMz9LxzibV98epHW5urqUtZ7/view?usp=sharing
as mentioned in the comments, you need a #State property to pass the value
that you type in your TextField to the sheet with CreatingWorkout. Try something like this:
struct CreatingWorkout: View {
#State var workoutTitle: String
#State var desc: String
#State var timetext: Int32
// ....
var body: some View {
Text("\(timetext)")
}
}
struct CreateWorkout: View {
#State var showingCreateWorkout = false
#State var timetext: Int32 = 0 // <-- here
var body: some View {
VStack {
TextField("type a number", value: $timetext, format: .number).border(.red) // <-- here
Button {
showingCreateWorkout.toggle()
} label: {
Image(systemName: "plus").font(.largeTitle).foregroundColor(.red).padding()
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(Color("AccentColor"))
.frame(width: 50, height: 50)
)
}.sheet(isPresented: $showingCreateWorkout) {
CreatingWorkout(workoutTitle: "", desc: "", timetext: timetext) // <-- here
}
}
}
}
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
}
}
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")
}
}
}
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)
}
}
When I'm trying to dismiss/cancel an Add Object Modal, it is creating an empty object instead of just cancelling.
I've tried deleteObject, context.rollback(), and a bunch of other random things. Would love some help and able to answer any questions.
I realize that this isn't an issue by putting the Cancel button in a NavigationBarItem but would like to be able to understand how to make an separate "cancel (or dismiss)" button.
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Game.gameName, ascending: true)]) var games: FetchedResults<Game>
#State private var showingAddGame = false
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.games, id: \.self) { games in
NavigationLink(destination: GameGoalsDetail(game: games)) {
VStack(alignment: .leading) {
Text(games.gameName ?? "Unknown Game")
Text(games.gameDescription ?? "Unknown Game Description")
}
}
}
.onDelete(perform: self.removeGames)
}
.navigationBarItems(leading:
HStack {
Button(action: {
self.showingAddGame.toggle()
}) {
Text("Add Game")
.padding(.top, 50)
.foregroundColor(Color.yellow)
}.sheet(isPresented: self.$showingAddGame) {
AddGameView().environment(\.managedObjectContext, self.moc)
}
Image("Game Goals App Logo")
.resizable()
.frame(width: 100, height: 100)
.padding(.leading, (geometry.size.width / 2.0) + -160)
.padding(.bottom, -50)
}, trailing:
EditButton()
.padding(.top, 50)
.foregroundColor(Color.yellow)
)
}
}
}
func removeGames(at offsets: IndexSet) {
for index in offsets {
let game = games[index]
moc.delete(game)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newGame = Game(context: context)
newGame.gameName = "Apex Legends"
newGame.gameDescription = "Maybe this will work"
return ContentView().environment(\.managedObjectContext, context)
}
}
AddGameView.swift
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
HStack {
Button("Add Game") {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
}
}
}
}
struct AddGameView_Previews: PreviewProvider {
static var previews: some View {
AddGameView()
}
}
I've searched all over so if there is something out there that I've missed as far as a stackoverflow post, please link it as I'd like to not only fix this but understand why.
Your Cancel button is not creating an empty object. The problem is that the whole row in your form that has Add and Cancel buttons is interactive and triggers actions of your both buttons.
I have found an answer here: https://stackoverflow.com/a/59402642/12315994
To keep your current layout you need to simply add one line to each of your buttons:
.buttonStyle(BorderlessButtonStyle())
After this, only taping on each button will trigger actions. Form's row with the buttons will not be clickable.
There are 2 other solutions. Both are to move your buttons out of Form.
Solution 1
is to move buttons to NavigationBarItems like this:
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
}
}
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
,
trailing:
Button(action: {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}) {
Text("Add Game")
}
)
}
}
}
Solution 2
Is to move buttons out of Form and move them to the bottom of the screen. Like this:
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
VStack {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
}
HStack {
Button(action: {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}) {
Text("Add Game")
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
}
}
}
}
Both options are better than your current layout from the UX point of view, because buttons are now in more standard locations. Especially version 1 is a more standard way of presenting buttons like this in iOS.