UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: "Accessing StateObject's object without being installed on a View. This will create a new instance each time."

Forums > SwiftUI

I'm trying to edit some Core Data object values (ratings and a text), obtaining them from a local notification.

To do so, I have added some actionIdentifiers to my notification request, which are handled this way on AppDelegate:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

        //...some code...

        let categoryIdentifier = response.notification.request.content.categoryIdentifier
        let actionIdentifier = response.actionIdentifier
        let userInfo = response.notification.request.content.userInfo
        let rehearsalId = userInfo["rehearsalId"] as? String

        if categoryIdentifier == "rehearsalCategory" {
            let view = MainView()
            switch actionIdentifier {
            case "rehearsalTimer.rate0Action":
                view.rateRehearsalFromNotification(rehearsalId: rehearsalId ?? "", rating: 0, notes: "")
            case "rehearsalTimer.rate1Action":
                view.rateRehearsalFromNotification(rehearsalId: rehearsalId ?? "", rating: 1, notes: "")
            case "rehearsalTimer.rate2Action":
                view.rateRehearsalFromNotification(rehearsalId: rehearsalId ?? "", rating: 2, notes: "")
            case "rehearsalTimer.notesAction":
                if let userInput = (response as? UNTextInputNotificationResponse)?.userText {
                    view.rateRehearsalFromNotification(rehearsalId: rehearsalId ?? "", rating: 1, notes: userInput)
                }
            default: break
            }
        }

        completionHandler()
    }

On my MainView (the main view of my tabbed app), I've written this method:

struct MainView: View {

@Environment(\.managedObjectContext) var moc
@FetchRequest(sortDescriptors: []) var rehearsals: FetchedResults<Rehearsal>

//More code here...

func rateRehearsalFromNotification(rehearsalId: String, rating: Int16, notes: String) {        
        rehearsals.nsPredicate = NSPredicate(format: "%K==%@", #keyPath(Rehearsal.id), rehearsalId)
        let rehearsalToEdit = rehearsals.first
        rehearsalToEdit?.setValue(rating, forKey: "rating")
        try? moc.save()
    }
}

I do can access this three pieces of data on my view, but the question is that I can't find the way to make the fetch request, with its predicate, select that exact object (Rehearsal), with that id, to set its new values. The above code, for example, says "Ambiguous reference to member 'id'", although I've tried with many different ways of fecth request sintax and so, without success, many of them finishing with the text I write on the title of this post.

Any ideas? Thanks in advance.

1      

The error message or warning in the title should come from let view = MainView().

I don't think you have to do your work in userNotificationCenter. You could create a class which conforms to UNUserNotificationCenterDelegate and use this in your MainView to handle actions according to notifications.

1      

Thank you, Hatsushira.

This is one of the options I'm working on. In fact, the code is not on userNotificationCenter, but on AppDelegate. But I'm not able to understand completely how. As far as I know, notifications are handled exclusively by AppDelegate, aren't they? Or you suggest adding this method to a new class, and adding my "actionIdentifier" control there?

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)

But, in that case, I still can't see the exact way to instantiate this method "inside" my View. Could you please give me any guidance code?

Thanks again.

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

In adittion, this must be possible directly in AppDelegate:

let request: NSFetchRequest<Rehearsal>
request = Rehearsal.fetchRequest()
request.fetchLimit = 1
request.predicate = NSPredicate(format: "%K==%@", "id", rehearsalId)
let object = try? moc.fetch(request)

But just one problem I can't solve for weeks: I need a moc, a ManagedObjectContext, which is added to the environment the way Paul explains, but absolutely inaccesible from AppDelegate class:

@Environment(\.managedObjectContext) var moc

How can I access the singleton of that moc, the unique instance used on all the app, and available on environment? I can't find the answer to this question after reading dozens of articles and web pages. Nobody knows? May is it impossible?

1      

Or you suggest adding this method to a new class, and adding my "actionIdentifier" control there?

Yes, you should create a new class implementing UNUserNotificationCenterDelegate protocol.

But, in that case, I still can't see the exact way to instantiate this method "inside" my View. Could you please give me any guidance code?

You could handle it all on your AppDelegate but I would create an instance of your new class in your View. That's what the delegate is used for. I'm not an expert at it but for me it worked with my own NotificationHandler.

How can I access the singleton of that moc, the unique instance used on all the app, and available on environment?

You need to pass it via initializer. You can't access EnvironmentObjects outside of a View. These only work in a View. You can't access EnvironmentObjects in the initializer of the View, either. This is working as intended.

1      

Thanks! Trying your indications. Now I have a new class to handle notifications, away from AppDelegate:

