2

Steps to reproduce:

  1. Start creation of a new question
  2. Swipe it down
  3. Start creation of an another one question
  4. Swipe it down too
  5. Tap on bottom block, you will see this:

    layers

  6. Switch to another app via double clicking the home button

  7. Return to the Stack Exchange app via the same double click
  8. Wait a moment, you will see a single empty layer:

    empty layer

  9. Tap anywhere and all layers are lost

  • App Version: 1.6.1.2
  • Device: iPad Air 2 (Wi-Fi)
  • OS Version: Version 10.1.1 (Build 14B100)
  • No issue on iPhone – Cai Dec 1 '16 at 10:05
  • Indeed. No repro here on iPhone. Maybe it is iPad specific or related to networking issues. – Patrick Hofman Dec 1 '16 at 22:48
  • @PatrickHofman I need to get rid of the iPad, byu iPhone and stop submitting bug reports :) – αλεχολυτ Dec 2 '16 at 8:20
  • I will give you my postal address to send the device to ;) – Patrick Hofman Dec 2 '16 at 8:21
  • Does rotating to portrait and back fix it? – Brian Nickel Dec 5 '16 at 22:46
  • NM, I see what's happening here. – Brian Nickel Dec 5 '16 at 22:47
2

This will be fixed in 1.6.2.1.

There are a couple bugs in play here.

1. The app crashes on the iPad when you leave this screen.

Here's a fun thing you can do with your iPad or iPhone. After using the device in portrait for a while, leave the app your in, rotate to landscape (probably get into a game on an iPhone), and open the app switcher.

On the iPad you'll have up-to-date screenshots of your all your apps. On the iPhone, they will be old, comically old. Open any app on the iPhone and it won't look like the screenshot.

The reason for this exercise is to lead into why this bug happened on iPad but not iPhone. To get landscape and portrait screenshots, the iPad does the following after entering the background:

  1. Take a snapshot of what was last on the screen.
  2. Resize the app as if the iPad rotated, sending all events to the app.
  3. Take a snapshot of what the app looks like now.
  4. Resize the app to its original dimensions, sending all events to the app.

While all this occurs, the app is in the background so a variety of functions you'd expect to work don't. Namely, UIView.snapshotView(afterScreenUpdates:) returns nil. Unwrapping nil crashed the app.

The full fix I employed for this was to A) not crash if the snapshot is nil, but also B) employ more complex logic for this scenario.

If we see that we're rotating in the background, we don't blindly discard the snapshots. We hold onto them until the app returns to its original orientation. Additionally, don't assume the snapshots we have are any good if the app wasn't active when we took them. Now we the app becomes active, it checks if it needs snapshots and takes them (after a small delay). This also fixed the blank layer you saw when the app restored.

The full change is on GitHub.

2. We weren't properly restoring the view controllers on restart.

The app crashed when you left, so it restored when you came back. Unfortunately, the draft view controllers didn't restore with it. That's because the view controllers were offscreen (you just see a snapshot on the layers) so they're managed by a restored object which was failing to restore.

iOS state restoration uses a somewhat magic NSKeyedUnarchiver. If a view controller (or object) didn't meet the criteria to be saved or the restored version doesn't meet specific criteria (same class or a subclass, same restoration identifier), it becomes nil. If that object was stored in an array or dictionary, restoring that array or dictionary throw an exception (which is always fatal in Objective-C).

To avoid crashing on restoration mistakes, I have a few methods on NSCoder that will wrap the nullable object and return a stripped down array on restore:

extension NSCoder {

    func encodeSafeArray(_ array:[AnyObject], forKey key:String) {
        encode(SafeWrapper.wrapArray(array), forKey: key)
    }

    func decodeSafeArrayForKey<T: AnyObject>(_ key:String) -> [T] {
        return SafeWrapper.unwrapArray(decodeObject(forKey: key))
    }
}

The problem is that these methods are useful so I added them to another package and they were defined multiple times. This wouldn't normally be a problem, Swift names are module mangled so I can declare them internally in this module and publicly in another module I don't reference but...

  1. Because this is an extension on an Objective-C class, and encodeSafeArray(_:forKey:) is Objective-C eligible, it got added to the Objective-C runtime once from each module. Which one wins is undefined.
  2. Because it's in the Objective-C runtime and ???, this encodeSafeArray(_:forKey:) got dynamically dispatched.
  3. Because decodeSafeArrayForKey(_:) is not eligible for the Objective-C runtime, it got statically dispatched.

As a result, when the state was saved and restored, the app was calling incompatible versions of the two methods, resulting in casts returning nil and an empty array being returned.

The fix was to annotate encodeSafeArray(_:forKey:) as @nonobjc ensuring static dispatch.

The change is on GitHub.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .