13 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
XuXuCheck is an iOS SwiftUI app (bundle id com.niaodaifu.xuxu.XuXuCheck) that analyzes dry-chemistry urinalysis test strips: the user photographs a strip with the camera, the app locates the strip with a C++/OpenCV pipeline, samples 16 reagent pads over 16 frames, and posts the HSV/Lab measurements to a remote API. Everything else in the app (product info, records, members, account) is server-rendered web content shown in a WKWebView.
The app UI is in English, but the business domain and the original C++ pipeline are Chinese (comments in XuXuCheck/opencv/). String catalog XuXuCheck/Localizable.xcstrings ships en + zh-Hans; always route user-facing text through localizeString(_:) (utils/StringExts.swift), never a bare literal.
Build / run
Always use the workspace, not the .xcodeproj — the app links the CocoaPods framework.
open XuXuCheck.xcworkspace # or:
xcodebuild -workspace XuXuCheck.xcworkspace -scheme XuXuCheck \
-destination 'generic/platform=iOS' build
xcodebuild -workspace XuXuCheck.xcworkspace -scheme XuXuCheck \
-destination 'platform=iOS Simulator,name=iPhone 17' build
pod installafter changingPodfile/Podfile.lock(currently CocoaPods 1.16.2; Alamofire 4.x, not 5.x, soAlamofire.request(...)andDataResponse<T>are correct here).- The
Podfileends with apost_installhook that is load-bearing. It forces every pod target'sIPHONEOS_DEPLOYMENT_TARGETto 15.6. The podspecs declare 8.0 andplatform :iosused to say 13.0; Xcode 27 only accepts 15.0–27.0, so without the hookpod installproduces a project that fails before compiling anything (The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 15.0 to 27.0.x). This was previously a hand-edit to 22 lines ofPods.xcodeprojand was silently reverted by every regeneration — i.e. anypod installused to break the build. Don't move it back into the project file. - Only three pods remain:
Alamofire,AlamofireObjectMapperand its transitiveObjectMapper.ProgressHUD,SDWebImage/SDWebImageSwiftUIandWrappingHStackwere removed — no app source ever imported them, yetuse_frameworks!was embedding all of them as dynamic frameworks (3.7 MB of the 6.4 MB inXuXuCheck.app/Frameworks). CheckXuXuCheck.app/Frameworksafter touching this: 3 frameworks, ~2.8 MB. - The test targets are empty Xcode stubs (
XuXuCheckTestsuses Swift Testing,XuXuCheckUITestsuses XCTest). There is no meaningful test suite yet; run a single one with-only-testing:XuXuCheckTests/XuXuCheckTests/example. - A physical device is required to exercise the camera/scan flow; the simulator will show the empty camera branch.
The OpenCV framework is vendored in the repo
Frameworks/opencv2.xcframework (309 MB upstream; 203 MB in this git copy, which keeps only the two iOS slices — see below) is checked into the working directory, so a plain checkout builds without anything external. It is wired up by exactly one PBXFileReference in XuXuCheck.xcodeproj/project.pbxproj:
path = Frameworks/opencv2.xcframework; sourceTree = "<group>";
resolved from the project directory (the Frameworks group has no path, so it contributes nothing). There are no FRAMEWORK_SEARCH_PATHS / HEADER_SEARCH_PATHS anywhere — at build time Xcode's ProcessXCFramework copies the matching slice into DerivedData and both the compiler and linker find it through the automatic -F$(BUILT_PRODUCTS_DIR). So moving the bundle means editing that one line and nothing else.
Upstream facts worth knowing before touching it:
- All four slices are static (
current ar archive), so nothing is embedded and there is no Embed phase. Two of them are visionOS (xros-arm64,xros-arm64-simulator, ~106 MB combined) and are dead weight for this iOS-only target. In this git copy those two visionOS slices have been deleted — together with theirAvailableLibrariesentries in the xcframework'sInfo.plist— leaving onlyios-arm64andios-arm64_x86_64-simulator;ProcessXCFramework --platform ioswas verified against the edited plist by a build. The desktop copy still has all four. - Nothing in this repo builds it. It was produced outside, at
~/opencv/build_all/, and the same file is also linked by~/Desktop/cvgithub/OpenCVLightBuzz/OpenCV-Apple-Mutliplatform— keep a copy there if that project still matters, or refresh both when OpenCV is rebuilt. ~/Downloads/opencv2.frameworkis unrelated: the rawopencv-4.11.0-ios-framework.zipunpacked, a 564 MB fat binary containingarmv7 armv7s i386 x86_64 arm64with no separate simulator slice. Do not repoint the project at it.- Debug builds split the real code into
XuXuCheck.debug.dylib(Xcode 16+ debug-dylib), leaving a ~90 KB executable stub. Judge binary size from the.dylib, not the executable.
Deployment target is iOS 18.4 with CLANG_CXX_LANGUAGE_STANDARD = gnu++20 (required by opencv/ImgLocateDryChemistry.cpp); the Podfile's platform :ios, '15.6' only applies to the pods — it is there to clear Xcode 27's 15.0 floor, see the post_install note above.
Architecture
App state: Observation framework, not ObservableObject
XuXuCheckApp.swift builds the whole object graph as @State and injects it with .environment(...):
Status (nav chrome), LoginViewModel, CameraViewModel, LoginStatus, WebViewModel.
Read them in views with @Environment(Type.self) var vm / @Bindable — there are no @EnvironmentObject/ObservableObject/@Published anywhere. When adding shared state, follow this pattern (a @Observable class + .environment() + @Environment(X.self)).
Storage.shared (utils/Storage.swift) is a separate singleton wrapper over @AppStorage UserDefaults keys (token, login flag, active member) — it is the source of truth for auth and is read imperatively from non-View code (Router, bridge handlers), so it is intentionally not part of the environment graph.
Navigation
ContentView switches on @AppStorage("selectedTab") over the Tab enum (navigation/Tab.swift) and overlays TabBar. Status.showNavDetail controls the tab bar and its name is inverted — true means "a detail page is showing", which ContentView renders as TabBar().offset(y: 200), i.e. off screen. XuXuWebView pairs onAppear (true) with onDisappear (false) so the bar comes back however the page is left; the goback and logout web commands also reset it inline so it returns together with the dismiss animation. Both halves are required — resetting from only one exit path is what caused the "log out from Settings and the tab bar never comes back" bug. Tabs: Home / Members / Records / Mine; Members and Records fall back to LoginView when loginStatus.isLogin == false. TabBarView.swift is an older TabView-based variant that is not used by the current ContentView.
Hybrid web/native split
Most feature screens are remote web pages, not SwiftUI. Home.swift lists server URLs and api/API.swift holds the base URLs (https://xuxucheck.com/web/...) plus the ?channle=...&platform=1&usertoken=... query string built by GetGeneralRequestParams() in utils/Globe.swift.
- Rendering:
XuXuWebView→WebViewRepresentable(aWKWebViewUIViewRepresentable) →WebViewModel, which owns the URL, loading state, and JS evaluation. - Web → Swift: the page calls
window.webkit.messageHandlers.receiveData.postMessage(<json string>). The JSON decodes intoJS2SwiftModel { command, parameter }(Model/JS2SwiftModel.swift). A command is handled in two places, and both are required:WebViewRepresentable.Coordinator.userContentController— mutatesStorage/app state (setdefaultmember,changeself,logout,login).XuXuWebView.onChange(of: webViewModel.hasNewCommand)— drives SwiftUI presentation/dismissal (goback,scancode,selectphoto,selectcamera,scanpaper). Adding a command means editing both switches (andJS2SwiftDataif it carries new payload).
- Swift → Web:
WebViewModel.callJsImage/callJsBarcodeinvoke a global JS function on the page:CallJS('SelectPhoto'|'BarCode', payload). Photos are sent as a JPEG base64 data URL. - Vertical edges:
XuXuWebViewrenders web views full-bleed at the bottom and with a small app-side inset at the top (topSafeInset, default 20), paired withcontentInsetAdjustmentBehavior = .neverinWebViewRepresentable. The pages' own headers already leave ~49pt for a browser status bar, so the old full safe-area inset doubled up into an oversized blue band; conversely the web view must reach the physical bottom or the parent view's white shows through as a 34pt band. All three pieces are one mechanism and affect every web page at once. Measurements, tuning table and how to sample screenshots:docs/webview-safe-area.md. - Local HTML (
Web_Assets/index.html,css/,images/) is leftover scaffolding from the upstream WKWebView sample and is only loaded whenWebViewModel.urlis nil; ignore it when tracing real behavior. Note the sample handler name there (MyObserver) differs from the one the bridge actually registers (receiveData).
Networking
api/Router.swift is an Alamofire 4 URLRequestConvertible enum — one case per endpoint, with params, path, and method all in the switch. api/APIService.swift is the singleton facade with (Model?, XuXuRespError?) completion handlers, mapping via AlamofireObjectMapper (MemberModel, RecordResultModel are Mappable classes; RecordModel is Codable).
Conventions worth preserving:
model.code > 0means a business error; it is surfaced asXuXuRespError(code:message:). Transport/HTTP failures are normalized tocode == 10086.- Every request carries
X-Platform,X-Channel,X-Time(unix seconds),X-Sign,X-TimeZone,X-Locale, and, when logged in,Authorization: Bearer <usertoken>.X-Signis derived fromX-TimebygetSign(time:)inutils/Globe.swift(MD5 slices of a hardcoded salt + the timestamp). If you change how time is generated, signing breaks. - Most endpoints send form-encoded params (
URLEncoding.default);interface_result_addrecordis the exception — it builds a snake_case JSONRecordModelbody withJSONEncoding.defaultand base64-free"v"strings of comma-joined measurements.
Camera + OpenCV scan pipeline
Flow: HomeView.HomeScanView or a web command → ScanView → TimerCountView (countdown) → CameraView.
CameraManagerruns anAVCaptureSessionand exposes frames asAsyncStream<CGImage>(previewStream);CameraViewModelpumps it intocurrentFrameon the main actor.CameraViewModelalso owns the accumulatedarray3Dand the upload call.CameraView's body is the processing loop: every re-render callsOpenCVWrapper.analysisImage(uiImage)inline and inspects the result. Work is gated onresult.paperWbValue[3] == 0(a valid strip/locator was found); measurements accumulate one frame at a time until 16 frames are collected, thenaddRecordis called. Any change to this view's structure changes how often the pipeline runs and how fast the 16 frames fill.opencv/OpencvWrapper.mmis Objective-C++ bridging to the C++ImageLocation/ImgLocateDryChemistrylocator, which finds the strip from its colored locator blocks (ContourSelector,ContourGf*). Only the wrapper header is exposed to Swift, viaXuXuCheck-Bridging-Header.h— note it importsopencv/OpenCVWrapper.hwhile the file on disk isOpencvWrapper.h; this resolves only because macOS filesystems are case-insensitive, so keep the name if you want it to keep compiling.- The bridge returns an
ImgResultObjective-C object with untypedNSMutableArrayproperties, which Swift force-casts (result.paperAnalResults as! [[Double]]). The shapes are fixed:paperWbValue=[locatorColor, items, locatorLength, validFlag],paperTypeDesc= same 4 slots as Ints,paperAnalResults=16 × [H, S, V, L, a, b]. Those casts crash if the C++ side changes a dimension — updateImgResult.m's defaults too.
Notes and quirks
Color(hex:)exists twice:DynamicColor/is a vendored copy of yannickl/DynamicColor supplyingColor(hex: UInt64)(used asColor(hex: 0x2449f1));utils/exts.swiftadds a separateColor(hex: String). Check the argument type before assuming which one resolves.Model/JS2SwiftModel.swiftmaps the JS key"self"onto a property deliberately namedjsSelf— the file comments explain that naming itselfDatabroke compilation.XuXuCheck.entitlementscontains macOS-style sandbox keys, which have no effect on the iOS target.- Comments and code in this repo are a mix of the original author's notes, upstream sample code, and dead/commented-out blocks (notably in
ContentView.swift,CameraView.swift, andWebViewRepresentable.swift). Read the live code path before trusting a commented alternative.