Swift 2.0 .contextMenu Multiple Delete From Core Data - ios

First time post in here and new to coding... so I hope I am following proper protocol. I am putting together a view in Xcode 12.2 (SwiftUI 2) that outputs a list of data from Core Data and have a context menu to provide the user the option to edit, delete, and delete multiple. The context menu is working properly for edit and delete, however, I am facing a road block in how to implement the functionality to delete multiple list items. I am imagining the user would hard press one of the list items, the context menus pops open and if they press the "Delete Multiple" option, the view activates something similar to an edit mode that populates little circle on the left of each item which the user can select and delete more than one item at a time. I can see other article on how to do this, however, I cannot find guidance on how to implement this through Core Data. I have pasted my code below.
Please let me know if I am missing any other information that would make my question more clear.
I really appreciate the forums expertise and guidance.
Struct List : View {
#StateObject var appData = AppViewModel()
#Environment(\.managedObjectContext) var context
//Fetch Data...
#FetchRequest(entity: EntryData.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], animation: .spring()) var results : FetchedResults<EntryData>
var body : some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom), content: {
VStack{
ScrollView(.vertical, showsIndicators: false, content: {
LazyVStack(alignment: .leading, spacing: 20){
ForEach(results){task in
VStack(alignment: .leading, spacing: 5, content: {
Text(task.category ?? "")
.font(.title)
.fontWeight(.bold)
Text(task.date ?? Date(), style:. date)
.fontWeight(.bold)
Text("\(task.number.formattedCurrencyText)")
})
.padding(.horizontal, 14)
.padding(.top, 10)
.foregroundColor(Color("ColorTextList"))
.contextMenu{
Button(action: {appData.editItem(item: task)}, label: {
Text("Edit")
})
Button(action: {
context.delete(task)
try! context.save()
}, label: {
Text("Delete")
})
Button(action: {}, label: {
Text("Delete Mutiple")
})
}
}
}
})
}
VStack(){
VisualEffectView(effect: UIBlurEffect(style: .regular))
.frame(width: UIScreen.main.bounds.width, height: 50, alignment: .top)
.edgesIgnoringSafeArea(.all)
.background(Color.clear)
Spacer()
}
})
.background(Color.clear)
.sheet(isPresented: $appData.isNewData, content: {
AddDataView(appData: appData)
.environment(\.managedObjectContext, self.context)
})
}
}
Adding the viewModel of the app. How do I tap into into this and delete each of the attributes in a multi-list selection?
import SwiftUI
import CoreData
class AppViewModel : ObservableObject, Identifiable{
#Published var cateogry = ""
#Published var date = Date()
#Published var number : Double? = nil
#Published var notes = ""
#Published var id = UUID()
}
And adding the actual Core Data model screenshot.

You can implement a Selection Set for your List. This will contain all elements that are selected. Then you can dynamically show the contextMenu for delete or deleteAll based on the count of the set. Here is a full example with the implementation of deleteAll
struct SelectionDemo : View {
#State var demoData = ["Dave", "Tom", "Phillip", "Steve"]
#State var selected = Set<String>()
var body: some View {
HStack {
List(demoData, id: \.self, selection: $selected){ name in
Text(name)
.contextMenu {
Button(action: {
//Delete only one item
}){
Text("Delete")
}
if (selected.count > 1) {
Button(action: {
//Delete all
deleteAll()
})
{
Text("Delete all")
}
}
}
}.frame(width: 500, height: 460)
}
}
func deleteAll() {
for element in selected {
self.demoData.removeAll(where: {
$0 == element
})
}
}
}

Related

Two extra views put inside of a ForEach with a filtering search bar

