Reputation: 59
I am trying to create an array of locations as an observable object variable so that the view updates anytime there is a change to the array. The data in pins
is being added/deleted in another view. I have created the observable object class as shown with an identifiable struct:
struct Location: Identifiable {
let id = UUID()
let coordinate: CLLocationCoordinate2D
let pinTitle: String
let pinDescription: String
}
class Pins: ObservableObject {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(sortDescriptors: []) private var pins: FetchedResults<Pin>
@Published var locations: [Location] = []
func loadPins() -> [Location] {
for pin in pins {
locations.append(Location(coordinate: CLLocationCoordinate2D(latitude: pin.pinLatitude, longitude: pin.pinLongitude), pinTitle: pin.pinTitle ?? "error", pinDescription: pin.pinDescription ?? "error"))
}
return locations
}
}
Then I call the function loadPins()
in the .onAppear {}
modifier to the view as shown below however the array is somehow empty when I print the result ([]
):
struct CurrentView: View {
@ObservedObject var pinLocations = Pins()
var body: some View {
VStack {
// view content
}.onAppear {
pinLocations.loadPins()
print(pinLocations.locations)
}
}
Ideally, the observable object should stay updated automatically when additions/deletions are made to the observable object array from the other view however I cannot even get it to be initialized. Any help would be greatly appreciated!
SOLUTION:
The solution to this issue is to abstract out Core Data. The @FetchRequest property wrapper is only meant to be used inside of a View. The code below summarizes the approach. All credit for this solution is given to https://www.donnywals.com/. The link to the blog post is https://www.donnywals.com/fetching-objects-from-core-data-in-a-swiftui-project/. I have included the code for quick reference:
@main
struct MyApp: App {
let persistenceManager: PersistenceManager
@StateObject var pinItemStorage: PinItemStorage
init() {
let manager = PersistenceManager()
self.persistenceManager = manager
let managedObjectContext = manager.persistentContainer.viewContext
let storage = PinItemStorage(managedObjectContext: managedObjectContext)
self._pinItemStorage = StateObject(wrappedValue: storage)
}
var body: some Scene {
WindowGroup {
MotherView(pinItemStorage: pinItemStorage)
}
}
}
class PersistenceManager {
let persistentContainer: NSPersistentContainer = {
// name is name of core data data model file
let container = NSPersistentContainer(name: "MyAppModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
}
extension Pin {
static var dueSoonFetchRequest: NSFetchRequest<Pin> {
let request: NSFetchRequest<Pin> = Pin.fetchRequest()
request.sortDescriptors = []
return request
}
}
class PinItemStorage: NSObject, ObservableObject {
@Published var dueSoon: [Pin] = []
private let dueSoonController: NSFetchedResultsController<Pin>
init(managedObjectContext: NSManagedObjectContext) {
dueSoonController = NSFetchedResultsController(fetchRequest: Pin.dueSoonFetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil, cacheName: nil)
super.init()
dueSoonController.delegate = self
do {
try dueSoonController.performFetch()
dueSoon = dueSoonController.fetchedObjects ?? []
} catch {
print("failed to fetch items!")
}
}
}
extension PinItemStorage: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let pins = controller.fetchedObjects as? [Pin]
else { return }
dueSoon = pins
}
}
Use in view as:
struct MotherView: View {
@ObservedObject var pinItemStore: PinItemStorage
var body: some View {
List {
Section {
ForEach(pinItemStore.dueSoon) { pin in
Text(pin.name)
}
}
}
}
}
Upvotes: 1
Views: 1429
Reputation: 100
MKPointAnnotations & Pins need to be wrapped after fetching from CoreData like so:
import MapKit
extension MKPointAnnotation: ObservableObject{
public var wrappedTitle: String{
get{
self.pinTitle ?? "No Title"
}
set{
self.pinTitle = newValue
}
}
public var wrappedSubtitle: String{
get{
self.pinDescription ?? "No information on this location"
}
set{
self.pinDescription = newValue
}
}
}
You can also Try adding the following functions to your Location class.
import SwiftUI
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
let objectWillChange = PassthroughSubject<Void, Never>()
private let geocoder = CLGeocoder()
@Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
@Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
@Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
private func geocode() {
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
}
})
}
}
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 }
self.location = location
self.geocode()
}
}
extension CLLocation {
var latitude: Double {
return self.coordinate.latitude
}
var longitude: Double {
return self.coordinate.longitude
}
}
Upvotes: 1