Files
kangkang/康康/Services/HealthProfileImportService.swift
link2026 77a4ee1c37 缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成:

```
<type>(<scope>): <subject>

<body>
```

其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
2026-06-07 14:17:18 +08:00

189 lines
6.2 KiB
Swift

import Foundation
import HealthKit
struct HealthProfileImportDraft: Identifiable, Equatable {
let id = UUID()
var birthYear: Int?
var biologicalSexRaw: String?
var heightCM: Int?
var bloodTypeRaw: String?
var hasAnyImportableField: Bool {
birthYear != nil ||
biologicalSexRaw != nil ||
heightCM != nil ||
bloodTypeRaw != nil
}
func apply(to profile: UserProfile, now: Date = .now) {
if let birthYear { profile.birthYear = birthYear }
if let biologicalSexRaw { profile.biologicalSexRaw = biologicalSexRaw }
if let heightCM { profile.heightCM = heightCM }
if let bloodTypeRaw { profile.bloodTypeRaw = bloodTypeRaw }
profile.updatedAt = now
}
}
struct HealthProfileImportPreview {
struct Field: Equatable {
let title: String
let current: String
let imported: String?
var willUpdate: Bool {
guard let imported else { return false }
return imported != current
}
}
let birthYear: Field
let sex: Field
let height: Field
let bloodType: Field
var fields: [Field] { [birthYear, sex, height, bloodType] }
init(draft: HealthProfileImportDraft, current profile: UserProfile) {
birthYear = Field(
title: String(appLoc: "出生年份"),
current: profile.birthYear.map(String.init) ?? String(appLoc: "未设置"),
imported: draft.birthYear.map(String.init)
)
sex = Field(
title: String(appLoc: "性别"),
current: profile.sex.label,
imported: draft.biologicalSexRaw.map { Self.sexLabel(raw: $0) }
)
height = Field(
title: String(appLoc: "身高"),
current: profile.heightCM.map { "\($0)cm" } ?? String(appLoc: "未设置"),
imported: draft.heightCM.map { "\($0)cm" }
)
bloodType = Field(
title: String(appLoc: "血型"),
current: profile.bloodTypeRaw.isEmpty ? String(appLoc: "未设置") : "\(profile.bloodTypeRaw)",
imported: draft.bloodTypeRaw.map { "\($0)" }
)
}
private static func sexLabel(raw: String) -> String {
(UserProfile.Sex(rawValue: raw) ?? .undisclosed).label
}
}
enum HealthProfileImportError: LocalizedError {
case unavailable
case noReadableFields
var errorDescription: String? {
switch self {
case .unavailable:
return String(appLoc: "这台设备暂不支持读取 Apple 健康资料。")
case .noReadableFields:
return String(appLoc: "Apple 健康里没有可导入的生日、性别、身高或血型。")
}
}
}
struct HealthProfileImportService {
static let shared = HealthProfileImportService()
private let store = HKHealthStore()
func fetchDraft() async throws -> HealthProfileImportDraft {
guard HKHealthStore.isHealthDataAvailable() else {
throw HealthProfileImportError.unavailable
}
let readTypes = readObjectTypes()
try await requestReadAuthorization(for: readTypes)
async let birthYear = readBirthYear()
async let sex = readBiologicalSexRaw()
async let height = readLatestHeightCM()
async let bloodType = readBloodTypeRaw()
let draft = HealthProfileImportDraft(
birthYear: try await birthYear,
biologicalSexRaw: try await sex,
heightCM: try await height,
bloodTypeRaw: try await bloodType
)
guard draft.hasAnyImportableField else {
throw HealthProfileImportError.noReadableFields
}
return draft
}
private func readObjectTypes() -> Set<HKObjectType> {
var types: Set<HKObjectType> = [
HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
HKObjectType.characteristicType(forIdentifier: .biologicalSex)!,
HKObjectType.characteristicType(forIdentifier: .bloodType)!,
]
if let height = HKObjectType.quantityType(forIdentifier: .height) {
types.insert(height)
}
return types
}
private func requestReadAuthorization(for readTypes: Set<HKObjectType>) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
store.requestAuthorization(toShare: [], read: readTypes) { _, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
private func readBirthYear() throws -> Int? {
try store.dateOfBirthComponents().year
}
private func readBiologicalSexRaw() throws -> String? {
switch try store.biologicalSex().biologicalSex {
case .female: return "female"
case .male: return "male"
default: return nil
}
}
private func readBloodTypeRaw() throws -> String? {
switch try store.bloodType().bloodType {
case .aPositive, .aNegative: return "A"
case .bPositive, .bNegative: return "B"
case .abPositive, .abNegative: return "AB"
case .oPositive, .oNegative: return "O"
default: return nil
}
}
private func readLatestHeightCM() async throws -> Int? {
guard let heightType = HKObjectType.quantityType(forIdentifier: .height) else {
return nil
}
let sort = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
return try await withCheckedThrowingContinuation { continuation in
let query = HKSampleQuery(
sampleType: heightType,
predicate: nil,
limit: 1,
sortDescriptors: [sort]
) { _, samples, error in
if let error {
continuation.resume(throwing: error)
return
}
let sample = samples?.first as? HKQuantitySample
let cm = sample?.quantity.doubleValue(for: .meterUnit(with: .centi))
continuation.resume(returning: cm.map { Int($0.rounded()) })
}
store.execute(query)
}
}
}