Problem
When entering a canvas screen on iPad, the PKToolPicker initially appeared with its collapsed/minimized circular button showing no tool icon — just a blank black circle.
The icon only appeared correctly after the user manually expanded the picker, selected or changed a tool, and collapsed it again.
Scope
- Multiple
PKCanvasViewinstances across several screens in the app - All canvases share a single
PKToolPickerinstance
Root Cause
The PKToolPicker's internal collapsed view does not perform an initial layout pass when shown programmatically. Even after calling setVisible(_:forFirstResponder:), the layout needed to render the tool icon in the collapsed state is not automatically triggered — an internal Apple framework behavior.
Solution
The fix combined two changes:
1. UIViewRepresentable → UIViewControllerRepresentable
Changed the canvas wrapper view from UIViewRepresentable to UIViewControllerRepresentable to leverage the viewDidAppear lifecycle. Following Apple's WWDC19 recommended pattern, the tool picker is set up in viewDidAppear.
Reason: UIViewRepresentable's makeUIView/updateUIView can be called before the view is fully added to the window, making picker initialization timing unreliable.
2. Force Redraw via Window Hierarchy Traversal
After the picker is shown, the window hierarchy is traversed on the next run loop cycle to find picker-related internal views and force layout/display.
private static func forcePickerRedraw(in view: UIView) {
let className = String(describing: type(of: view))
if className.contains("Picker") || className.contains("Floating") || className.contains("Palette") {
view.setNeedsLayout()
view.layoutIfNeeded()
view.setNeedsDisplay()
for subview in view.subviews {
subview.setNeedsLayout()
subview.layoutIfNeeded()
subview.setNeedsDisplay()
}
}
for subview in view.subviews {
forcePickerRedraw(in: subview)
}
}
Call timing: Invoked via DispatchQueue.main.async after becomeFirstResponder(), delaying by one run loop cycle so that the picker has been added to the window before forcing layout.
picker.setVisible(isToolPickerVisible, forFirstResponder: canvasView)
if isToolPickerVisible {
canvasView.becomeFirstResponder()
DispatchQueue.main.async { [weak self] in
guard let window = self?.canvasView.window else { return }
Self.forcePickerRedraw(in: window)
}
}
Approaches That Didn't Work
| Approach | Result |
|---|---|
Setting selectedToolItemIdentifier | Not reflected in the collapsed icon |
Assigning PKToolPickerInkingItem to selectedToolItem | Internal tool selected but collapsed icon not rendered |
stateAutosaveName = nil | Only prevented collapsed state restoration; icon issue remained |
PKCanvasView subclass + didMoveToWindow() | Unstable behavior |
| Toggle button for Picker on/off | Not a root fix; blank circle still shown on toggle |
becomeFirstResponder() → setVisible() order | Apple recommends the opposite order; reordering alone didn't fix it |
UIViewControllerRepresentable + viewDidAppear only | Improved timing but collapsed icon still not displayed |
Changes Summary
- Created a dedicated
UIViewControllersubclass as part of converting the canvas wrapper toUIViewControllerRepresentable - Added
forcePickerRedraw(in:)method
Key Takeaways
PKToolPicker's collapsed view does not perform its own initial layout when shown programmatically- Setting up the picker in
viewDidAppearis the correct timing, but it alone is not enough - You must find picker-related views in the window hierarchy and force
setNeedsLayout/layoutIfNeeded/setNeedsDisplayfor the collapsed icon to render - This forced layout must run on the next run loop after
becomeFirstResponder()to be effective