@capgo/capgo-skills
Version:
55 agent skills for Capacitor mobile development
102 lines (76 loc) • 6.5 kB
Markdown
# Native Replacements (Swift and Objective-C)
Applies to the app's own native code (`ios/App/App`, custom plugins in the app) and to plugin source you maintain.
## Rules
1. Use the information closest to the code: a view's `traitCollection` over its window's, a parameter over a property chain.
2. Never replace one global with another. `UITraitCollection.current`, `UIApplication.shared.windows`, `UIDevice.current` and `UIScreen.main` all carry the same single-window assumption.
3. Keep values dynamic. Never replace a read with a literal.
4. Keep control flow. Swap the value inside a condition; never collapse `if`/`else` branches or drop `#available`, `respondsToSelector:` or nil guards.
5. A cached value needs invalidation. If you store a trait-derived value, register for trait changes in the same edit.
6. Touch only lines that contain the target API. No opportunistic cleanup.
7. Ask before changing a public signature or a header other modules import.
## In a Capacitor plugin: where is "local"?
A `CAPPlugin` is not a view. The local context is the bridge's view controller:
```swift
guard let vc = bridge?.viewController else { return call.reject("No view controller") }
let traits = vc.traitCollection // display scale, size classes, idiom of this scene
let bounds = vc.view.bounds // the window's content area, not the screen
let scene = vc.view.window?.windowScene // this scene, for activation state or geometry
```
UI work must run on the main thread: `DispatchQueue.main.async { ... }`. Plugin methods are called on a background queue.
## `UIScreen.main`
| Use | Replacement |
|---|---|
| `UIScreen.main.scale` in a view / view controller method | `traitCollection.displayScale` |
| `UIScreen.main.scale` in a plugin | `bridge?.viewController?.traitCollection.displayScale` |
| `UIScreen.main.bounds` for sizing an overlay | The presenting view's `bounds` (`bridge?.viewController?.view.bounds`), plus Auto Layout constraints so it follows resizes |
| `UIWindow(frame: UIScreen.main.bounds)` | `UIWindow(windowScene:)` in `SceneDelegate` |
| Cached scale in a `static let` / `dispatch_once` helper | Add an overload that takes `traitCollection: UITraitCollection`; keep the old one as a deprecated wrapper that forwards (the only place where `UITraitCollection.current` is acceptable) |
| SwiftUI | `@Environment(\.displayScale)`, `GeometryReader` |
Cached value plus invalidation (iOS 17+ API; gate with `#available` on Capacitor 8 / iOS 15-16):
```swift
// in class OverlayViewController: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
overlay.layer.contentsScale = traitCollection.displayScale
if #available(iOS 17.0, *) {
registerForTraitChanges([UITraitDisplayScale.self]) { (self: OverlayViewController, _) in
self.overlay.layer.contentsScale = self.traitCollection.displayScale
}
}
}
```
Use `registerForTraitChanges` instead of overriding `traitCollectionDidChange(_:)` (deprecated in iOS 17) on iOS 17+. Keep the override as the fallback below 17. Values read fresh in `layoutSubviews`, `draw(_:)` or `viewWillLayoutSubviews` need no registration.
Native views sized from frames must follow resizes: use Auto Layout constraints to the presenting view, or recompute frames in `layoutSubviews` / `viewWillLayoutSubviews`. A frame set once at present time is wrong after the first resize.
## Orientation
Only layout uses are in scope. Camera capture orientation, video recording, and motion sensors are not.
| Pattern | Replacement |
|---|---|
| `statusBarOrientation`, `interfaceOrientation`, `UIDevice.current.orientation` to choose a layout | Size classes (`traitCollection.horizontalSizeClass == .compact`) or bounds aspect (`view.bounds.width > view.bounds.height`) |
| `windowScene.effectiveGeometry.interfaceOrientation` for layout | Same as above. A landscape-oriented window can be taller than wide |
| `windowScene(_:didUpdate:interfaceOrientation:traitCollection:)` (deprecated) | `windowScene(_:didUpdateEffectiveGeometry:)`, iOS 26+. Capacitor 8 and 9 deploy below 26: keep the old method and add a TODO. To support both, implement both methods (a larger change; ask the user) |
| Left vs right landscape, rotation transforms, animation direction | Leave the orientation source. Add a TODO explaining why, or ask the user |
## Idiom
| Pattern | Replacement |
|---|---|
| `userInterfaceIdiom == .pad` to pick a wide layout | `traitCollection.horizontalSizeClass == .regular` |
| `== .phone` to pick a narrow layout | `horizontalSizeClass == .compact` |
| Phone landscape, hide a bar | `verticalSizeClass == .compact` |
| A real device-class decision (pointer/hover affordances, a feature only on iPad) | Keep the check, but read `traitCollection.userInterfaceIdiom` from the local view, not `UIDevice.current` |
| `UI_USER_INTERFACE_IDIOM()` | Deprecated since iOS 13. Same rules |
| Analytics, per-device asset names | Out of scope. Leave as is |
The mapping is not 1:1 on purpose: an iPad in narrow Split View reports `.compact`, a large iPhone in landscape reports `.regular`.
## Key window and window lists
| Pattern | Replacement |
|---|---|
| `UIApplication.shared.keyWindow`, `.windows.first` to present something | `bridge?.viewController` (plugins), `view.window` (views) |
| `UIApplication.shared.windows.first?.safeAreaInsets` | The presenting view's `safeAreaInsets` |
| `UIApplication.shared.statusBarFrame` | `view.window?.windowScene?.statusBarManager?.statusBarFrame`, or better the safe area |
## Safe areas in native views
- Constrain content to `safeAreaLayoutGuide`, not to the superview edges. Let backgrounds extend under the bars.
- Replace hard-coded 20 / 44 / 64 / 88 / 34 / 49 / 83 pt with safe-area or layout-guide anchors.
- Never mirror one side's inset to the other side. A vertical bar can sit on one edge only, for the whole session.
- Never store an inset read once. Override `safeAreaInsetsDidChange()` or use constraints.
- `topLayoutGuide` / `bottomLayoutGuide` (deprecated) → `view.safeAreaLayoutGuide`.
- Do not hand-roll keyboard avoidance with `keyboardWillShow` frames. Use `view.keyboardLayoutGuide` (iOS 15+).
## Lifecycle
See [info-plist-and-scenes.md](info-plist-and-scenes.md). In plugins, observe notifications (`UIScene.didEnterBackgroundNotification` filtered to `bridge?.viewController?.view.window?.windowScene`, or `UIApplication.didEnterBackgroundNotification`), never the AppDelegate methods.