SwiftUI + Core Data - updating an object (Detail -> DetailEdit) - core-data

Goal: update a core data object with SwiftUI: DetailView -> EditDetail -> DetailView (updated).
Problem: code bellow works, but creates a new object, instead of updating existing one.
import SwiftUI
struct DetailView: View {
var order = Order()
#State var showOrderEdit = false
var body: some View {
Form{
Text(order.tableNumber)
Text(order.pizzaType)
}
.navigationTitle(order.pizzaType)
.toolbar {
ToolbarItem(placement: .primaryAction) {
//edit button
Button(action: {
showOrderEdit = true
}, label: {
Text("Edit")
})
.sheet(isPresented: $showOrderEdit) {
OrderEdit(order: order)
}
}
}
}
}
import SwiftUI
struct DetailEdit: View {
#State var tableNumber = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
var order = Order()
var body: some View {
NavigationView {
Form {
TextField("table number", text: $tableNumber)
//update button
Button(action: {
updateOrder(order: order)
}) {
Text("Update")
.foregroundColor(.blue)
}
}
//passing data item detail -> item edit
.onAppear {
self.tableNumber = self.order.tableNumber
}
.navigationTitle("Edit Order")
}
}
func updateOrder(order: Order) {
let newtableNumber = tableNumber
viewContext.performAndWait {
order.tableNumber = newtableNumber
try? viewContext.save()
}
}

You create new Order object in each view, so it is stored as new one into database. Instead you need to inject CoreData object from parent view (which shows DetailView) as observed object,
struct DetailView: View {
#ObservedObject var order: Order // << here !!
// .. other code
and
struct DetailEdit: View {
#State var tableNumber = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
#ObservedObject var order: Order // << here !!
// ... other code
in such approach you will work with same instance of Order in both views and they will be updated because observe that instance for modifications.

Related

SwiftUI show loading view while core data is being loaded

How I can show loading view while core data is being loaded?.
Currently my app's core data store some many images in Binary Data. So when I switch to another tab showing data stored in core data, app lags 1.5 seconds.
So here are two things I have tried:
first I tried to minimize amount of data being loaded from core data using downsample function:
func downsample(imageAt imageURL: Data, to pointSize: CGSize, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
// Create an CGImageSource that represent an image
//CGImageSourceCreateWithData(_ data: CFData, _ options: CFDictionary?)
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(imageURL as CFData, imageSourceOptions) else {
return nil
}
// Calculate the desired dimension
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
// Perform downsampling
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
// Return the downsampled image as UIImage
return UIImage(cgImage: downsampledImage)
}
let small = downsample(imageAt: data, to: size)
Image(uiImage: small!)
But there were no difference in lagging time.
So i tried this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ToDoItem2.createdAt, ascending: false)])
var toDoItems: FetchedResults<ToDoItem>
var body: some View {
VStack{
if toDoItems.isEmpty {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
}
}
Tried to detect no loaded state as toDoItems.isEmpty but it doesn't work
Would be there anyway to show loading view while core data is being loaded?
Thanks
you could try using NSAsynchronousFetchRequest, something like this approach (does not have to be exactly like this (untested) code):
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var toDoItems: [ToDoItem] = []
#State var isLoading = true
var body: some View {
VStack {
if isLoading {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
.onAppear {
isLoading = true
let fetchRequest: NSFetchRequest<ToDoItem> = ToDoItem.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ToDoItem.createdAt, ascending: false)]
let asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { fetchResult -> Void in
if let resutls = fetchResult.finalResult {
self.toDoItems = resutls
}
self.isLoading = false
}
do {
_ = try viewContext.execute(asyncFetchRequest)
} catch {
print("error: \(error)")
}
}
}
}

Refresh a view when the children of an object are changed in SwiftUI

