Files

13 KiB
Raw Permalink Blame History

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 install after changing Podfile/Podfile.lock (currently CocoaPods 1.16.2; Alamofire 4.x, not 5.x, so Alamofire.request(...) and DataResponse<T> are correct here).
  • The Podfile ends with a post_install hook that is load-bearing. It forces every pod target's IPHONEOS_DEPLOYMENT_TARGET to 15.6. The podspecs declare 8.0 and platform :ios used to say 13.0; Xcode 27 only accepts 15.0–27.0, so without the hook pod install produces 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 of Pods.xcodeproj and was silently reverted by every regeneration — i.e. any pod install used to break the build. Don't move it back into the project file.
  • Only three pods remain: Alamofire, AlamofireObjectMapper and its transitive ObjectMapper. ProgressHUD, SDWebImage/SDWebImageSwiftUI and WrappingHStack were removed — no app source ever imported them, yet use_frameworks! was embedding all of them as dynamic frameworks (3.7 MB of the 6.4 MB in XuXuCheck.app/Frameworks). Check XuXuCheck.app/Frameworks after touching this: 3 frameworks, ~2.8 MB.
  • The test targets are empty Xcode stubs (XuXuCheckTests uses Swift Testing, XuXuCheckUITests uses 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 their AvailableLibraries entries in the xcframework's Info.plist — leaving only ios-arm64 and ios-arm64_x86_64-simulator; ProcessXCFramework --platform ios was 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.framework is unrelated: the raw opencv-4.11.0-ios-framework.zip unpacked, a 564 MB fat binary containing armv7 armv7s i386 x86_64 arm64 with 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 (a WKWebView UIViewRepresentable) → 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 into JS2SwiftModel { command, parameter } (Model/JS2SwiftModel.swift). A command is handled in two places, and both are required:
    • WebViewRepresentable.Coordinator.userContentController — mutates Storage/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 (and JS2SwiftData if it carries new payload).
  • Swift → Web: WebViewModel.callJsImage / callJsBarcode invoke a global JS function on the page: CallJS('SelectPhoto'|'BarCode', payload). Photos are sent as a JPEG base64 data URL.
  • Vertical edges: XuXuWebView renders web views full-bleed at the bottom and with a small app-side inset at the top (topSafeInset, default 20), paired with contentInsetAdjustmentBehavior = .never in WebViewRepresentable. 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 when WebViewModel.url is 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 > 0 means a business error; it is surfaced as XuXuRespError(code:message:). Transport/HTTP failures are normalized to code == 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-Sign is derived from X-Time by getSign(time:) in utils/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_addrecord is the exception — it builds a snake_case JSON RecordModel body with JSONEncoding.default and base64-free "v" strings of comma-joined measurements.

Camera + OpenCV scan pipeline

Flow: HomeView.HomeScanView or a web command → ScanView → TimerCountView (countdown) → CameraView.

  • CameraManager runs an AVCaptureSession and exposes frames as AsyncStream<CGImage> (previewStream); CameraViewModel pumps it into currentFrame on the main actor. CameraViewModel also owns the accumulated array3D and the upload call.
  • CameraView's body is the processing loop: every re-render calls OpenCVWrapper.analysisImage(uiImage) inline and inspects the result. Work is gated on result.paperWbValue[3] == 0 (a valid strip/locator was found); measurements accumulate one frame at a time until 16 frames are collected, then addRecord is called. Any change to this view's structure changes how often the pipeline runs and how fast the 16 frames fill.
  • opencv/OpencvWrapper.mm is Objective-C++ bridging to the C++ ImageLocation / ImgLocateDryChemistry locator, which finds the strip from its colored locator blocks (ContourSelector, ContourGf*). Only the wrapper header is exposed to Swift, via XuXuCheck-Bridging-Header.h — note it imports opencv/OpenCVWrapper.h while the file on disk is OpencvWrapper.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 ImgResult Objective-C object with untyped NSMutableArray properties, 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 — update ImgResult.m's defaults too.

Notes and quirks

  • Color(hex:) exists twice: DynamicColor/ is a vendored copy of yannickl/DynamicColor supplying Color(hex: UInt64) (used as Color(hex: 0x2449f1)); utils/exts.swift adds a separate Color(hex: String). Check the argument type before assuming which one resolves.
  • Model/JS2SwiftModel.swift maps the JS key "self" onto a property deliberately named jsSelf — the file comments explain that naming it selfData broke compilation.
  • XuXuCheck.entitlements contains 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, and WebViewRepresentable.swift). Read the live code path before trusting a commented alternative.