SwiftUI: Cannot Delete List Item If Just Viewed - ios

I have a very odd issue. I have a list app that crashes when I delete a list item that I just viewed. I can delete an item that is different than the one I just viewed without the crash. The crash error is:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/XXX/Documents/Xcode Projects Playground/Test-Camera-CloudKit/Test-Camera-CloudKit/DetailView.swift, line 20
Line 20 of the DetailView.swift file is the line that displays the image/photo [Image(uiImage: UIImage(data: myItem.photo!) ?? UIImage(named: "gray_icon")!)]. Below are the files from my stripped down app to try to run this issue to ground. I am using CoreData and CloudKit.
ContentView.swift:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]) var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(myItem: item)) {
HStack {
Text(item.name ?? "Unknown name")
}
}
}.onDelete(perform: delete)
}
.navigationBarTitle("Items")
.navigationBarItems(trailing:
Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.moc)
}
}
}
func delete(at offsets: IndexSet) {
for index in offsets {
let item = items[index]
moc.delete(item)
}
do {
try moc.save()
} catch {
print("Error deleting objects")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddItemView.swift:
import SwiftUI
struct AddItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#State private var image : Data = .init(count: 0)
#State private var name = ""
#State private var show = false
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $name)
}
Section {
VStack {
Button(action: {self.show = true}) {
HStack {
Image(systemName: "camera")
}
}
Image(uiImage: UIImage(data: self.image) ?? UIImage(named: "gray_icon")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300, alignment: Alignment.center)
.clipped()
}
.sheet(isPresented: self.$show, content: {
ImagePicker(show: self.$show, image: self.$image)
})
}
Section {
Button("Save") {
let newItem = Item(context: self.moc)
newItem.name = self.name
newItem.photo = self.image
try? self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var myItem: Item
var body: some View {
VStack {
Text(myItem.name ?? "Unknown")
Image(uiImage: UIImage(data: myItem.photo!) ?? UIImage(named: "gray_icon")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300, alignment: Alignment.center)
.clipped()
}
.navigationBarTitle(Text(myItem.name ?? "Unknown"), displayMode: .inline)
}
}
struct DetailView_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let item = Item(context: moc)
item.name = "Test"
return NavigationView {
DetailView(myItem: item)
}
}
}
ImagePicker.swift
import SwiftUI
import Combine
struct ImagePicker : UIViewControllerRepresentable {
#Binding var show : Bool
#Binding var image : Data
func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(child1: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var child : ImagePicker
init(child1: ImagePicker) {
child = child1
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.child.show.toggle()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let image = info[.originalImage]as! UIImage
let data = image.jpegData(compressionQuality: 0.45)
self.child.image = data!
self.child.show.toggle()
}
}
}
I am really struggling with how does the app have an error in a view that is not being shown when an item is deleted. The list items does get deleted and removed from CloudKit. The delete operation works. The crash and error happens whether the coredata attribute for the photo has a photo or not. In other words, it has the error even when the item does have a photo and is not nil. Where am I going wrong? How do I get it to allow me to delete an item that I just viewed in the DetailView without a nil error? Any help is greatly appreciated. Thanks.

Related

Interacting with a SwiftUI View via ARKit blendshapes

