Files
ios/Xuxucheck/XuXuCheck/Components/XuXuWebView.swift
T
admin d6aeede2bc 导入 XuXuCheck 工程(iOS 干化学尿液试纸分析 App)
从桌面工程复制而来,作为本仓库的 Xuxucheck/ 子目录。

为控制仓库体积做了两处删减:
- 移除 Frameworks/opencv2.xcframework 下的两个 visionOS 切片
  (xros-arm64、xros-arm64-simulator,合计约 106 MB),并同步删掉该
  xcframework 内 Info.plist 里对应的两条 AvailableLibraries 记录,
  只保留 ios-arm64 与 ios-arm64_x86_64-simulator。
- 移除全部 .DS_Store 与 xcuserdata/。

已用 xcodebuild 从本目录完整编译验证:BUILD SUCCEEDED,
ProcessXCFramework 以 --platform ios 成功选中框架,产物含 3699 个 cv:: 符号
(OpenCV 确实链入),嵌入 framework 3 个,与移除无用 pod 之后的状态一致。

仓库约 206 MB,其中 OpenCV 203 MB、其余源码与依赖约 3 MB。
2026-09-23 14:17:31 +08:00

204 lines
8.7 KiB
Swift

//
// XuXuWebView.swift
// XuXuCheck
//
// Created by henseycai on 2025/6/25.
//
import Foundation
import SwiftUI
struct XuXuWebView: View {
// @Bindable var webViewModel = WebViewModel()
var currentUrl: String
@Environment(\.dismiss) private var dissmiss
@Environment(LoginStatus.self) var loginStatus
@Environment(WebViewModel.self) var webViewModel
@Environment(Status.self) var status
@State private var showingNaiveSafariViewFullScreenCover = false
@State var imageViewModel = ImagePickerViewModel()
@State var iScanQrPresented = false
@State private var scannedCode: String?
/// 顶部留白高度(pt)。
/// 网页的头部是按"浏览器里已经让开状态栏"来设计的,把 webview 直接铺到物理顶端(y=0)后,
/// 它的标题会顶进灵动岛,左上角的返回键也会落进系统手势区里导致点不到。
/// 这里稍微下移一点点:标题和返回键都能落在灵动岛下方,又不像原来那样露出一大条蓝色空白。
/// 调整手感只需要改这一个数值。
private let topSafeInset: CGFloat = 20
let actions = ["changeText('hello from Swift')", "goBack()"]
static var date: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: Date())
}
var body: some View {
ZStack{
// 网页的底色层:必须铺满四周(含底部)。
// 它就是 webview 后面那层背景,底部漏掉的话,webview 停在安全区上时
// 最下面 34pt 露出的会是 XuXuWebView 外面父视图的白底,在网页浅色背景下面很显眼。
// 键盘那一路单独忽略,是为了键盘弹出时这层底色不被顶起来。
Color(hex: 0x395cff).ignoresSafeArea(.keyboard,edges: .bottom)
.ignoresSafeArea(.container,edges: [.top,.horizontal,.bottom])
VStack {
WebViewRepresentable(currentUrl: currentUrl)
.cornerRadius(1)
.onChange(of: webViewModel.hasNewCommand) {
oldState,
newState in
print(webViewModel.hasNewCommand)
if oldState == false && newState == true {
//https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange
//https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)
switch webViewModel.jsCommand?.lowercased() {
case "print":
break
case "tel":
break
case "clearcache":
break
case "setdefaultmember":
break
case "changeself":
break
case "goback":
// print(" go back")
status.showNavDetail = false
// status.showNavDetail.toggle()
dissmiss()
break
case "scancode":
iScanQrPresented.toggle()
break
case "selectphoto":
print("i will select photo")
imageViewModel.choosePhoto()
break
case "selectcamera":
print("i will take photo")
imageViewModel.takePhoto()
break
case "scanpaper":
showingNaiveSafariViewFullScreenCover.toggle()
break
case "logout":
print("logout")
// 网页里退出登录后要退出本页回到 Mine,和 goback 一样属"正常离开",
// 所以这里立刻把导航条恢复;onDisappear 那层是兜底,
// 万一将来又加一条不以 goback/logout 结束的退出路径也不会漏。
status.showNavDetail = false
dissmiss()
break
case "login":
break
default:
break
}
webViewModel.hasNewCommand = false
}
}
.onChange(of: imageViewModel.isPresentingImagePicker){
oldState, newState in
if oldState == true && newState == false {
if let image = imageViewModel.selectedImage {
// webViewModel.callJsImage(imageString: image.jpegBase64)
if let base64 = image.getJpegBase64(){
webViewModel.callJsImage(imageString: base64)
}
//webViewModel.callJsImage(imageString: image.getJpegBase64())
// webViewModel.callJsImage(imageString: convertImageToBase64(image: image))
}
}
}
.fullScreenCover(isPresented: $imageViewModel.isPresentingImagePicker, content: {
ImagePicker(sourceType: imageViewModel.sourceType, completionHandler: imageViewModel.didSelectImage)
})
.fullScreenCover(
isPresented: $showingNaiveSafariViewFullScreenCover
) {
ScanView()
}
.sheet(isPresented: $iScanQrPresented) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
if let code = scannedCode {
webViewModel.callJsBarcode(named: code)
}
iScanQrPresented = false
}
}
}
}
// 顶部(半通屏):先把内容下移 topSafeInset,再让容器铺到物理顶端。
// 顺序不能反 —— 先忽略安全区再加 .padding,等于给一个已经满屏的视图加外衬距,底部会被挤出容器。
// 这样网页从灵动岛下方开始渲染,上面露出的那一小条就是本页的蓝色底。
// 底部(全通屏):必须一直铺到物理底边。原先这里只忽略 .top,webview 停在底部安全区上,
// 最下面 34pt 没有任何一层去画,露出的是父视图的白底。
// 顶部和底部的现象都是"留白",但成因相反:顶部是让位让多了(要收),底部是压根没铺到(要铺满)。
// 判断依据是这块区域显示的是谁的颜色,详见 docs/webview-safe-area.md
.padding(.top, topSafeInset)
.ignoresSafeArea(.container, edges: [.top, .bottom])
// 底部导航的藏/显示必须成对绑在本视图的生命周期上。
// 注意 showNavDetail 名字是反的:true = 正在看详情页 = 把 TabBar 推出屏幕
// (ContentView 里 `TabBar().offset(y: showNavDetail ? 200 : 0)`)。
// 原先只有 onAppear 这一半,"恢复"是由网页上报 goback 命令顺带做的 ——
// 于是任何不以 goback 结束的退出路径都会把导航条留在屏幕外
// (Mine → 设置 → 退出登录 就是:那个分支只 dismiss,没有恢复)。
.onAppear {
//status.showNavDetail.toggle()
status.showNavDetail = true
}
.onDisappear {
status.showNavDetail = false
}
}
// .frame(maxWidth: .infinity,maxHeight: .infinity)
// .edgesIgnoringSafeArea(.all)
}
}
#Preview {
XuXuWebView(currentUrl: API.webrecordUrl + GetGeneralRequestParams())
}