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

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.

Related

Refresh List After a New Entity is Added in Core Data for SwiftUI App

I am building a small app using SwiftUI and Core Data. I have a main view, which launches the sheet. The sheet allows me to add a new movie to the SQLite database through Core Data. But I am having a hard time to refresh the parent view once the sheet is dismissed.
ContentView
struct ContentView: View {
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(vm.movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.onAppear {
try? vm.populateMovies()
}.padding()
}
}
}
AddMovieView
struct AddMovieView: View {
#Environment(\.dismiss) private var dismiss
#StateObject private var vm = AddMovieViewModel()
var body: some View {
Form {
TextField("Title", text: $vm.title)
Button("Save") {
do {
try vm.saveMovie()
dismiss()
} catch {
print(error.localizedDescription)
}
}
}
}
}
Do I need to call vm.populateMovies() on the onDismiss function of the sheet from the ContentView?
You can use a #FetchRequest as follows:
struct ContentView: View {
#FetchRequest(sortDescriptors: []) var movies: FetchedResults<Movie>
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.padding()
}
}
}
You won't need a populateMovies() as the FetchRequest result is automatically populated.

Updating EnvironmentObject in child view causes view to pop

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
}
}

Swift - Update List from different View

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!")
}
}
}
}

How to push many views into a NavigationView in SwiftUI [duplicate]

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")
}
}
}

How to fill TextField with data from Core Data and update changes?

I am trying to learn how to Save, Edit and Delete data using Core Data. So far, with the help of this great community, I have managed to Save and Delete, but I don't know how to Edit and Update currently saved data.
Here is a simple example of an app I am working on. It is a list with items from Core Data. I am adding new list entries on a modal (AddItemView) and deleting them on EditItemView.
I would like to edit and update data as well on the AddItemView view.
I managed to pass data to hint of TextField, but what I wanted is:
pass the current data to text of TextField and make it editable
Save/update the data after tapping
Core Data has 1 Entity: ToDoItem. It has 1 Attribute: title (String). Codegen: Class Definition, Module: Current Product Module.
I have added some additional comments in the code.
ContentView
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: ToDoItem.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \ToDoItem.title, ascending: true)
]
) var toDoItems: FetchedResults<ToDoItem>
#State private var show_modal: Bool = false
var body: some View {
NavigationView {
List{
ForEach(toDoItems, id: \.self) {todoItem in
NavigationLink(destination: EditItemView(todoItem: todoItem)) {
Text(todoItem.title ?? "")
.font(.headline)
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing:
Button(action: {
self.show_modal = true
}) {
Text("Add")
}.sheet(isPresented: self.$show_modal) {
AddItemView().environment(\.managedObjectContext, self.managedObjectContext)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
AddItemView
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State private var title = ""
var body: some View {
NavigationView {
ScrollView {
TextField("to do item...", text: $title)
.font(Font.system(size: 30))
Spacer()
}
.padding()
.navigationBarTitle(Text("Add Item"))
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
},
trailing:
Button(action: {
let toDoItem = ToDoItem(context: self.managedObjectContext)
toDoItem.title = self.title
do {
try self.managedObjectContext.save()
}catch{
print(error)
}
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
}
)
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
EditItemView
import SwiftUI
struct EditItemView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var todoItem: ToDoItem
//I am only using this newTitle variable, because I don't know how to properly bind TextField to todoItem.title
#State private var newTitle = ""
var body: some View {
ScrollView {
TextField(todoItem.title != nil ? "\(todoItem.title!)" : "", text: $newTitle)
//TextField("to do...", text: (todoItem.title != nil ? "\(todoItem.title!)" : ""))
//ERROR
//I need something like the above, but this gives me an error: Cannot convert value of type 'String' to expected argument type 'Binding<String>'
}
.padding()
.navigationBarTitle(Text("Edit item"))
.navigationBarItems(
trailing:
Button(action: {
print("Delete")
self.managedObjectContext.delete(self.todoItem)
do {
try self.managedObjectContext.save()
self.presentationMode.wrappedValue.dismiss()
}catch{
print(error)
}
}) {
Text("Delete")
.foregroundColor(.red)
}
)
}
}
struct EditItemView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Test data
let todoItem = ToDoItem.init(context: context)
todoItem.title = "Title"
return EditItemView(todoItem: todoItem).environment(\.managedObjectContext, context)
}
}
I would do it in the following way
TextField("_here_is_label_name_", text: $newTitle, onCommit: {
self.todoItem.title = self.newTitle
try? self.managedObjectContext.save()
})
.onAppear {
self.newTitle = self.todoItem.title != nil ? "\(self.todoItem.title!)" : ""
}
.onDisappear {
self.todoItem.title = self.newTitle
try? self.managedObjectContext.save()
}
Update: added .onDisappear modifier; duplicated code can be extracted in dedicated private function to have good design.

Resources