I have a strange bug I can't resolve.
I'm trying to create an app where the user can interact with blend shapes instead of buttons.
I have my ContentView where I define an array of animals of type Card.
What the app do is that when the user blinks, a sheetView is shown. In this sheetView it's displayed a CardView with a random animal taken from the array. The user can play the sound of that specific animal with a button (for now).
The user can also close the sheetView opening his mouth.
I'll show my code
ContentView.swift
import SwiftUI
import AVKit
class ViewModel: ObservableObject {
#Published var changeAnimal = false
#Published var realMouthOpen = false
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
let animals: [Card] = [Card(image: "monkey", heading: "monkey", tag: 1, callSound: "Monkey", animal: .monkey), Card(image: "dog", heading: "dog", tag: 2, callSound: "dog", animal: .dog), Card(image: "chick", heading: "chick", tag: 3, callSound: "chick", animal: .chick)]
var body: some View {
ZStack {
SwiftUIViewController(viewModel: viewModel) // wrapper controller uikit
CardViewPreviewScroll(card: Card(image: "unknown", heading: "random animal", tag: 0, callSound: "", animal: .unknown))
.sheet(isPresented: $viewModel.changeAnimal) { // BUG: the view is changed 2 times
changeAnimal() // l'animale corrente, nel momento in cui cambia, la view viene aggiornata
}
}
}
//MARK: Functions
func changeAnimal() -> (CardView?) {
let animal = animals.randomElement()!
return (CardView(card: animal))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Card.swift
import Foundation
import SwiftUI
enum TypeAnimal: Identifiable {
case monkey, dog, chick, unknown
var id: Int {
hashValue
}
}
struct Card: Hashable{
var image: String
var heading: String
var id = UUID()
var tag: Int
var callSound: String
var animal: TypeAnimal
static let example = Card(image: "chick", heading: "chick", tag: 0, callSound: "chick", animal: .chick)
}
CardView.swift
import SwiftUI
import AVKit
struct CardView: View {
let card: Card
#State var audioPlayer: AVAudioPlayer!
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(.green)
.shadow(radius: 3)
VStack {
Image(card.image)
.resizable()
.aspectRatio(contentMode: .fit)
VStack(alignment: .leading) {
Text(card.heading)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
HStack {
Spacer()
Button(action: {
self.audioPlayer.play()
}) {
Image(systemName: "play.circle.fill").resizable()
.frame(width: 50, height: 50)
.aspectRatio(contentMode: .fit)
}
Spacer()
Button(action: {
self.audioPlayer.pause()
}) {
Image(systemName: "pause.circle.fill").resizable()
.frame(width: 50, height: 50)
.aspectRatio(contentMode: .fit)
}
Spacer()
}
}
.layoutPriority(100)
.padding()
}
}
.onAppear {
let sound = Bundle.main.path(forResource: card.callSound, ofType: "mp3")
self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound!))
}
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(card: Card.example)
}
}
And this is my ViewController, wrapped in UIViewControllerRepresentable.
This ViewController conforms to the ARSessionDelegate protocol so that I can change the #Published property defined in my ObservableObject ViewModel and this means changing the $viewModel.changeAnimal binding in .sheet.
The fact is, as the CardView in the sheetView change whenever a blendshape of type browInnerUp is detected, I tried to catch this value only 1 time without finding a solution.
The best thing I managed to do is that, when the sheetView is displayed, an instance of CardView appears and this one is immediately replaced with another one.
I tried with DispatchQueue, timers and other things about treads in order to catch only a single value of the parameter of the blendshape in question but I failed
ViewController.swift
import UIKit
import SwiftUI
import ARKit
class ViewController: UIViewController, ARSessionDelegate {
var viewModel: ViewModel?
var session: ARSession!
override func viewDidLoad() {
super.viewDidLoad()
session = ARSession()
session.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard ARFaceTrackingConfiguration.isSupported else {print("IPhone X required"); return}
let configuration = ARFaceTrackingConfiguration()
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
if let faceAnchor = anchors.first as? ARFaceAnchor {
update(withFaceAnchor: faceAnchor)
}
}
func update(withFaceAnchor faceAnchor: ARFaceAnchor) {
let bledShapes:[ARFaceAnchor.BlendShapeLocation:Any] = faceAnchor.blendShapes
guard let jawOpen = bledShapes[.jawOpen] as? Float else { return }
guard let browInnerUp = bledShapes[.browInnerUp] as? Float else { return }
if browInnerUp > 0.85 {
self.session.pause()
self.viewModel?.changeAnimal = true
// print("eyeBlinkLeft: \(eyeBlinkLeft)") // right eye
}
if jawOpen > 0.85 {
self.session.pause()
self.viewModel?.changeAnimal = false
// print("eyeBlinkRight: \(jawOpen)") // left eye
}
}
}
struct SwiftUIViewController: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> ViewController{
let controller = ViewController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
}
The question is: is there a way to fix this bug or I just messed up?

Adding multiple images into a view from photo library - SwiftUI

