UIViewPropertyAnimator crashes on dealloc after calling finishAnimation(at:) - ios

When using UIViewPropertyAnimator and calling finishAnimation(at:) after stopAnimation(false):
You either get a crash log of:
- [UIViewPropertyAnimator dealloc]
Or if you catch the assertion in a debugger (by setting an breakpoint for Objective-C exceptions)
Assertion failure in -[UIViewPropertyAnimator finishAnimationAtPosition:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3900.12.16/UIViewPropertyAnimator.m:1981
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'finishAnimationAtPosition: should only be called on a stopped animator!'
If you manage to catch the assertion, (the crash log isn't that helpful unless you catch those at release runtime as well), you will see that it complains about finishAnimation being called on a non-stopped animator.
Yet my code shows the animator always being stopped before finishAnimation is called.
animator.stopAnimation(false)
animator.finishAnimation(at: .start)
What could be causing this crash, the state should always be stopped after calling stopAnimation?

The reason that this happened for me was a timing issue with completion blocks:
It means that UIViewPropertyAnimator.state (UIViewAnimatingState) is in an invalid state. Since this is just an Objective C enum it can have any raw value beyond what is defined in the cases. In the case of my crash the value was 5.
I was doing a bunch of chained animations that referenced each other and had something like this:
animator.addCompletion({ _ in
// Here I called a piece of code that eventually referenced this animator
// and attempted to call `stopAnimation(false)` and `finishAnimation(at:)`
// on it causing the crash.
//
// You will notice if you examine the `state` of the animator here, it will have
// an invalid `rawValue` of 5, at least up to iOS 13.2, as this is an implementation detail.
})
The solution is to not attempt any operations on the animator until the completion block is finished executing.
If you can not avoid it, a workaround that works with current UIKit implementations is to DispatchQueue.main.async out of that completion block which will allow it to finish. Of course, this is not a guarantee that the state will be valid inside of that async in future versions of iOS.
On Swift, to debug this you can use the following extension:
extension UIViewAnimatingState: CustomStringConvertible, CustomDebugStringConvertible {
var isValid: Bool {
switch self {
case .inactive, .active, .stopped:
return true
default:
return false
}
}
public var description: String {
switch self {
case .inactive:
return "inactive"
case .active:
return "active"
case .stopped:
return "stopped"
default:
return "\(rawValue)"
}
}
public var debugDescription: String {
return description
}
}
Therefore when you call stopAnimation(false) and finishAnimation(at:) you can at least avoid a crash and alert yourself to a possible logical issue with your animators:
// This is for debugging, the code below should be safe even if isValid is false
assert(animator.state.isValid, "animator state is not valid")
animator.stopAnimation(false)
if animator.state == .stopped {
// This call is the one that can assert and crash the app
animator.finishAnimation(at: .start)
}

Related

Throwing in Combine map operator gives "Main actor-isolated property 'foo' can not be referenced from a non-isolated context"

I have the following operator in a Combine pipeline, which must be run on the main thread, before returning to the global dispatch queue. That's because FooManager.shared.foo is on the MainActor:
extension AnyPublisher where Output == (Foo, Bar), Failure == Error {
func checkForFoo() -> AnyPublisher<Output, Failure> {
self
.receive(on: RunLoop.main)
.tryMap { x, y in
let isFoo = FooManager.shared.foo // Error here
if isFoo {
guard FooManager.shared.bar == true else {
throw MyError.cancelled
}
}
return (x, y)
}
.receive(on: DispatchQueue.global(qos: .userInitiated))
.eraseToAnyPublisher()
}
}
Throwing from inside the guard causes the error Main actor-isolated property 'foo' can not be referenced from a non-isolated context. However, if I just return nothing from the guard statement, it doesn't do this. I need to be able to throw, but I don't understand why it's mentioning the main actor stuff, when I've switched to receive on the main runloop?
First, the compiler doesn't know that you've switched to the main thread, you do that at runtime, but the compiler doesn't know that when it compiles your code.
Second, the MainActor and the main thread are two different things. The MainActor guarantees that, when it is running code, the thread it is running that code on will be the only thread running MainActor code. That does not guarantee your code will run on the main thread. But if it's running on another thread, the main thread will be suspended.
Finally the complaint you are seeing from the compiler is not that you're code is not on the main thread, but that you are using async code in a non async context. Because FooManager is (apparently) actor isolated (which actor is immaterial), any calls to it must be awaited and you can only use await in an asynchronous context. Here the closure you pass to tryMap is a synchronous context, it's not allowed to suspend, so the compiler complains.

Can two same closures on the main thread, from the same function call, crash an iOS program written in Swift?

My users experienced crashes when I sent them an update on TestFlight. After examining the eight crash reports they submitted, I've noticed a commonality - there are two of the same closures sitting on top of thread 0. Could this have caused the crash? What do you think is the cause, if not?
Please see image for crash report of thread 0. All other threads generally look the same in the report.
Note - when the users opened their app subsequent times after the initial opening, they did not experience further crashes.
Thank you in advance for your thoughts.
Update from comments, 9/29/22 -
Here's the closure code as requested by Damien and Tadreik:
When the app is opened, this line runs during initialization, which sets up the variables the connection view controller needs. Thus the empty closure.
if !twilioIDs.isEmpty {
ProfileModelManager.shared.getUsersForConnectionView(withTwilioIDs: twilioIDs) { _ in }
}
And the code below is invoked when the connection view is tapped on from the menu tab the second time:
if !twilioIDs.isEmpty {
ProfileModelManager.shared.getUsersForConnectionView(withTwilioIDs: twilioIDs) { result in
guard let success = result else { return }
if success {
self.handleSuccessOfGettingConnectionCards()
}
else {
self.handleFailureOfGettingConnectionCards()
}
}
}
Here is the code for handleSuccessOfGettingConnectionCards -
refreshControl.endRefreshing()
hideNoConnectionsLabel()
createChatViewControllersForChannels()
connectionsTableView.alpha = 1
errorConnectingLabel.alpha = 0
connectionsTableView.reloadData()
showActivitySpinner(false)
navigationController?.navigationBar.isHidden = true
Here is the code for handleFailureOfGettingConnectionCards -
showErrorConnectingMessage()
refreshControl.endRefreshing()
connectionsTableView.alpha = 0
hideNoConnectionsLabel()
showActivitySpinner(false)
Thanks in advance again for any intuition or experience you may share.
The crash log for thread 0
The code inside the closure ProfileModelManager.shared.getUsersForConnectionView(withTwilioIDs: twilioIDs) { result in captures a reference to self, your problem might be that at the time of execution, self is pointing to a null reference (has been deallocated), which can cause a crash. Try to set a weak reference to self like that and see if the crash still occurs :
if !twilioIDs.isEmpty {
ProfileModelManager.shared.getUsersForConnectionView(withTwilioIDs: twilioIDs) { [weak self] result in
guard let success = result else { return }
if success {
self?.handleSuccessOfGettingConnectionCards()
}
else {
self?.handleFailureOfGettingConnectionCards()
}
}
}
If you want you could also handle the nil case of the weak self as any other optional.

AVCaptureSession stopRunning() Mysterious Crash

Recently I got notified about a crash from firebase and this is the message:
[AVCaptureSession stopRunning] stopRunning may not be called between calls to beginConfiguration and commitConfiguration
I went through my code and the weirdest part is I never call and there is no mention nowhere of beginConfiguration() and commitConfiguration().
In my CameraManager class this is the function that triggers the crash, it called on deinit:
func stop() {
guard isStarted else { return Log.w("CameraManager wasn't started") }
queue.async {
self.isStarted = false
self.isCapturingFrame = false
self.isCapturingCode = false
self.session?.stopRunning()
self.session = nil
self.device = nil
}
notificationCenter.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
layer.removeFromSuperlayer()
layer.session = nil
}
The queue is just a serial disptach queue.
No matter what I tried, I couldn't reproduce this crash.
Tried pulling the menu, push notification, phone call, simulate memory warning etc...
Just to clarify, there is not a single place in my code calling beginConfiguration and commitConfiguration.
I could imagine that layer.session = nil will cause a re-configuration of the capture session (since the connection to the preview layer is removed). And since you are calling stopRunning() async, I guess you can run into race conditions where stopRunning() gets called right in the middle of the configuration change.
I would suggest you either try to make the cleanup calls synchronous (queue.sync { ... }) or move the layer cleanup into the async block as well.

UIManagedDocuemnt won't open

My app (Xcode 9.2, Swift 4) uses UIManagedDocument as a basic Core Data stack. Everything was working fine for months but lately I've noticed several cases where the app won't load for existing users because the core data init isn't completing. This usually happens after a crash in the app (I think but not sure).
I've been able to recreate the problem on the debugger and narrowed the problem down to the following scenario:
App starts up --> core data is called to start up --> UIManagedDocument object is init'd --> check doc status == closed --> call open() on doc --> open never completes - the callback closure is never called.
I've subclassed UIManagedDocument so I could override configurePersistentStoreCoordinator() to check if it ever reaches that point but it doesn't. The subclass override for handleError() is never called either.
The open() process never reaches that point. What I can see if I pause the debugger is that a couple of threads are blocked on mutex/semaphore related to the open procedure:
The 2nd thread (11) seems to be handling some kind of file conflict but I can't understand what and why. When I check documentState just before opening the file I can see its value is [.normal, .closed]
This is the code to init the doc - pretty straight forward and works as expected for most uses and use cases:
class MyDataManager {
static var sharedInstance = MyDataManager()
var managedDoc : UIManagedDocument!
var docUrl : URL!
var managedObjContext : NSManagedObjectContext {
return managedDoc.managedObjectContext
}
func configureCoreData(forUser: String, completion: #escaping (Bool)->Void) {
let dir = UserProfile.profile.getDocumentsDirectory()
docUrl = dir.appendingPathComponent(forUser + GlobalDataDocUrl, isDirectory: true)
managedDoc = UIManagedDocument(fileURL: docUrl)
//allow the UIManagedDoc to perform lieghtweight migration of the DB in case of small changes in the model
managedDoc.persistentStoreOptions = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
switch (self.managedDoc.documentState)
{
case UIDocumentState.normal:
DDLogInfo("ManagedDocument is ready \(self.docUrl)")
case UIDocumentState.closed:
DDLogInfo("ManagedDocument is closed - will open it")
if FileManager.default.fileExists(atPath: self.docUrl.path) {
self.managedDoc.open() { [unowned self] (success) in
DDLogInfo("ManagedDocument is open result=\(success)")
completion(success)
}
}
else{
self.managedDoc.save(to: self.managedDoc.fileURL, for: .forCreating) { [unowned self] (success) in
DDLogInfo("ManagedDocument created result=\(success) ")
completion(success)
}
}
case UIDocumentState.editingDisabled:
fallthrough
case UIDocumentState.inConflict:
fallthrough
case UIDocumentState.progressAvailable:
fallthrough
case UIDocumentState.savingError:
fallthrough
default:
DDLogWarn("ManagedDocument status is \(self.managedDoc.documentState.rawValue)")
}
}
}
Again - the closure callback for managedDoc.open() never gets called. It seems like the file was left in some kind of bad state and cannot be opened.
BTW, if I copy the app container from the device to my mac and open the SQLLite store I can see everything is there as expected.

Coredata concurrency issues

I have a NSOperation subclass that has its private context, and a singleton data manager class that has a context on main queue. All the UI and crud operation are done by this singleton class and a background fetch from cloud kit is done by the NSOperation subclass. Had few doubts as below.
Following is the code i have in NSoperation subclass.Can below code create deadlock?
self.localStoreMOC?.performBlockAndWait({ () -> Void in
//Long process of fetching data from cloud and pushing changes to cloud happens here.
var error:NSErrorPointer = nil
if self.localStoreMOC!.hasChanges
{
do
{
try self.localStoreMOC!.save()
}
catch let error1 as NSError
{
error.memory = error1
}
if error == nil
{
self.localStoreMOC!.parentContext!.performBlockAndWait({
do
{
try self.localStoreMOC!.parentContext!.save()
}
catch let error1 as NSError
{
print("wasSuccessful error1 \(error1)")
}
})
}
}
}
If i have a another singleton class using this class NSManagedOBject do i need to pass them though ID ?
First, you need to turn on -com.apple.CoreData.ConcurrencyDebug 1 in your run time arguments. That will help insure you are calling everything on the proper thread/queue.
Second, you are doing a lot of forced unwrapping of optionals, that is a very bad habit to be in. Best to unwrap them properly or use the optional unwrapping.
Third, what happens when you pause the debugger? Where is the line of code that it is pausing on and what queues are you on?
Just turning on the concurrency debug will most likely show you your issue.
Item 2
If you are wanting to pass a reference to a NSManagedObject from one context to another then yes, you need to use the NSManagedObjectID as the NSManagedObject is not safe to pass between contexts.
Code Formatting
Was playing with the formatting a bit, the results may be of interest to you:
guard let local = localStoreMOC else { fatalError("Local store is nil") }
guard let parent = local.parentContext else { fatalError("Parent store is nil") }
local.performBlockAndWait {
//Long process of fetching data from cloud and pushing changes to cloud happens here.
if !local.hasChanges { return }
do {
try local.save()
parent.performBlockAndWait {
do {
try parent.save()
} catch {
print("wasSuccessful error1 \(error)")
}
}
} catch {
print("Failed to save local: \(error)")
}
}
This removes the forced unwrapping of optionals and prints both errors out if you get one in either case.
Update
Also, some developers, say that nested performblockandwait like above will cause deadlock.
performBlockAndWait will never cause a deadlock. It is more intelligent than that.
If you are going from one queue to another, then you want each queue to block and wait like the code describes.
If you were on the right queue and called performBlockAndWait then the call would effectively be a no-op and will NOT deadlock

Resources