import UserNotifications
import CoreData
import UIKit

class NotificationHandler: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    private let center = UNUserNotificationCenter.current()

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

        let center = UNUserNotificationCenter.current()
        center.delegate = self
        let categoryIdentifier = response.notification.request.content.categoryIdentifier
        let actionIdentifier = response.actionIdentifier
        let userInfo = response.notification.request.content.userInfo
        let rehearsalId = userInfo["rehearsalId"] as! String

        if categoryIdentifier == "rehearsalCategory" {

            let view = MainView()

            switch actionIdentifier {
            case "rehearsalTimer.rate0Action":
                view.rateRehearsalFromNotification(rehearsalId: rehearsalId, rating: 0, notes: "")
            case "rehearsalTimer.rate1Action":
                view.rateRehearsalFromNotification(rehearsalId: rehearsalId, rating: 1, notes: "")
            case "rehearsalTimer.rate2Action":
                view.rateRehearsalFromNotification(rehearsalId: rehearsalId, rating: 2, notes: "")
            case "rehearsalTimer.notesAction":
                if let userInput = (response as? UNTextInputNotificationResponse)?.userText {
                    view.rateRehearsalFromNotification(rehearsalId: rehearsalId, rating: 1, notes: userInput)
                }
            default: break
            }
        }
    }
}

I have commented all the same code on my AppDelegate file, in order not to confuse the code. But now, after notification taps, nothing occurs. It seems that this new class is not handling them correctly, as I control with breakpoints the running on XCode and it doesn't stop in this method. What am I missing?

1      

Maybe is this the problem?:

https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-uiapplicationdelegateadaptor-property-wrapper

Only one class can "work as" appDelegate. But, as I have another methods on AppDelegate to handle quick actions from home icon (shortcuts), how can I handle both in differente classes? 🧐

1      

I set also

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }

In the AppDelegate. My AppDelegate is

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate

My Notification Manager is only

class LocalNotificationManager: NSObject, ObservableObject

1      

I finally have found a much better solution. Not code, but logic:

  • I need to access Managed Object Context for AppDelegate to manage my CoreData changes, one option is indeed "moving" data to another place where that context is available.
  • But the other option is... placing the CoreData stack definition itself INSIDE AppDelegate:
import UIKit
import CoreData

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    //Core Data Stack!!
        lazy var persistentContainer: NSPersistentContainer = {
            let container = NSPersistentCloudKitContainer(name: "CompingApp")
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    //Here the rest of AppDelegate methods

So now I can handle perfectly all stuff (notifications reponses, home quick access actions...) from here.

The curious thing in conclusion: I have read up to hundred articles trying to find a solution for this issue, but nobody has seen the solution "out of the box", or "upside down" ;-)

Anyway: thank you very much @Hatsushira for your ideas, which has been crucial to get to the easiest solution.

2      

I'm not sure what will happen if you open the same container twice. I would guess you need the CoreDataStack in your views as well. How do you pass it to your views?

1      

I keep both: passing Core Data context to app environment (@Environment(.managedObjectContext) var moc in my views):

@main
struct MyApp: App {

   @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

   var body: some Scene {
        WindowGroup {
            let context = appDelegate.persistentContainer.viewContext
            MainView()
                .environment(\.managedObjectContext, context)

And using it directly on AppDelegate class itself, for notifications handling:

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentCloudKitContainer(name: "CompingApp")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

//More code...

let moc = self.persistentContainer.viewContext (*)

ALTHOUGH, in fact, I'm seeing some strange behaviour with Core Data saved through AppDelegate class 🧐. If I save data using usual environment way in my views, and later update the same data using AppDelegate method, some cases work, some not, missing data.

Do you suggest that this way would drive to two (or more) different instances of viewContext running on the app the same time? I suposse that the instance I use on AppDelegate (marked with asterisk on the above description) is the same as environment one. But, you are right!, it is not initializated there 😅.

Any further suggestions? Thanks again! I'm learning a lot with your orientations. Let's try to make it work at last 😊.

1      

The strange behaviour in your data could be because you open the container twice and have two instances running.

I looked up my old code and I missed a part. As I told you I have a class NotificatonManager.

class NotificationManager: NSObject, ObservableObject

In the init method of this class I call

override init() {
      super.init()
      UNUserNotificationCenter.current().delegate = self // setting the delegate here.
  }

You should be able to add an additional init with the viewContext.

Additionally (that I overlooked and forgot) I have an extension on my NotificationManager

extension NotificationManager: UNUserNotificationCenterDelegate  {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        // this should be the function you add your code.
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { }
}

These functions should be responsible when for the events that trigger when a notification is received.

So you should be able to do this in your view with a separate class.

1      

Thanks again, @Hatsushira. I'm giving a try again to your proposal. I have now this NotificationManager class, with your indications:

class NotificationManager: NSObject, ObservableObject {
    override init(){
        super.init()
        UNUserNotificationCenter.current().delegate = self
    }
}

extension NotificationManager: UNUserNotificationCenterDelegate  {
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

       //My notifications handling methods here

        }
        completionHandler()
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        center.delegate = self
        completionHandler([.banner, .badge, .sound])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { }
}
  1. You suggest "adding an additional init with the viewContext". Do you mean including CoreData stack inside this NotificationManager class? Does it has sense? The common way is creating it on another specific class (apart from AppDelegate option I used before, which we both think could be problematic). This is how I have it just now (working perfectly):
class DataController: ObservableObject {
    var container = NSPersistentCloudKitContainer(name: "MyApp")
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Failed to load Core Data: \(error.localizedDescription)")
            }
            self.container.viewContext.automaticallyMergesChangesFromParent = true
            self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
        }
    }
}