I want to add images from phone's photo library into a collage layout that I made. First I made the collage layout as a separate view in SwiftUI called CollageLayoutOne.
import SwiftUI
struct CollageLayoutOne: View {
var uiImageOne: UIImage
var uiImageTwo: UIImage
var uiImageThree: UIImage
var body: some View {
Rectangle()
.fill(Color.gray)
.aspectRatio(1.0, contentMode: .fit)
.overlay {
HStack {
Rectangle()
.fill(Color.gray)
.overlay {
Image(uiImage: uiImageOne)
.resizable()
.aspectRatio(contentMode: .fill)
}
.clipped()
VStack {
Rectangle()
.fill(Color.gray)
.overlay {
Image(uiImage: uiImageTwo)
.resizable()
.aspectRatio(contentMode: .fill)
}
.clipped()
Rectangle()
.fill(Color.gray)
.overlay {
Image(uiImage: uiImageThree)
.resizable()
.aspectRatio(contentMode: .fill)
}
.clipped()
}
}
.padding()
}
}
}
Then I have a separate view (PageView) where I want to show the CollageLayoutOne view and it also hosts the button to get to the image library.
struct PageView: View {
#State private var photoPickerIsPresented = false
#State var pickerResult: [UIImage] = []
var body: some View {
NavigationView {
ScrollView {
if pickerResult.isEmpty {
} else {
CollageLayoutOne(uiImageOne: pickerResult[0], uiImageTwo: pickerResult[1], uiImageThree: pickerResult[2])
}
}
.edgesIgnoringSafeArea(.bottom)
.navigationBarTitle("Select Photo", displayMode: .inline)
.navigationBarItems(trailing: selectPhotoButton)
.sheet(isPresented: $photoPickerIsPresented) {
PhotoPicker(pickerResult: $pickerResult,
isPresented: $photoPickerIsPresented)
}
}
}
#ViewBuilder
private var selectPhotoButton: some View {
Button(action: {
photoPickerIsPresented = true
}, label: {
Label("Select", systemImage: "photo")
})
}
}
My problem is that for some unknown reason the app crashes every time I select the photos and try to add them. If I do pickerResult[0] for all three it works just fine, but displays only the first selected photo on all 3 spots. Also if I start with all 3 as pickerResult[0] and then change them to [0], [1], [2] while the preview is running it doesn't crash and displays correctly.
I'm just starting with Swift and SwiftUI, so excuse me if it's some elementary mistake. Below I am also adding my code for PhotoPicker that I got from an article I found.
PhotoPicker.swift:
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
#Binding var pickerResult: [UIImage]
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> some UIViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = .images // filter only to images
if #available(iOS 15, *) {
configuration.selection = .ordered //number selection
}
configuration.selectionLimit = 3 // ignore limit
let photoPickerViewController = PHPickerViewController(configuration: configuration)
photoPickerViewController.delegate = context.coordinator
return photoPickerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
private let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.pickerResult.removeAll()
for image in results {
if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
image.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] newImage, error in
if let error = error {
print("Can't load image \(error.localizedDescription)")
} else if let image = newImage as? UIImage {
self?.parent.pickerResult.append(image)
}
}
} else {
print("Can't load asset")
}
}
parent.isPresented = false
}
}
}
image.itemProvider.loadObject is an asynchronous function, and it loads images one by one.
When the first image is processed, you add it to pickerResult and your pickerResult.isEmpty check becomes false, but your array contains only one item so far.
The safe thing to do here is to check the count:
if pickerResult.count == 3 {
CollageLayoutOne(uiImageOne: pickerResult[0], uiImageTwo: pickerResult[1], uiImageThree: pickerResult[2])
}
Also, in such cases, it's a good idea to wait until all asynchronous requests are complete before updating the UI, for example, like this:
var processedResults = [UIImage]()
var leftToLoad = results.count
let checkFinished = { [weak self] in
leftToLoad -= 1
if leftToLoad == 0 {
self?.parent.pickerResult = processedResults
self?.parent.isPresented = false
}
}
for image in results {
if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
image.itemProvider.loadObject(ofClass: UIImage.self) { newImage, error in
if let error = error {
print("Can't load image \(error.localizedDescription)")
} else if let image = newImage as? UIImage {
processedResults.append(image)
}
checkFinished()
}
} else {
print("Can't load asset")
checkFinished()
}
}