I am working on a CoreData application with two entities MyList and MyListItem. MyList can have many MyListItem (one to many). When the app is launched, I can see all the lists. I can tap on a list to go to the list items. On that screen, I tap a button to add an item to the selected list. After, adding the item when I go back to the all lists screen I cannot see the number of items reflected in the count. The reason is that MyListsView is not rendered again since the number of lists have not changed.
The complete code is shown below:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
#Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
#Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
Inside ContentView there is a view called "MyListsView". That view is not rendered when the items are added. Since, according to that view nothing changed since the number of lists are still the same.
How do you solve this problem?
UPDATE:
What happens if I add one more level of views like for ListCellView as shown below:
struct MyListCellView: View {
#StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
#MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
#Published var name: String = ""
#Published var items: [MyListItem] = []
}
struct MyListsView: View {
#StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
Now the count is again not being updated.
Your ViewModel is an ObserveableObject, but you are not observing it in MyListsView. When you initialized MyListsView, you set a let constant. Of course that won't update. Do this instead:
struct MyListsView: View {
#ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
Now the #Published in ViewModel will cause MyListView to change when it does, and that includes adding a related entity.
We don't need MVVM in SwiftUI, the View data structs already fill that role and property wrappers make them behave like objects giving best of both worlds. In your case use the #FetchRequest property wrapper for the list and #ObservedObject for the detail and body will be called on any changes to the model data. Examine the code in the app template in Xcode with Core Data checked. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")

Convert address to coordinates using MKLocalSearchCompleter and CoreLocation

I have tried to make an app with a textfield to let user input a location, using MKLocalSearchCompleter to complete the searching. After that i would like to get the coordinate and display on the MapKit. However, I failed to get the coordinate using the Geocoder.
class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
#Published var searchQuery = ""
var completer: MKLocalSearchCompleter
#Published var completions: [MKLocalSearchCompletion] = []
var cancellable: AnyCancellable?
override init() {
completer = MKLocalSearchCompleter()
super.init()
cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer)
completer.delegate = self
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
self.completions = completer.results
}
}
The location manager as follows:
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private func lookupLocation() {
guard let location = self.location else { return }
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
} else {
self.placemark = nil
}
})
}
// !!! This is the function I would like to use to get the Coordinate from the address obtained from LocationSearchService
func getCoordinate(address: String) {
geocoder.geocodeAddressString(address, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
self.location = self.placemark?.location
} else {
self.placemark = nil
self.location = nil
}
})
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return } //.first or .last?
self.location = location
self.lookupLocation()
}
}
Content View like this:
struct ContentView: View {
#State private var location: String = ""
#ObservedObject var lm = LocationManager()
private let completer = MKLocalSearchCompleter()
#ObservedObject var locationSearchService = LocationSearchService()
var body: some View {
NavigationView {
VStack {
AddressSearchBar(text: $locationSearchService.searchQuery)
List(locationSearchService.completions, id: \.self) { completion in
VStack(alignment: .leading) {
Text(completion.title)
// Error here, I cannot translate the address to location
//Text(lm.getCoordinate(address: completion.title))
}
}.navigationTitle("Search Location")
}
}
A few issues here:
I would like to convert the user selected item (which I failed to implement here) to the address (completion.title) -- i.e., need to get user selection on the suggested item.
I would like to convert the address found in the suggestion to a coordinate, so that I can mark on MapView.

How to show a value from CoreData Entity as a state variable after editing

I'm currently developing an application using CoreData in SwiftUI.
I want to show a value from CoreData Entity as a state variable after I edit that data.
When I add a value as a new date I can show that value well, but after I edit the value it doesn't reflect the editing...
If I don't use #State, the value can be shown well, even after I edit it. But I need to use as #State value to bind it to a child view.
How could I solve this problem?
(I want to show the collect value after editing in a place I comment out as B same as like A in ListView.swift )
CDTest.xcdatamodeld
CDTestApp.swift
import SwiftUI
#main
struct CDTestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
ListView()
}
}
PersistenceController.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CDTest")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
HomeViewModel.swift
import SwiftUI
import CoreData
class HomeViewModel :ObservableObject{
#Published var content = ""
//for newdata sheet
#Published var isNewDate = false
//storing update Item
#Published var updateItem : Task!
func writeData(context : NSManagedObjectContext){
if updateItem != nil {
updateItem.content = content
try! context.save()
updateItem = nil
isNewDate.toggle()
content = ""
return
}
let newTask = Task(context: context)
newTask.content = content
do{
try context.save()
isNewDate.toggle()
content = ""
}catch{
print(error.localizedDescription)
}
}
func EditItem(item:Task){
updateItem = item
content = item.content!
isNewDate.toggle()
}
}
ListView.swift
import SwiftUI
import CoreData
struct ListView: View {
#StateObject var homeData = HomeViewModel()
#FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(key:"date",
ascending:true)],animation:.spring()) var results:FetchedResults<Task>
#Environment(\.managedObjectContext) var context
var body: some View {
NavigationView{
VStack(spacing:0){
//Empty View...
if results.isEmpty{
Text("NO Data")
}else{
LazyVStack{
ForEach(results){task in
VStack(alignment: .leading, spacing: 5, content: {
Text(task.content ?? "") // A
ListRow(content: task.content ?? "") // B
})
.contextMenu{
Button(action: {
homeData.EditItem(item: task)
}, label: {
Text("Edit")
})
Button(action: {
context.delete(task)
try! context.save()
}, label: {
Text("Delete")
})
}
}
}
.padding()
}
//Add Button...
Button(action: {homeData.isNewDate.toggle()}, label: {
Image(systemName: "plus")
.font(.largeTitle)
.padding()
})
.sheet(isPresented: $homeData.isNewDate, content: {
NewDataView(homeData: homeData)
})
}
}
}
}
ListRow.swift
import SwiftUI
struct ListRow: View {
#State var content:String
var body: some View {
Text(content)
NavigationLink(destination:DetailView(content: $content)){
Text("DETAIL")
}
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
#Binding var content:String
var body: some View {
Text(content)
}
}
NewDataView.swift
import SwiftUI
struct NewDataView: View {
#ObservedObject var homeData : HomeViewModel
#Environment(\.managedObjectContext) var context
var body: some View {
VStack{
Text("\(homeData.updateItem == nil ? "Add New" : "Up date")Task")
.font(.title)
TextEditor(text: $homeData.content)
.padding()
//Add Button...
Button(action: {homeData.writeData(context: context)}, label: {
Text(homeData.updateItem == nil ? "Add Now" : "Update")
.font(.title)
.fontWeight(.bold)
})
.padding()
.disabled(homeData.content == "" ? true : false)
.opacity(homeData.content == "" ? 0.5 : 1)
}
}
}
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App
Just pass entire task object into ListRow, like
VStack(alignment: .leading, spacing: 5, content: {
Text(task.content ?? "") // A
ListRow(task: task) // B // << here !!
so now we can use ObservedObject wrapper, which gives binding to published property:
struct ListRow: View {
#ObservedObject var task: Task
var body: some View {
Text(task.content ?? "")
NavigationLink(destination:DetailView(content: $task.content)){
Text("DETAIL")
}
}
}

Swift UI + CoreData + Toggle

I am using SwiftUI with CoreData. Each cell has a Toggle which updates CoreData isComplete: Bool. It works great if I have more than one item in core data, if I only have one item, I have to click the Toggle 3 times before it disappears or the view refreshes.
CategoryCompleteToggle(passedCategory: passedCategory, isOn: $isOn)
} // End HStack
Text(catName)
} // End VStack
.padding(10)
}
}
struct CategoryCompleteToggle: View {
var passedCategory: Category
#Environment(\.managedObjectContext) var moc
#ObservedObject var catToggle = CategoryToggle()
#Binding var isOn: Bool
var body: some View {
VStack(alignment: .trailing) {
Toggle(isOn: $isOn) {
EmptyView()
}
.onAppear {
self.isOn = self.passedCategory.catCompleted
print("ONLOAD ISON VALUE: \(self.isOn)")
}
.onTapGesture {
self.catToggle.catToggleOn.toggle()
self.catToggle.catToggleOn = !self.isOn
print("IsOn Value: \(self.isOn)")
print("Passed Category Name: \(self.passedCategory.catName ?? "Unknown")")
print("Current Completed Value: \(self.passedCategory.catCompleted)")
self.passedCategory.catCompleted = !self.isOn
do {
try self.moc.save()
} catch {
print(error.localizedDescription)
}

Resources