缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成: ``` <type>(<scope>): <subject> <body> ``` 其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
188
康康/Services/HealthProfileImportService.swift
Normal file
188
康康/Services/HealthProfileImportService.swift
Normal file
@@ -0,0 +1,188 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user