Skip to content

fix(ios): refactor screen webview lifecycle handling#20366

Merged
ngutman merged 3 commits intomainfrom
fix/ios-screen-webview-lifecycle
Feb 18, 2026
Merged

fix(ios): refactor screen webview lifecycle handling#20366
ngutman merged 3 commits intomainfrom
fix/ios-screen-webview-lifecycle

Conversation

@ngutman
Copy link
Contributor

@ngutman ngutman commented Feb 18, 2026

Summary

  • refactor iOS screen rendering so ScreenController no longer owns a long-lived WKWebView
  • move WKWebView creation, delegate wiring, script handler setup, and teardown into ScreenWebViewCoordinator
  • keep ScreenController focused on state/navigation/debug commands and attach/detach a weak active web view
  • update ScreenControllerTests to mount via coordinator-owned web view lifecycle

Why

We were seeing crashes around gesture handlers and __NSArrayM insertObject:atIndex:. Reusing a shared WKWebView instance across SwiftUI lifecycle transitions can lead to unstable UIKit/WebKit internals. This change aligns ownership with UIViewRepresentable lifecycle (makeUIView/dismantleUIView) and ensures delegates/script handlers are cleaned up deterministically.

Validation

  • xcodebuild build -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,OS=18.6,name=iPhone 16'
  • xcodebuild test ... -only-testing:OpenClawTests/ScreenControllerTests could not run to completion because of an unrelated existing compile failure in GatewayConnectionSecurityTests (GatewayTLSStore not found)

Greptile Summary

This PR refactors the iOS ScreenController / ScreenWebView lifecycle so that WKWebView creation, delegate wiring, and teardown are owned by a new ScreenWebViewCoordinator, aligned with the UIViewRepresentable lifecycle (makeUIView/dismantleUIView). This fixes crashes related to gesture handlers and __NSArrayM insertObject:atIndex: caused by reusing a shared WKWebView across SwiftUI lifecycle transitions.

  • ScreenController no longer owns a WKWebView; it holds a weak reference set via attachWebView/detachWebView
  • ScreenWebViewCoordinator handles web view creation, AutoLayout, navigation delegate wiring, A2UI script handler install/teardown
  • ScreenNavigationDelegate and CanvasA2UIActionMessageHandler moved from ScreenController.swift to ScreenWebView.swift (logic unchanged)
  • Tests updated to use the coordinator-based mount lifecycle with proper teardown() in defer blocks
  • Clean separation of concerns: ScreenController handles state/navigation/debug, coordinator handles UIKit/WebKit lifecycle

Confidence Score: 4/5

  • This PR is safe to merge — it's a well-structured ownership refactor with no behavioral changes to delegate/handler logic.
  • The refactor cleanly separates WebView lifecycle ownership from state management. Delegate and handler logic is moved verbatim. The weak/strong reference patterns are correct for the UIViewRepresentable lifecycle. Tests are updated to exercise the new coordinator path. Minor concern: the reload() call in init() is now dead code but harmless.
  • No files require special attention. The ScreenWebView.swift coordinator is the most significant new code but follows standard UIViewRepresentable patterns.

Last reviewed commit: 28452d9

@openclaw-barnacle openclaw-barnacle bot added app: ios App: ios size: M maintainer Maintainer-authored PR labels Feb 18, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 24 to 26
init() {
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
let a2uiActionHandler = CanvasA2UIActionMessageHandler()
let userContentController = WKUserContentController()
for name in CanvasA2UIActionMessageHandler.handlerNames {
userContentController.add(a2uiActionHandler, name: name)
}
config.userContentController = userContentController
self.navigationDelegate = ScreenNavigationDelegate()
self.a2uiActionHandler = a2uiActionHandler
self.webView = WKWebView(frame: .zero, configuration: config)
// Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays.
self.webView.isOpaque = true
self.webView.backgroundColor = .black
self.webView.scrollView.backgroundColor = .black
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
self.webView.scrollView.contentInset = .zero
self.webView.scrollView.scrollIndicatorInsets = .zero
self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.applyScrollBehavior()
self.webView.navigationDelegate = self.navigationDelegate
self.navigationDelegate.controller = self
a2uiActionHandler.controller = self
self.reload()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-op reload() in init()
self.reload() in init() is now effectively dead code — activeWebView is always nil at construction time, so both applyScrollBehavior() and the web-view guard inside reload() early-return immediately. The real reload happens when attachWebView(_:) is called later. Consider removing this call to avoid confusion.

Suggested change
init() {
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
let a2uiActionHandler = CanvasA2UIActionMessageHandler()
let userContentController = WKUserContentController()
for name in CanvasA2UIActionMessageHandler.handlerNames {
userContentController.add(a2uiActionHandler, name: name)
}
config.userContentController = userContentController
self.navigationDelegate = ScreenNavigationDelegate()
self.a2uiActionHandler = a2uiActionHandler
self.webView = WKWebView(frame: .zero, configuration: config)
// Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays.
self.webView.isOpaque = true
self.webView.backgroundColor = .black
self.webView.scrollView.backgroundColor = .black
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
self.webView.scrollView.contentInset = .zero
self.webView.scrollView.scrollIndicatorInsets = .zero
self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.applyScrollBehavior()
self.webView.navigationDelegate = self.navigationDelegate
self.navigationDelegate.controller = self
a2uiActionHandler.controller = self
self.reload()
}
init() {
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/Sources/Screen/ScreenController.swift
Line: 24-26

Comment:
**No-op `reload()` in `init()`**
`self.reload()` in `init()` is now effectively dead code — `activeWebView` is always nil at construction time, so both `applyScrollBehavior()` and the web-view guard inside `reload()` early-return immediately. The real reload happens when `attachWebView(_:)` is called later. Consider removing this call to avoid confusion.

```suggestion
    init() {
    }
```

How can I resolve this? If you propose a fix, please make it concise.

@ngutman ngutman force-pushed the fix/ios-screen-webview-lifecycle branch from 54839a8 to 49ba1c6 Compare February 18, 2026 21:02
@ngutman ngutman force-pushed the fix/ios-screen-webview-lifecycle branch from 49ba1c6 to 7beb794 Compare February 18, 2026 21:04
@ngutman ngutman merged commit dd28a77 into main Feb 18, 2026
10 checks passed
@ngutman ngutman deleted the fix/ios-screen-webview-lifecycle branch February 18, 2026 21:05
@ngutman
Copy link
Contributor Author

ngutman commented Feb 18, 2026

Merged via maintainer flow /review-pr -> /prepare-pr -> /merge-pr.

Details:

  • Prepared head SHA: 7beb794a066309c53dbd85349816fd5eb92ce810
  • Merge commit SHA: dd28a77df0efc9bd94cd1726632f6c9a8a1d8178
  • Rebased onto latest main during prepare
  • Ran gates in prep worktree: pnpm build, pnpm check, pnpm test
  • Added required changelog entry before merge

anschmieg pushed a commit to anschmieg/openclaw that referenced this pull request Feb 19, 2026
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7beb794
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
yneth-ray-openclaw pushed a commit to yneth-ray-openclaw/openclaw that referenced this pull request Feb 19, 2026
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7beb794
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
HenryChenV pushed a commit to HenryChenV/openclaw that referenced this pull request Feb 20, 2026
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7beb794
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: ios App: ios maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments