mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
* perf(mobile): answer a page's ready that arrived before its bridge host A page's ready can reach the shell before the screen has built its bridge host (the host waits on the client and the host profile read). It used to be dropped, so init waited for the page's next retry, up to 2 s later. Keep the latest ready per session and answer it once the host exists. * perf(mobile): keep one prewarmed page document while Home is shown While native Home is on screen, boot one page document for the most recently connected host with a cached build, parked invisibly in the window. Opening a page from that build adopts it instead of building a WebView, which removes the WebView start and script parse from the tap. The spare is dropped when Home is left without a claim (2 s grace), on background, memory pressure, its own failure and before the cache deletes its files. It is never kept beside a shown page, and only phones with more than 3 GiB of RAM (4 GB phones) get one. A parked page is told to stop asking for init; the adopting view answers the ready it sent. Builds on Jinwoo's spare pool and replay work in #32203 and #32204. Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com> * fix(mobile): unregister the shell's memory callbacks from the context that registered them --------- Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
112 lines
3.8 KiB
Swift
112 lines
3.8 KiB
Swift
import ExpoModulesCore
|
|
import WebKit
|
|
|
|
/// React Native's handle on one document: it lays the WebView out and owns the back-edge swipe.
|
|
final class OrcaMobileWebShellView: ExpoView, MobileWebShellDocumentSink,
|
|
UIGestureRecognizerDelegate {
|
|
let onLoadState = EventDispatcher()
|
|
let onBridgeMessage = EventDispatcher()
|
|
let onExternalNavigation = EventDispatcher()
|
|
let onBackEdgeSwipe = EventDispatcher()
|
|
|
|
// The page's own Back: the stack's swipe is off while the page claims Back, and a strip overlay would eat taps.
|
|
private let backEdgePan = UIPanGestureRecognizer()
|
|
/// Taken on the first prop commit, not in init: the session id says whether a spare was booted for it.
|
|
private var document: MobileWebShellDocument?
|
|
private var generationDirectory = ""
|
|
private var sessionId = ""
|
|
private var bridgeEnabled = false
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
document?.loadWhenIsolated()
|
|
}
|
|
|
|
required init(appContext: AppContext? = nil) {
|
|
super.init(appContext: appContext)
|
|
backEdgePan.isEnabled = false
|
|
backEdgePan.addTarget(self, action: #selector(handleBackEdgePan(_:)))
|
|
backEdgePan.delegate = self
|
|
addGestureRecognizer(backEdgePan)
|
|
}
|
|
|
|
private func hostDocument() -> MobileWebShellDocument {
|
|
let document = MobileWebShellSparePool.shared.document(for: sessionId, sink: self)
|
|
let webView = document.webView
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(webView)
|
|
NSLayoutConstraint.activate([
|
|
webView.topAnchor.constraint(equalTo: topAnchor),
|
|
webView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
webView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
webView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
])
|
|
webView.scrollView.panGestureRecognizer.require(toFail: backEdgePan)
|
|
return document
|
|
}
|
|
|
|
func setBackEdgeSwipe(_ value: Bool) {
|
|
backEdgePan.isEnabled = value
|
|
}
|
|
|
|
/// Only touches that start at the shell's left edge; a landscape safe area insets it from the screen's.
|
|
func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
recognizer !== backEdgePan || touch.location(in: self).x <= 20
|
|
}
|
|
|
|
override func gestureRecognizerShouldBegin(_ recognizer: UIGestureRecognizer) -> Bool {
|
|
guard recognizer === backEdgePan else { return super.gestureRecognizerShouldBegin(recognizer) }
|
|
let velocity = backEdgePan.velocity(in: self)
|
|
return velocity.x > abs(velocity.y)
|
|
}
|
|
|
|
/// Commit like the stack's own swipe: past half the width, or a rightward flick.
|
|
@objc private func handleBackEdgePan(_ pan: UIPanGestureRecognizer) {
|
|
guard pan.state == .ended else { return }
|
|
let travel = pan.translation(in: self).x
|
|
let speed = pan.velocity(in: self).x
|
|
if speed > 300 || (speed > -300 && travel > bounds.width / 2) {
|
|
onBackEdgeSwipe()
|
|
}
|
|
}
|
|
|
|
func setGenerationDirectory(_ value: String) {
|
|
generationDirectory = value
|
|
}
|
|
|
|
func setSessionId(_ value: String) {
|
|
sessionId = value
|
|
}
|
|
|
|
func setBridgeEnabled(_ value: Bool) {
|
|
bridgeEnabled = value
|
|
}
|
|
|
|
func propsDidUpdate() {
|
|
let document = self.document ?? hostDocument()
|
|
self.document = document
|
|
document.apply(
|
|
generationDirectory: generationDirectory,
|
|
sessionId: sessionId,
|
|
bridgeEnabled: bridgeEnabled
|
|
)
|
|
}
|
|
|
|
func postBridgeMessage(_ json: String, promise: Promise) throws {
|
|
guard let document else { throw MobileWebShellBridgeUnavailableException() }
|
|
try document.postBridgeMessage(json, promise: promise)
|
|
}
|
|
|
|
func documentLoadState(_ payload: [String: Any]) {
|
|
onLoadState(payload)
|
|
}
|
|
|
|
func documentBridgeMessage(_ json: String) {
|
|
onBridgeMessage(["json": json])
|
|
}
|
|
|
|
func documentExternalNavigation(_ url: String) {
|
|
onExternalNavigation(["url": url])
|
|
}
|
|
}
|