导入 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。
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// BlobView.swift
|
||||
// XuXuCheck
|
||||
//
|
||||
// Created by henseycai on 2025/6/11.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BlobView: View {
|
||||
@State var appear = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
TimelineView(.animation) { timeline in
|
||||
let now = timeline.date.timeIntervalSinceReferenceDate
|
||||
let angle = Angle.degrees(now.remainder(dividingBy: 3) * 50)
|
||||
let x = cos(angle.radians)
|
||||
let angle2 = Angle.degrees(now.remainder(dividingBy: 6) * 10)
|
||||
let x2 = cos(angle2.radians);
|
||||
|
||||
Canvas { context, size in
|
||||
context.fill(path(in: CGRect(x: 0, y: 0, width: size.width, height: size.height), x: x, x2: x2), with: .linearGradient(Gradient(colors: [.pink,.blue]), startPoint: CGPoint(x: 0, y: 0), endPoint: CGPoint(x: 390, y: 390)))
|
||||
}
|
||||
.frame(width: 400, height: 410)
|
||||
.rotationEffect(.degrees(appear ? 360 : 30))
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.linear(duration: 15).repeatCount(4, autoreverses: true)) {
|
||||
appear = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// .foregroundStyle(.linearGradient(colors: [.pink, .blue], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
|
||||
|
||||
func path(in rect: CGRect, x: Double, x2: Double) -> Path {
|
||||
var path = Path()
|
||||
let width = rect.size.width
|
||||
let height = rect.size.height
|
||||
path.move(to: CGPoint(x: 0.9923*width, y: 0.42593*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6355*width, y: height), control1: CGPoint(x: 0.92554*width*x2, y: 0.77749*height*x2), control2: CGPoint(x: 0.91864*width*x2, y: height))
|
||||
path.addCurve(to: CGPoint(x: 0.08995*width, y: 0.60171*height), control1: CGPoint(x: 0.35237*width*x, y: height), control2: CGPoint(x: 0.2695*width, y: 0.77304*height))
|
||||
path.addCurve(to: CGPoint(x: 0.34086*width, y: 0.06324*height*x), control1: CGPoint(x: -0.0896*width, y: 0.43038*height), control2: CGPoint(x: 0.00248*width, y: 0.23012*height*x))
|
||||
path.addCurve(to: CGPoint(x: 0.9923*width, y: 0.42593*height), control1: CGPoint(x: 0.67924*width, y: -0.10364*height*x), control2: CGPoint(x: 1.05906*width, y: 0.07436*height*x2))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
struct BlobView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BlobView()
|
||||
}
|
||||
}
|
||||
|
||||
struct BlobShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let width = rect.size.width
|
||||
let height = rect.size.height
|
||||
path.move(to: CGPoint(x: 0.9923*width, y: 0.42593*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6355*width, y: height), control1: CGPoint(x: 0.92554*width, y: 0.77749*height), control2: CGPoint(x: 0.91864*width, y: height))
|
||||
path.addCurve(to: CGPoint(x: 0.08995*width, y: 0.60171*height), control1: CGPoint(x: 0.35237*width, y: height), control2: CGPoint(x: 0.2695*width, y: 0.77304*height))
|
||||
path.addCurve(to: CGPoint(x: 0.34086*width, y: 0.06324*height), control1: CGPoint(x: -0.0896*width, y: 0.43038*height), control2: CGPoint(x: 0.00248*width, y: 0.23012*height))
|
||||
path.addCurve(to: CGPoint(x: 0.9923*width, y: 0.42593*height), control1: CGPoint(x: 0.67924*width, y: -0.10364*height), control2: CGPoint(x: 1.05906*width, y: 0.07436*height))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// HexagonView.swift
|
||||
// XuXuCheck
|
||||
//
|
||||
// Created by henseycai on 2025/6/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HexagonView: View {
|
||||
var body: some View {
|
||||
Image(systemName: "hexagon")
|
||||
.symbolVariant(.fill)
|
||||
.foregroundColor(.blue)
|
||||
.font(.system(size: 200))
|
||||
}
|
||||
}
|
||||
|
||||
struct HexagonView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HexagonView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
//
|
||||
// ScanView.swift
|
||||
// XuXuCheck
|
||||
//
|
||||
// Created by henseycai on 2025/6/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@preconcurrency
|
||||
import WebKit
|
||||
|
||||
struct WebViewRepresentable: UIViewRepresentable {
|
||||
|
||||
var currentUrl: String
|
||||
|
||||
@Environment(WebViewModel.self) var webViewModel
|
||||
|
||||
// let viewModel: WebViewModel
|
||||
let webView = WKWebView()
|
||||
@Environment(LoginStatus.self) var loginStatus
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
webView.navigationDelegate = context.coordinator
|
||||
|
||||
// 让网页从 webView 自己的顶端开始渲染。默认的 .automatic 会在 webView 铺到状态栏
|
||||
// 下面之后再加一份安全区 inset,内容被二次下推,顶部就多出一条空白带。
|
||||
// 它同时决定底部:内容区不再被安全区收窄,所以底部也不会自动让出 34pt,
|
||||
// 滚动内容会一直用到 webView 的物理底边。
|
||||
// 顶部到底留多少由 XuXuWebView 的 topSafeInset 统一控制;推导见 docs/webview-safe-area.md
|
||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
|
||||
if(currentUrl.isEmpty == false){
|
||||
webViewModel.hasNewCommand = false
|
||||
webViewModel.url = URL(string:currentUrl)
|
||||
}
|
||||
|
||||
if let url = webViewModel.url {
|
||||
webView.load(URLRequest(url: url))
|
||||
}
|
||||
webViewModel.assignWebView(webView: webView)
|
||||
return webView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: WKWebView, context: Context) {
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
|
||||
var parent: WebViewRepresentable
|
||||
|
||||
var webView: WKWebView {
|
||||
parent.webView
|
||||
}
|
||||
var webViewModel: WebViewModel {
|
||||
parent.webViewModel
|
||||
}
|
||||
|
||||
init(_ parent: WebViewRepresentable) {
|
||||
self.parent = parent
|
||||
super.init()
|
||||
|
||||
// Add addScriptMessageHandler in javascript: window.webkit.messageHandlers.MyObserver.postMessage()
|
||||
webView.configuration.userContentController.add(self, name: "receiveData")
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if navigationAction.navigationType == .linkActivated {
|
||||
if let url = navigationAction.request.url {
|
||||
webView.load(URLRequest(url: url))
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
}
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
webViewModel.isLoading = true
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
webViewModel.isLoading = false
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
webViewModel.isLoading = false
|
||||
webViewModel.error = error
|
||||
print("loading error: \(error)")
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
let messageFromWebPage = message.body as! String
|
||||
|
||||
let jsonData = messageFromWebPage.data(using: .utf8)!
|
||||
|
||||
print(jsonData)
|
||||
|
||||
do {
|
||||
let js2obj = try JSONDecoder().decode(JS2SwiftModel.self, from: jsonData)
|
||||
webViewModel.jsParameter = js2obj.parameter
|
||||
webViewModel.jsCommand = js2obj.command
|
||||
|
||||
webViewModel.hasNewCommand = true
|
||||
|
||||
print(js2obj)
|
||||
|
||||
print(webViewModel.jsCommand?.lowercased())
|
||||
|
||||
switch webViewModel.jsCommand?.lowercased(){
|
||||
case "print":
|
||||
|
||||
break
|
||||
case "tel":
|
||||
|
||||
break
|
||||
case "clearcache":
|
||||
|
||||
break
|
||||
case "setdefaultmember":
|
||||
if let member_data = webViewModel.jsParameter?.member_data {
|
||||
Storage.shared.member_id = member_data.member_id ?? 0
|
||||
Storage.shared.member_name = member_data.member_name ?? ""
|
||||
}
|
||||
break
|
||||
case "changeself":
|
||||
if let dataSelf = webViewModel.jsParameter?.jsSelf {
|
||||
Storage.shared.userid = dataSelf.user_id ?? 0
|
||||
Storage.shared.mobile = dataSelf.user_mobile ?? ""
|
||||
Storage.shared.email = dataSelf.user_email ?? ""
|
||||
Storage.shared.username = dataSelf.username ?? ""
|
||||
}
|
||||
break
|
||||
case "goback":
|
||||
|
||||
|
||||
|
||||
break
|
||||
case "scanpaper":
|
||||
|
||||
|
||||
break
|
||||
case "logout":
|
||||
Storage.shared.email = ""
|
||||
Storage.shared.username = ""
|
||||
Storage.shared.isLogin = false
|
||||
Storage.shared.member_id = 0
|
||||
Storage.shared.mobile = ""
|
||||
Storage.shared.userid = 0
|
||||
Storage.shared.usertoken = ""
|
||||
Storage.shared.member_name = ""
|
||||
|
||||
//can do this
|
||||
parent.loginStatus.isLogin = false
|
||||
|
||||
break
|
||||
case "login":
|
||||
Storage.shared.email = ""
|
||||
Storage.shared.username = ""
|
||||
Storage.shared.isLogin = false
|
||||
Storage.shared.member_id = 0
|
||||
Storage.shared.mobile = ""
|
||||
Storage.shared.userid = 0
|
||||
Storage.shared.usertoken = ""
|
||||
Storage.shared.member_name = ""
|
||||
|
||||
//can do this
|
||||
parent.loginStatus.routeToLoginView = true
|
||||
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
|
||||
}catch{
|
||||
print("error decoding json:\(error)")
|
||||
}
|
||||
|
||||
|
||||
// if messageFromWebPage == "index" {
|
||||
// viewModel.backButtonIsEnabled = false
|
||||
// viewModel.loadFile(name: messageFromWebPage)
|
||||
// return
|
||||
// }
|
||||
// if messageFromWebPage == "goPage" {
|
||||
// viewModel.backButtonIsEnabled = true
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let alertController = UIAlertController(title: "Javascript said:", message: messageFromWebPage, preferredStyle: .alert)
|
||||
// let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
|
||||
// print("OK")
|
||||
// }
|
||||
// alertController.addAction(okAction)
|
||||
// guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
// let rootViewController = windowScene.windows.first?.rootViewController else {
|
||||
// return
|
||||
// }
|
||||
// rootViewController.present(alertController, animated: true)
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Callback from javascript: window.webkit.messageHandlers.MyObserver.postMessage(message)
|
||||
// func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
// let messageFromWebPage = message.body as! String
|
||||
//
|
||||
// if messageFromWebPage == "index" {
|
||||
// viewModel.backButtonIsEnabled = false
|
||||
// viewModel.loadFile(name: messageFromWebPage)
|
||||
// return
|
||||
// }
|
||||
// if messageFromWebPage == "goPage" {
|
||||
// viewModel.backButtonIsEnabled = true
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let alertController = UIAlertController(title: "Javascript said:", message: messageFromWebPage, preferredStyle: .alert)
|
||||
// let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
|
||||
// print("OK")
|
||||
// }
|
||||
// alertController.addAction(okAction)
|
||||
// guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
// let rootViewController = windowScene.windows.first?.rootViewController else {
|
||||
// return
|
||||
// }
|
||||
// rootViewController.present(alertController, animated: true)
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// 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())
|
||||
}
|
||||
Reference in New Issue
Block a user