All observations have been made on iOS 10 and Xcode 8. Some points may not be applicable for iOS-9-style widgets.
Additional iOS 11 observations have been added.
(obviously missing some within the object lifecycle, but these are the ones we feel are important)
-
viewDidLoad()boundsandtraitCollectionare not yet correctly initialized, subviews frames are completely wrong (1000x1000pt)
-
widgetActiveDisplayModeDidChange(_:withMaximumSize:)- Called with state-preserved display mode
- Should update the preferred content size unless unambiguously defined by constraints. Can lead to infinite calls to
viewWillTransition(to:with:)if the preferred size was not set. traitCollectionand subview frames are still incomplete/wrong, root view's frame size is equal to the maximum size that is passed as parameter
-
viewWillTransition(to:with:)- iOS 11: not called initially
- iOS 10: First method to be called with the correct view bounds, including subviews. Should be used to resize everything that is not updated through auto layout (e.g., collection view item sizes and insets).
traitCollectionis also complete: Compact width and regular height on iPhone (incl. 6/7 Plus in landscape), both regular on iPad. Does not seem to change during rotation.
-
viewWillAppear(_)- iOS 11: Resize everything that is not updated through auto layout.
- iOS 10: Should probably not be implemented in most cases.
- May be delayed slightly, see discussion below.
-
The widget UI is reloaded from scratch (i.e., a completely new instance of the VC) often
-
e.g., when:
- going back to the homescreen and waiting for ~5s
- going back to the homescreen and opening another app
- scrolling the widget out of view and waiting for ~5s
-
Updating data in
viewDidLoad()(and potentially in the background throughwidgetPerformUpdate(completion:)) is sufficient; should not update data inviewWillAppear(_)
-
-
Try to load/display cached data in
viewDidLoad()for smooth launch -
viewWillAppear(_)may be called while the widget is already visible on screen: If you scroll, it's not called until scrolling has finished- Anything you'd do that does not depend on the correct bounds should be done in
viewDidLoad()
- Anything you'd do that does not depend on the correct bounds should be done in
-
Updates to
widgetLargestAvailableDisplayModeare animated so you should not initialize it to one value just to set it back right afterwards, as this will cause the "Show More"/"Show Less" button to flicker for a second -
The widget process lives significantly longer than the widget VC – it may even continue to live when the widget is hidden and when doing resource-intensive stuff while the widget is off screen. This effect may be amplified when the debugger is attached, but is generally true for non-debugging use, too.
-
Although this may change over time, we observed that VCs are rarely re-used – after a few seconds off screen, the VC will be discarded, and the same widget process will create a new VC when required. However, the old VC's
dealloc/deinitis not called until the process becomes active again and creates a new VC, so you should not wait fordealloc/deinitto clean up or stop unnecessary work (like streaming data from your backgrounded app to the widget).
-
iOS 11 does not have a two-column layout on iPad
-
Widgets can have different widths depending on the orientation and the column which they are assigned to
- Portrait: All widgets in one column, equal widths
- Landscape: Widgets in two user-definable columns, left widgets are wider than right widgets
-
Widgets in the right column seem to rotate with a width change
- Instead of rotating the widget, the system creates a second instance
- The system creates two instances at once when you assign a widget to the right column, probably for snapshotting purposes
- Wait for
widgetActiveDisplayModeDidChange(_)to be called - Update the preferred content size
- Wait for
viewWillTransition(to:with:)to be called and update UI accordingly
- Make sure Data Protection is enabled for your main app
- The first time your main app starts, create a dummy file (let's call it
ProtectionMonitor.dummy) in a shared group container which is accessible from both the main app and the widget- You can create this file with
NSFileManager.default.createFile(atPath:contents:attributes:) - Make sure you pass the dictionary
{NSFileProtectionKey: NSFileProtectionComplete}as the attributes parameter. This ensures the file is protected with Data Protection.
- You can create this file with
- Every time your widget starts, try to read the file
ProtectionMonitor.dummy. If it is unreadable the device is locked. Otherwise it's not.- You may read the file with
Data(contentsOfFile:options:) - However, every other method that reads a file from disk should be fine, too
- You may read the file with
-
Presented view controllers are discouraged
- Views from presented VCs are not snapshotted, leading to weird launch behaviour
- Only the primary VC has the
extensionContextproperty set (and it'sreadonly) - Child view controller should be fine
-
If you have more than one widget, use the
UIApplicationShortcutWidgetInfo-Plist key to specify the widget (by bundle ID) which should be shown upon 3D-touching the homescreen app icon. -
Expanded display mode cannot be enforced programmatically, user must expand the widget manually
- Display mode is automatically state-preserved and restored
- Compact mode is fixed in height, although the exact value depends on the system font size
- Expanded mode is variable in height (within limits, approx. the screen height)
-
Scroll views cannot be scrolled with the default gestures, only programmatically
-
At any time, there may be zero, one, or many VC instances within one widget process
- Apart from the obvious VC that is visible, the system may create other instances, e.g., for snapshotting purposes, and new instances may be created when rotating or changing the order/position of widgets
-
Asserts silently quit the widget and do not show up in the debugger – logs are probably easier to find
-
Data Protection should not be enabled for widgets (do not set
com.apple.developer.default-data-protectionto a valid value in your widget's entitlements file). If you do, iOS will fail to create a snapshot of your widget after showing widgets in the lock screen. You can see a similar log entry in the console:ImageIO: IIOImageWriteSession:111: cannot create: '/private/var/mobile/Containers/Data/PluginKitPlugin/C4C7DD84-E4FC-4507-8D77-364970936BD8/Library/Caches/com.apple.notificationcenter/Snapshots/<Widget-Identifier>-PhoneTall-359-<Locale>-UICTContentSizeCategoryL-NCWidgetDisplayModeCompact.ca/assets/image0.ktx.sb-e8f991c7-ZpFG9m' error = 1 (Operation not permitted)In this case, Springboard will crash the next time your widget should be shown (as the snapshot is missing).