Swift: updating diffable data source with animatingDifference crash app - swift

i am trying to update dataSource on iphone SE/ iOS14 device and i got app crash:
var snapshot = NSDiffableDataSourceSnapshot<Section, SettingsProfileMenuModel>()
snapshot.appendSections(Section.allCases)
snapshot.appendItems(menuList, toSection: Section.settingsProfileSection)
self.dataSource.apply(snapshot, animatingDifferences: false)
Error is on line "self.dataSource.appl....": Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue
If i try to do it like this, but it doesn't even update
DispatchQueue.main.async {
self.dataSource.apply(snapshot, animatingDifferences: false)
}
What can be wrong? Also i have one more crash when i try to close keyboard by tapping on "close" button.
as I understand the reason for this subscription to the keyboard
NotificationCenter.default.addObserver(self,
selector: #selector(presentKeyboard),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(hideKeyboard),
name: UIResponder.keyboardWillHideNotification,
object: nil)
I mean, maybe it's important. My screen has a tableView from the top of the screen to the bottom. If i set animatingDifference = true, all fine besides crash of keyboard

Related

NSNotification observer not being removed unless it has been triggered once

In summary
If a user takes a photo with an overlay on VC1 it triggers a notification. The corresponding observer gets removed by my method as a I present VC2.
If the user doesn't take a photo on VC1, the notification observer isn't trigger. The same removal method is called as VC2 is presented but the observer stays active and causes unwanted behaviours with VC2 photo capture.
Full explanation
I have a registration form where a user can take an optional photo of their face. I'm using a simple UIImagePickerController and presenting a 'passport' style overlay.
if sourceType == .camera {
imagePickerController.cameraDevice = .front
let overlay = PassportOverlayView(frame: imagePickerController.view.frame)
imagePickerController.cameraOverlayView = overlay
}
The overlay covers the 'retake' and 'choose' buttons of the UIImagePickerController so I observe the following NSNotifications to remove the overlay when a photo taken, and re-add it if required should the user wish to retake their photo.
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidCaptureItem"), object:nil, queue:nil, using: { note in
self.imagePickerController.cameraOverlayView = nil
})
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidRejectItem"), object:nil, queue:nil, using: { note in
self.imagePickerController.cameraOverlayView = PassportOverlayView(frame: self.imagePickerController.view.frame)
})
Everything works as intended. I remove the notifications before the next UIViewController is pushed onto the stack.
func removeObservers(){
print("remove Observers")
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidCaptureItem"), object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidRejectItem"), object: nil)
}
If the user has taken a photo and therefore called the observer adding a photo on the next UIViewController, which doesn't require an overlay, works fine.
If however, the user doesn't take a photo on the first UIViewController [which is perfectly fine for the purposes of my app] they run into an issue on the second UIViewController.
In both scenerios the removeObservers() function is called. "remove Observers" is printed to the console on both occasions. Yet when the users tries to take a photo from within the second UIViewController the app crashes as it Unexpectedly found nil while implicitly unwrapping an Optional value and points me to the self.imagePickerController.cameraOverlayView = nil line of the _UIImagePickerControllerUserDidCaptureItem notification.
I understand the error, it's trying to remove a cameraOverlayView that isn't there. What I can't understand is why the observer is still there when I believe it's already been removed. If the user takes the first photo with the cameraOverlayView and triggers the _UIImagePickerControllerUserDidCaptureItem notification there isn't an issue. The observer is removed and subsequent UIImagePickerControllers don't have the same issue.
Any help would be greatly appreciated.
I think I know what your problem is. A retain cycle is causing the observer to not be removed successfully. The reason why you have a retain cycle is because you are using a strong reference to self in the closure where you set your notifications. Here is how you fix it:
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidCaptureItem"), object:nil, queue:nil, using: { [unowned self] note in
self.imagePickerController.cameraOverlayView = nil
})
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidRejectItem"), object:nil, queue:nil, using: { [unowned self] note in
self.imagePickerController.cameraOverlayView = PassportOverlayView(frame: self.imagePickerController.view.frame)
})
Again, you shouldn't have to worry about removing the observers, so try to run your code without removeObserver and it should work.
Hope this helps.

CoreData and CloudKit Sync works, but NSPersistentStoreRemoteChange notification never fires

I've updated an existing CoreData app to use CloudKit so that it can keep data synchronised across multiple devices (for the same iCloud account).
It works! The data sync's just fine.
But the primary UIViewController's view does not reflect the new data. To see the changes, I have to switch to a different view, then switch back to the primary view which has a refreshData() call in its viewWillAppear() function. It then re-reads from the data store and displays all the data (including whatever was updated on some other device.
The UIViewController should be observing the NSPersistentStoreRemoteChange notification as per the viewDidLoad() below:
override func viewDidLoad() {
super.viewDidLoad()
// NotificationCenter.default.addObserver(self, selector: #selector(handleRemoteChange), name: .NSPersistentStoreRemoteChange, object: AppDelegate.appDelegate().persistentContainer.persistentStoreCoordinator)
NotificationCenter.default.addObserver(self, selector: #selector(handleRemoteChange), name: .NSPersistentStoreRemoteChange, object: nil)
}
But the handler function never gets called:
#objc func handleRemoteChange(notification: Notification) {
print("Handling remote change to data in Collection Controller")
refreshData()
}
That print statement never prints anything, and a breakpoint set there never fires.
I have configured the managed object context to merge changes from parent as required immediately after the NSPersistentCloutKitContainer is created:
container.viewContext.automaticallyMergesChangesFromParent = true
The application has all the relevant iCloud capabilities and entitlements that I'm aware of after scouring the web (and Stack Overflow) for clues, including:
"Remote notifications" (Background Modes)
"CloudKit" (iCloud)
A container has been created and selected with an identifier of the
format: iCloud.reverse.domain.app
The Container's persistentStoreDescription has the NSPersistentStoreRemoteChangeNotificationPostOptionKey option set to true:
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("FAILED to load persistent store:\(storeDescription)\nERROR:\n\(error)\n\(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
for description in container.persistentStoreDescriptions {
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
}
Clearly the data is being sync'd OK. It's just the notification that is not working as expected. So the end user doesn't see the new/sync'd data automatically.
How can I get this to work?
I have the same problem, and solved by configuring custom persistent store descriptions before calling loadPersistentStores(completionHandler:)
container = NSPersistentCloudKitContainer(name: "RubikClub")
// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
NotificationCenter.default.addObserver(self, selector: #selector(type(of: self).storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
container.loadPersistentStores { (_, error) in
if let error = error {
fatalError("init core data error: \(error.localizedDescription)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
I have the same problem. I just found out that it works fine in the TestFlight builds, but when directly built with Xcode, it does not. Have you tried that?
Also remember to listen for remote changes as outlined here:
https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes
This seems to require the NotificationCenter.default.addObserver object: field to be container.persistentStoreCoordinator to work, which is in the documentation at https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes.
Apple's example code at https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud uses object: container but doesn't work.
Listener:
NotificationCenter.default.addObserver(self, selector: #selector(handleRemoteChange), name: .NSPersistentStoreRemoteChange, object: nil)
Should be added inside
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Here:
NotificationCenter.default.addObserver(self, selector: #selector(handleRemoteChange), name: .NSPersistentStoreRemoteChange, object: nil)
}
Don't forget to unsubscribe on view will disappear:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .NSPersistentStoreRemoteChange, object: nil)
}

Swift Notification Not Firing

I am having a little bit of trouble. I am trying to use the notification center to alert the app that some content is done loading. After the user signs up or logs in successfully I create the view controller and then and then make it the root view controller using the blow
func finishLoggingIn() {
// print("Finish logging in from LoginController")
let homeController = HomeViewController()
self.loginButton.stopAnimation(animationStyle: .expand, completion: {
self.view.window?.rootViewController = homeController
self.view.window?.makeKeyAndVisible()
})
}
The login button just creates a loading animation on the button for UI purposes.
When I first enter the controller I add the observer for the notification.
let MainVCSetup = Notification.Name("mainVCComplete")
NotificationCenter.default.addObserver(self, selector: #selector(handleRootViewSwitch), name: MainVCSetup, object: nil)
When the content in my mainVC is done loading I post this same notification to the Notification Center like so
NotificationCenter.default.post(name: MainVCSetup, object: nil)
However this function never fires off no matter what I do
#objc func handleRootViewSwitch(){
print("Trying to handle root view switch attack")
NotificationCenter.default.removeObserver(self, name: MainVCSetup, object: nil)
}
If anyone notices where I went wrong I would greatly appreciate it.

Observing Notification in Today Widget which is posted from main app

I have declared notification name in a swift file which has target membership of both main app and today widget:
let SpecialKey = Notification.Name("howLongNotif")
Then in my main app view controller, I am posting notification when location is updated (background mode for location updates is on):
NotificationCenter.default.post(name: SpecialKey, object: nil, userInfo: nil)
In my today widget viewDidLoad, I am observing it like this:
NotificationCenter.default.addObserver(self, selector: #selector(TodayViewController.dataReceived), name: SpecialKey, object: nil)
and have:
func dataReceived(_notification: NSNotification) {
print("data received")
}
But dataReceived function is never invoked.
I tested by moving the observer and dataReceived function to main app and it works fine there.
Your main app and today is extension are run as separate processes on the phone. NSNotificationCenter only works within a single process.
To pass information between your extension and main app, you can use NSUserDefaults or a file in the shared container.

WatchOS: getting applicationDidBecomeActive notifications

I'm making a framework that needs to do stuff when my apple watch is entering background and foreground.
I'm looking for an equivalent of this iOS code for the Apple watch since UIApplication is not present in UIKit anymore :
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "applicationDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
any help would be nice
As of watchOS 7 the following have been added:
WKExtension.applicationDidBecomeActiveNotification
WKExtension.applicationDidEnterBackgroundNotification
WKExtension.applicationDidFinishLaunchingNotification
WKExtension.applicationWillEnterForegroundNotification
WKExtension.applicationWillResignActiveNotification
Source: https://developer.apple.com/documentation/WatchKit/wkextension
Appears that the WatchOS equivalent of
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "applicationDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
is simply
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "applicationDidEnterBackground", name: "UIApplicationDidEnterBackgroundNotification", object: nil)
One just need to replace the emum by its string equivalent
Closest you get is applicationDidBecomeActive and applicationWillResignActive
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
}