NavigationView with Loading More in a List Crashes with Long Press only on iPad

I am creating a list that loads data when the user reaches the bottom of the list. I can crash the app when I load more elements and long-press an element within the list. The view is wrapped in a NavigationView and a NavigationLink. When the app crashes, you get EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) with the thread 1 specialized saying "RandomAccessCollection<>.index(_:offsetBy:))". Looking into the EXC_BAD_INSTRUCTION I thought it could be force unwrapping, but I don't see anywhere in the code that could cause this issue.
The issue only occurs on an iPad and happens randomly. With WWDC being yesterday, I thought this would have been fixed, so we downloaded the beta for Xcode 12, and this error still occurs.
Here is the full code:
import UIKit
import SwiftUI
import Combine
struct ContentView: View {
var body: some View {
RepositoriesListContainer(viewModel: RepositoriesViewModel())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
enum GithubAPI {
static let pageSize = 10
static func searchRepos(query: String, page: Int) -> AnyPublisher<[Repository], Error> {
let url = URL(string: "https://api.github.com/search/repositories?q=\(query)&sort=stars&per_page=\(Self.pageSize)&page=\(page)")!
return URLSession.shared
.dataTaskPublisher(for: url) // 1.
.tryMap { try JSONDecoder().decode(GithubSearchResult<Repository>.self, from: $0.data).items } // 2.
.receive(on: DispatchQueue.main) // 3.
.eraseToAnyPublisher()
}
}
struct GithubSearchResult<T: Codable>: Codable {
let items: [T]
}
struct Repository: Codable, Identifiable, Equatable {
let id: Int
let name: String
let description: String?
let stargazers_count: Int
}
class RepositoriesViewModel: ObservableObject {
#Published private(set) var state = State()
private var subscriptions = Set<AnyCancellable>()
// 2.
func fetchNextPageIfPossible() {
guard state.canLoadNextPage else { return }
GithubAPI.searchRepos(query: "swift", page: state.page)
.sink(receiveCompletion: onReceive,
receiveValue: onReceive)
.store(in: &subscriptions)
}
private func onReceive(_ completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure:
state.canLoadNextPage = false
}
}
private func onReceive(_ batch: [Repository]) {
state.repos += batch
state.page += 1
state.canLoadNextPage = batch.count == GithubAPI.pageSize
}
// 3.
struct State {
var repos: [Repository] = []
var page: Int = 1
var canLoadNextPage = true
}
}
struct RepositoriesListContainer: View {
#ObservedObject var viewModel: RepositoriesViewModel
var body: some View {
RepositoriesList(
repos: viewModel.state.repos,
isLoading: viewModel.state.canLoadNextPage,
onScrolledAtBottom: viewModel.fetchNextPageIfPossible
)
.onAppear(perform: viewModel.fetchNextPageIfPossible)
}
}
struct RepositoriesList: View {
// 1.
let repos: [Repository]
let isLoading: Bool
let onScrolledAtBottom: () -> Void // 2.
var body: some View {
NavigationView {
List {
reposList
if isLoading {
loadingIndicator
}
}
}
// .OnlyStackNavigationView()
}
private var reposList: some View {
ForEach(repos) { repo in
// 1.
RepositoryRow(repo: repo).onAppear {
// 2.
if self.repos.last == repo {
self.onScrolledAtBottom()
}
}
.onTapGesture {
print("TAP")
}
.onLongPressGesture {
print("LONG PRESS")
}
}
}
private var loadingIndicator: some View {
Spinner(style: .medium)
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}
}
struct RepositoryRow: View {
let repo: Repository
var body: some View {
NavigationLink(destination: LandmarkDetail()){VStack {
Text(repo.name).font(.title)
Text("⭐️ \(repo.stargazers_count)")
repo.description.map(Text.init)?.font(.body)
}}
}
}
struct Spinner: UIViewRepresentable {
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(style: style)
spinner.hidesWhenStopped = true
spinner.startAnimating()
return spinner
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {}
}
struct LandmarkDetail: View {
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
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.

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