qsaluan
qsaluan

Reputation: 59

How to use function in an Observable Object class to use in SwiftUI view?

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

Answers (1)

marwan37
marwan37

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

Related Questions