Is it correct? How can then I accomplish what you suggest, then? is it necessary? I know about the "singleton" technique (an static var for instanciate viewContext) from UIKit. I've tried, but can't make it match with SwiftUI.

  1. I have added this line of code to app entry point, as seen here: https://prafullkumar77.medium.com/how-to-handle-push-notifications-in-swiftuis-new-app-lifecycle-7532c21d32d7. Without it, complier doesn't arrive to NotificationManager.

    @StateObject var notificationCenter = NotificationManager()
  2. Finally, you say: "you should be able to do this in your view with a separate class". Sorry, I don't understand this last part: is this new NotificationManager the "separate class" you are saying? If so, and returning then again to the origin of the posts, how could I pass data obtained to that view, to call from there my @environment context, and saving to Core Data?

Please, sorry me for my so precise questions 😅, but I'm starting getting crazy with this, as I feel I am walking "on circles". Thank you very much anyway for all your time an orientatios 🙏.

1      

Well, you said, that you have to check in your data how to handle the information according to the actionIdentifier. So somehow there must be a reference to your DataController to get the data.

This is where SwiftUI is not really optimised yet. I guess the NotificationManager is in a View. As I said above, you can't access EnvironmentObjects in the init of a View. They don't exist in the init. There are two (or more ways) to achieve what you need.

  1. In this specific View you don't pass the DataController over the environment but over a init. With this way it is available in your init and you can pass it to other objects without creating a new one.

So in your View you declare variables:

@ObservedObject var dataController: DataController
@StateObject var notificationManager: NotificationManager

then the init of this View should be like

// If you have to instantiate other variables you have to add them as well
init(dataController: DataController) {
  self.dataController = dataController
  self._notificationManager = StateObject(wrappedValue: NotificationManager(dataController: dataController)) // Using the init of the NotificationManager you have to create.
  // do whatever you need to do
}

Please excuse syntax errors. I don't have an editor available.

  1. With a static function on your DataController and go the whole Singleton approach then.

I would go with the first one. Perhaps, there are more ways but unfortunately, I'm out of ideas :)

1      

Thanks for your patience, @Hatsushira.

"I guess the NotificationManager is in a View". No: it is and standalone class itself. But, I've dealing with the second option you suggest, after triple-cheaking this post: https://en.proft.me/2021/05/26/using-core-data-swiftui/, and I have added a .shared instance of DataController:

class NotificationManager: NSObject, ObservableObject {

    let dataController = DataController.shared

    override init(){
        super.init()
        UNUserNotificationCenter.current().delegate = self
    }
}

... as my DataController class has now a singleton: an static let.

class DataController: ObservableObject {

    static let shared = DataController()
    let container = NSPersistentCloudKitContainer(name: "MyApp")

    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Failed to load Core Data: \(error.localizedDescription)")
            }
            self.container.viewContext.automaticallyMergesChangesFromParent = true
            self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
        }
    }
}

This way, I can use both @environment tecnique for views:

@main
struct MyApp: App {

    let dataController = DataController.shared
    @StateObject var notificationCenter = NotificationManager()

    // More code here

    var body: some Scene {
        WindowGroup {
            MainView()
                .environment(\.managedObjectContext, dataController.container.viewContext)

... and THE SAME singleton on NotificationManager class.

I'm now testing the app and everything seems to work perfectly: no strange duplicated contexts, Core Data persistence, notifications handling...

If you thing this way will not throw errors on the future, we could say together "voilá, it's done"

Thanks again 😊

2      

You're welcome. Glad I could help. With this way we all were able to learn more ☺️

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.