Files
Innei a9bb77167e refactor(webview): ios preload webview module
- Refactored WebViewManager to improve lifecycle management and state handling.
- Introduced a debug panel for monitoring WebView state and actions in development mode.
- Updated JavaScript injection methods to use a centralized dispatch system for better maintainability.
- Improved handling of local HTML loading and state replay after process termination.
- Added new functions for debugging, including state retrieval and flushing pending scripts.

This update aims to enhance the overall performance and usability of the WebView component, providing developers with better tools for debugging and state management.

Signed-off-by: Innei <tukon479@gmail.com>
2025-08-26 22:20:57 +08:00

205 lines
7.0 KiB
Swift

//
// HelperModule.swift
// Pods
//
// Created by Innei on 2025/2/7.
//
import ExpoModulesCore
import UIKit
public class HelperModule: Module {
public func definition() -> ExpoModulesCore.ModuleDefinition {
Name("Helper")
AsyncFunction("openLink") { (urlString: String, promise: Promise) in
guard let url = URL(string: urlString) else {
return
}
DispatchQueue.main.async {
guard let rootVC = Utils.getRootVC() else { return }
let onDismiss = {
promise.resolve(["type": "dismiss"])
}
WebViewManager.presentModalWebView(url: url, from: rootVC, onDismiss: onDismiss)
}
}
Function("scrollToTop") { (reactTag: Int) in
DispatchQueue.main.async { [weak self] in
guard let bridge = self?.appContext?.reactBridge else { return }
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
let scrollView = self?.findUIScrollView(view: sourceView)
guard let scrollView = scrollView else {
return
}
scrollView.scrollToTopIfPossible(animated: true)
}
}
}
AsyncFunction("isScrollToEnd") { (reactTag: Int, promise: Promise) in
DispatchQueue.main.async { [weak self] in
guard let bridge = self?.appContext?.reactBridge else { return }
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
let scrollView = self?.findUIScrollView(view: sourceView)
guard let scrollView = scrollView else {
return
}
let contentHeight = scrollView.contentSize.height
let scrollViewHeight = scrollView.bounds.size.height
let contentOffsetY = scrollView.contentOffset.y
let bottomOffset = contentHeight - scrollViewHeight
promise.resolve(contentOffsetY >= bottomOffset - 1.0)
}
}
}
Function("saveImageByHandle") { (reactTag: Int) in
DispatchQueue.main.async { [weak self] in
guard let bridge = self?.appContext?.reactBridge else { return }
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
return
}
guard let image = imageView.image else { return }
UIImageWriteToSavedPhotosAlbum(image, self, #selector(HelperModule.image), nil)
Toast.show(options: .init(type: .success, title: "Saved to photos"))
}
}
}
Function("shareImageByHandle") { (reactTag: Int, url: String?) in
DispatchQueue.main.async { [weak self] in
guard let bridge = self?.appContext?.reactBridge else { return }
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
return
}
guard let image = imageView.image else { return }
let activityViewController = UIActivityViewController(
activityItems: [
image.asActivityItemSource(
url: try? URL(string: url ?? "")
)
], applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = sourceView
activityViewController.popoverPresentationController?.sourceRect = sourceView.bounds
activityViewController.popoverPresentationController?.permittedArrowDirections = .any
activityViewController.popoverPresentationController?.permittedArrowDirections = .any
Utils.getRootVC()?.present(activityViewController, animated: true)
}
}
}
AsyncFunction("getBase64FromImageViewByHandle") { (reactTag: Int, promise: Promise) in
DispatchQueue.main.async { [weak self] in
guard let bridge = self?.appContext?.reactBridge else { return }
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
promise.reject(
NSError(
domain: "HelperModule", code: 0,
userInfo: [NSLocalizedDescriptionKey: "Image view not found"]))
return
}
let base64 = self?.getBase64FromImageView(imageView: imageView)
promise.resolve(["base64": base64])
}
}
}
Function("copyImageByHandle") { (reactTag: Int) in
DispatchQueue.main.async { [weak self] in
guard let bridge = self?.appContext?.reactBridge else { return }
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
return
}
guard let image = imageView.image else { return }
guard let imageData = image.pngData() else { return }
UIPasteboard.general.setData(imageData, forPasteboardType: "public.png")
Toast.show(options: .init(type: .success, title: "Image copied to clipboard"))
}
}
}
}
func getBase64FromImageView(imageView: UIImageView) -> String? {
guard let image = imageView.image else { return nil }
guard let imageData = image.pngData() else { return nil }
let base64String = imageData.base64EncodedString(options: .lineLength64Characters)
return base64String
}
@objc func image(
_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer?
) {
if let error = error {
Toast.show(options: .init(type: .error, title: "Save image failed"))
} else {
Toast.show(options: .init(type: .success, title: "Saved to photos"))
}
}
private func findUIScrollView(view: UIView?) -> UIScrollView? {
return findUIViewOfType(view: view)
}
private func findUIImageView(view: UIView?) -> UIImageView? {
return findUIViewOfType(view: view)
}
private func findUIViewOfType<T: UIView>(view: UIView?) -> T? {
guard let view = view else {
return nil
}
if let view = view as? T {
return view
}
let subviews = view.subviews
for subview in subviews {
if let targetView = findUIViewOfType(view: subview) as T? {
return targetView
}
}
return nil
}
}
extension UIScrollView {
func scrollToTopIfPossible(animated: Bool) {
let encodedSelector = "X3Njcm9sbFRvVG9wSWZQb3NzaWJsZTo=" // "_scrollToTopIfPossible:"
if let decodedData = Data(base64Encoded: encodedSelector),
let decodedString = String(data: decodedData, encoding: .utf8)
{
let selector = NSSelectorFromString(decodedString)
if responds(to: selector) {
perform(selector, with: animated)
} else {
print("UIScrollView does not respond to decoded method")
setContentOffset(.zero, animated: animated)
}
} else {
setContentOffset(.zero, animated: animated)
}
}
}