So I have a ScrollView that contains a list of all the contacts imported from a user's phone. Above the ScrollView, I have a 'filter search bar' that has a binding that causes the list to show only contacts where the name contains the same string as the search bar filter. For some reason, the last two contacts in the list always pop up at the bottom of the list, no matter what the string is (even if it's a string not contained in any of the contact names on the phone). I tried deleting a contact and the problem persists, because the original contact was just replaced with the new second to last contact. Any help fixing this would be much appreciated!
struct SomeView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: CustomContact = []
var body: some View {
var filteredContactsCount = 0
if friendsFilterText.count != 0 {
for contact in appState.savedContacts {
if contact.name.lowercased().contains(friendsFilterText.lowercased()) {
filteredContactsCount += 1
}
}
} else {
filteredContactsCount = savedContacts.count
}
return HStack {
Image(systemName: "magnifyingglass")
ZStack {
HStack {
Text("Type a name...")
.opacity(friendsFilterText.count > 0 ? 0 : 1)
Spacer()
}
CocoaTextField("", text: $friendsFilterText)
.background(Color.clear)
}
Button(action: {
friendsFilterText = ""
}, label: {
Image(systemName: "multiply.circle.fill")
})
}.frame(height: 38)
HStack(spacing: 10) {
Text("Your contacts (\(filteredContactsCount))")
Spacer()
Button(action: {
fetchContacts()
}, label: {
Image(systemName: "square.and.arrow.down")
})
Button(action: {
// edit button action
}, label: {
Text("Edit")
})
}
ScrollView {
VStack {
ForEach(savedContacts, id: \.self.name) { contact in
if contact.name.lowercased().contains(friendsFilterText.lowercased()) || friendsFilterText.count == 0 {
Button(action: {
// contact button action
}, label: {
HStack(spacing: 20) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 41))
.frame(width: 41, height: 41)
VStack(alignment: .leading, spacing: 4) {
Text(contact.name)
Text(contact.phoneNumber)
}
Spacer()
}.frame(height: 67)
})
}
}
}
}
}
}
CustomContact is a custom struct with properties phoneNumber and name. I've attached images below of the issue I'm experiencing. I'm thinking MAYBE it's because there's something off timing-wise with the friendsFilterText and the ForEach rendering but I'm really not sure.
In the image set below, the 'Extra Contact 1' and 'Extra Contact 2' are ALWAYS rendered, unless I add a filter, then switch to a different view, then back to this view (which leads me to believe it's a timing thing again).
https://imgur.com/a/CJW2CUS
You should move the count calculation out of the view into a computed var.
And if CustomContact is your single contact struct, it should actually read #State var savedContacts: [CustomContact] = [] i.e. an array of CustomContact.
The rest worked fine with me, no extra contacts showing.
struct ContentView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: [CustomContact] = []
// computed var
var filteredContactsCount: Int {
if friendsFilterText.isEmpty { return savedContacts.count }
return savedContacts.filter({ $0.name.lowercased().contains(friendsFilterText.lowercased()) }).count
}
var body: some View {
...

SwiftUI: Slider in List/ForEach behaves strangely

It's very hard to explain without a recording from a second device that I don't have, but when I try to slide my slider, it will stop when my finger is definitely still moving.
I have my code posted below. I'd be happy to answer any questions and explain whatever. I'm sure it's something really simple that I should know. Any help would be very much appreciated, thanks!
import SwiftUI
class SettingsViewModel: ObservableObject {
#Published var selectedTips = [
10.0,
15.0,
18.0,
20.0,
25.0
]
func addTip() {
selectedTips.append(0.0)
selectedTips.sort()
}
func removeTip(index: Int) {
selectedTips.remove(at: index)
selectedTips = selectedTips.compactMap{ $0 }
}
}
struct SettingsTipsView: View {
#StateObject var model = SettingsViewModel()
var body: some View {
List {
HStack {
Text("Edit Suggested Tips")
.font(.title2)
.fontWeight(.semibold)
Spacer()
if(model.selectedTips.count < 5) {
Button(action: { model.addTip() }, label: {
Image(systemName: "plus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
ForEach(model.selectedTips, id: \.self) { tip in
let i = model.selectedTips.firstIndex(of: tip)!
//If I don't have this debug line here then the LAST slider in the list tries to force the value to 1 constantly, even if I remove the last one, the new last slider does the same. It's from a separate file but it's pretty much the same as the array above. An explanation would be great.
Text("\(CalculatorViewModel.suggestedTips[i])")
HStack {
Text("\(tip, specifier: "%.0f")%")
Slider(value: $model.selectedTips[i], in: 1...99, label: { Text("Label") })
if(model.selectedTips.count > 1) {
Button(action: { model.removeTip(index: i) }, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}
Using id: \.self within a List or ForEach is a dangerous idea in SwiftUI. The system uses it to identify what it expects to be unique elements. But, as soon as you move the slider, you have a change of ending up with a tip value that is equal to another value in the list. Then, SwiftUI gets confused about which element is which.
To fix this, you can use items with truly unique IDs. You should also try to avoid using indexes to refer to certain items in the list. I've used list bindings to avoid that issue.
struct Tip : Identifiable {
var id = UUID()
var tip : Double
}
class SettingsViewModel: ObservableObject {
#Published var selectedTips : [Tip] = [
.init(tip:10.0),
.init(tip:15.0),
.init(tip:18.0),
.init(tip:20.0),
.init(tip:25.0)
]
func addTip() {
selectedTips.append(.init(tip:0.0))
selectedTips = selectedTips.sorted(by: { a, b in
a.tip < b.tip
})
}
func removeTip(id: UUID) {
selectedTips = selectedTips.filter { $0.id != id }
}
}
struct SettingsTipsView: View {
#StateObject var model = SettingsViewModel()
var body: some View {
List {
HStack {
Text("Edit Suggested Tips")
.font(.title2)
.fontWeight(.semibold)
Spacer()
if(model.selectedTips.count < 5) {
Button(action: { model.addTip() }, label: {
Image(systemName: "plus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
ForEach($model.selectedTips, id: \.id) { $tip in
HStack {
Text("\(tip.tip, specifier: "%.0f")%")
.frame(width: 50) //Otherwise, the width changes while moving the slider. You could get fancier and try to use alignment guides for a more robust solution
Slider(value: $tip.tip, in: 1...99, label: { Text("Label") })
if(model.selectedTips.count > 1) {
Button(action: { model.removeTip(id: tip.id) }, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}

How do I navigate to another SwiftUI View when an Environment Object update is causing the same view to reload

What I'm trying to achieve: I'm a new SwiftUI developer. I'm trying to build a simple Address Book app. I have three views:
ContentView - The main view which contains all contacts in a List View with an Add Contact ('+') and Edit button at the top of the Navigation View
AddContact View - which has a "Name" and "Email" text field and a "Submit" button
DisplayContactDetails View - not relevant to this question.
I've created an Environment Object "myContacts" which is an array of "Contact" objects and passed it in the ContentView to keep track of all contacts in the Address Book
When the user navigates to AddContact View, adds a name and email and submits, I'd like the Environment Object "myContacts" to be updated and for the user to be navigated back to ContentView so they can see the Address Book with the new contact included.
Problem:
When the user presses "Submit" on AddContact View, it correctly invokes a navigation link I've created to send the user back to ContentView. But because the Environment Object "myContacts" has also been updated by submit, it immediately navigates back from ContentView to AddContact View again. So it appears to be executing the Navigation Link first but then reloading AddContact View due to the refresh of myContacts.
Code - Content view:
struct ContentView: View {
#EnvironmentObject var myContacts: Contacts
#State var isAddButtonPressed: Bool = false
var body: some View {
NavigationView{
List {
ForEach(myContacts.contacts) { item in
NavigationLink(
//Display items and send user to DisplayContactDetails
})
}
}
.navigationBarTitle("Address Book")
.toolbar {
ToolbarItem(placement: .navigationBarLeading){
Button(action: {
isAddButtonPressed.toggle()
}, label: {
NavigationLink(
destination: AddContactView(),
isActive: $isAddButtonPressed,
label: {
Image(systemName: "plus")
})
})
}
ToolbarItem(placement: .navigationBarTrailing){
EditButton()
}
}
}
}
}
Code - AddContactView
struct AddContactView: View {
#State var name: String = ""
#State var email: String = ""
#State var isButtonPressed: Bool = false
#EnvironmentObject var myContacts: Contacts
var body: some View {
VStack{
HStack{
Text("Name:")
TextField("Enter name", text: $name)
}
.padding(.bottom, 50)
HStack{
Text("Email:")
TextField("Enter email", text: $email)
}
.padding(.bottom, 50)
Button("Submit") {
let contactToAdd = Contact(name: name, email: email)
//Add is a simple function - all it does is append an item to the myContacts array using the .append method
myContacts.add(contact: contactToAdd)
isButtonPressed = true
}
.frame(width: 80, height: 30, alignment:.center)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
NavigationLink(destination: ContentView().navigationBarHidden(true),
isActive: $isButtonPressed,
label: {
EmptyView()
}).hidden()
}.padding()
}
}
What I've tried
If I comment out the the .add method and don't update the environment object, then the navigation back to ContentView works as expected. So I know that specifically is the cause of the problem.
I've tried adding a .onTapGesture modifier to the Button and invoking .add there.
I've tried adding a .onDisappear modifier to the entire view and invoking .add there.
--
Any help or clarity on resolving this would be much appreciated
Edit: Screen Recording - trying solution based on first comment:
What happens when I try the solution
Odd behaviour: The first attempt at adding a contact auto-routes back to AddContactView, producing the same error. But if I try it a second time then it routes correctly to ContactView.
Edit update. This is the code I used to test my answer:
import SwiftUI
#main
struct TestApp: App {
#StateObject var myContacts = Contacts()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(myContacts)
}
}
}
struct Contact: Identifiable {
var id = UUID()
var name: String = ""
var email: String = ""
}
class Contacts: ObservableObject {
#Published var contacts: [Contact] = [Contact(name: "name1", email: "email1"), Contact(name: "name2", email: "email2")]
func add(contact: Contact) {
contacts.append(contact)
}
}
struct AddContactView: View {
#Environment(\.presentationMode) private var presentationMode
#EnvironmentObject var myContacts: Contacts
#State var name: String = ""
#State var email: String = ""
var body: some View {
VStack{
HStack{
Text("Name:")
TextField("Enter name", text: $name)
}
.padding(.bottom, 50)
HStack{
Text("Email:")
TextField("Enter email", text: $email)
}
.padding(.bottom, 50)
Button("Submit") {
let contactToAdd = Contact(name: name, email: email)
myContacts.add(contact: contactToAdd)
presentationMode.wrappedValue.dismiss()
}
.frame(width: 80, height: 30, alignment:.center)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}.padding()
}
}
struct ContentView: View {
#EnvironmentObject var myContacts: Contacts
#State var isAddButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
ForEach(myContacts.contacts) { item in
NavigationLink(destination: AddContactView()) {
Text(item.name)
}
}
}
.navigationBarTitle("Address Book")
.toolbar {
ToolbarItem(placement: .navigationBarLeading){
Button(action: {
isAddButtonPressed.toggle()
}, label: {
NavigationLink(
destination: AddContactView(),
isActive: $isAddButtonPressed,
label: {
Image(systemName: "plus")
})
})
}
ToolbarItem(placement: .navigationBarTrailing){
EditButton()
}
}
}
}
}

A View with an ObservedObject and nothing will allow me to edit it

https://github.com/ryanpeach/RoutinesAppiOS
A Picker will not move, the Edit Button won't click, and a Text Field won't enter more than 1 character. Return on keyboard doesn't work. Most of these are tied to bindings to the ObservedObject and should be able to directly edit it. I believe there is a common cause with CoreData hanging on object updates. The done button on the final player view hangs, but the skip button does not! That means it only happens when TaskData is updated. If you delete an Alarm in certain situations the app crashes too.
Here's a video of the app behavior so far. In the view right before the end nothing will click. At the final view it hangs when you click the checkmark.
https://www.icloud.com/photos/#0HMiYqZ08ZYoFu5BEQXET4gRA
I am seeking some tips on how to debug this error. I can't put a breakpoint on the picker "when something changes" and I similarly cant do it on the Text Field. Why would a text field only take one character and then stop? Why would the edit button not work? This is the only view in the app where these sub-views don't work, the rest of the app works fine.
Some relevant background information:
I'm using coredata. There are 3 classes: AlarmData for the Routines Page, which has a one2many relationship to TaskData for the TaskList Page, which has a one2many relationship to SubTaskData for the TaskPlayerView and TaskEditor pages, the ones I'm having trouble with.
No further relationships.
I'm doing a fetchrequest at the root view and then using #ObservedObject the rest of the way down the view hierarchy. I'm using mostly isActive and tag:selection NavigationLinks.
The relevant file:
struct TaskEditorView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var taskData: TaskData
#State var newSubTask: String = ""
var subTaskDataList: [SubTaskData] {
var out: [SubTaskData] = []
for sub_td in self.taskData.subTaskDataList {
out.append(sub_td)
}
return out
}
var body: some View {
VStack {
TitleTextField(text: self.$taskData.name)
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
TimePickerRelativeView(time: self.$taskData.duration)
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
HStack {
Spacer().frame(width: DEFAULT_LEFT_ALIGN_SPACE, height: DEFAULT_HEIGHT_SPACING)
ReturnTextField(
label: "New Subtask",
text: self.$newSubTask,
onCommit: self.addSubTask
)
Button(action: {
self.addSubTask()
}) {
Image(systemName: "plus")
.frame(width: DEFAULT_LEFT_ALIGN_SPACE, height: 30)
}
Spacer().frame(width: DEFAULT_HEIGHT_SPACING)
}
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
Text("Subtasks:")
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
List {
ForEach(self.subTaskDataList, id: \.id) { sub_td in
Text(sub_td.name)
}
.onDelete(perform: self.delete)
.onMove(perform: self.move)
}
}
.navigationBarItems(trailing: EditButton())
}
...
}
It also doesn't like to be edited here (see FLAG comment):
struct TaskPlayerView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var taskDataList: [TaskData] {
return self.alarmData.taskDataList
}
var taskData: TaskData {
return self.taskDataList[self.taskIdx]
}
var subTaskDataList: [SubTaskData] {
var out: [SubTaskData] = []
for sub_td in self.taskData.subTaskDataList {
out.append(sub_td)
}
return out
}
#ObservedObject var alarmData: AlarmData
#State var taskIdx: Int = 0
// For the timer
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var isPlay: Bool = true
#State var done: Bool = false
#State var startTime: Date?
#State var lastTime: Date = Date()
#State var durationBeforePause: TimeInterval = 0
#State var durationSoFar: TimeInterval = 0
var body: some View {
VStack {
if !self.done {
...
HStack {
Spacer()
Button(action: {
withAnimation {
if self.subTaskDataList.count == 0 || self.subTaskDataList.allSatisfy({$0.done}) {
// FLAG: It fails here where setting task data
self.taskData.lastDuration_ = self.durationSoFar
self.taskData.done = true
self.taskData.lastEdited = Date()
self.next()
}
}
}) {
if self.subTaskDataList.count == 0 || self.subTaskDataList.allSatisfy({$0.done}) {
Image(systemName: "checkmark.circle")
.resizable()
.frame(width: 100.0, height: 100.0)
} else {
Image(systemName: "checkmark.circle")
.resizable()
.frame(width: 100.0, height: 100.0)
.foregroundColor(Color.gray)
}
}
Spacer()
}
...
}
...
}
...
}

Core Data Object still saved on dismiss/cancel in presentationMode SwiftUI

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.

Resources