포스트

HealthKit (1)

HealthKit을 사용한 앱을 만드는 걸 공부하면서 기록을 적어본다.

가급적 UI디자인은 크게 언급할 부분이 없다면 생략을 하면서 가도록 하겠다.

CleanShot 2024-12-12 at 06 55 11

첫 화면의 디자인은 다음과 같다.

Picker(Segmented) 적용하기

현재 디자인이 된 상태에서 Picker를 사용하여 step을 고를지, weight를 고를지를 정해본다.

Picker를 구현하기 앞서

enum을 통해 각 케이스를 구별해준다.

1
2
3
4
5
6
7
8
9
10
enum HealthMetricContext {
    case steps, weight
    
    var title: String {
        switch self {
        case .steps: return "Steps"
        case .weight: return "Weight"
        }
    }
}

그리고 이렇게 만든 열거형 HealthMetricContext을 View에서 사용하기 위해 변수를 만들어 준다.

@State private var selectedStat: HealthMetricContext = .steps

Steps 바로 위에 할것이므로 Vstack 바로 밑에서 적용을 한다.

1
2
3
4
5
6
7
8
ScrollView {
    VStack(spacing: 20) {
        Picker("Select Stat", selection: $selectedStat) {
            ForEach(HealthMetricContext.allCases) { metric in
                Text(metric.title)
            }
        }
        .pickerStyle(.segmented)

이때 ForEach에 enum이 들어가기 위해선 두 프로토콜을 충족시켜줘야 하므로

1
2
3
enum HealthMetricContext: CaseIterable, Identifiable {
    case steps, weight
    var id: Self { self } // new

이렇게 추가해준다.

CaseIterable은 이전글에 작성을 해둔게 있으니 한번 다시 확인해보는것도 좋을듯.

1
var id: Self { self }
  • Identifiable 프로토콜 준수: id는 Identifiable 프로토콜의 요구사항으로, 열거형의 각 케이스를 식별하는 고유한 값이다.
    • id: Self: 열거형 자체(Self)가 고유하기 때문에, 각 케이스를 그대로 id로 사용한다.
    • 열거형의 각 케이스는 자체적으로 고유한 값이므로 추가 작업 없이 self를 반환해도 충분히 구분 가능하다.
  • 예를 들어, HealthMetricContext.steps와 HealthMetricContext.weight는 각각 고유한 id를 가지며, 리스트나 반복문에서 중복 없이 사용할 수 있다.

CleanShot 2024-12-12 at 18 08 05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NavigationLink(value: selectedStat) {
    HStack {
        VStack(alignment: .leading) {
            Label("Steps", systemImage: "figure.walk")
                .font(.title3.bold())
                .foregroundStyle(.pink)
            
            Text("Avg: 10K Steps")
                .font(.caption)
        }
        
        Spacer()
        
        Image(systemName: "chevron.right")
    }
}

selectedStat에 따라 다른 값을 보여주기위해 사용했다.

ScrollView의 modifier로 navigationDestination를 달아준다.

1
2
3
4
5
6
7
8
ScrollView {
    // 생략
}
.padding()
.navigationTitle("Dashboard")
.navigationDestination(for: HealthMetricContext.self) { metric in
    Text(metric.title)
} // new

Dec-12-2024 18-54-17

이렇게 화면 전환이 되는걸 알 수 있다.

현재 Navigation Button이 Default로 파란색이 되어있다.

1
2
3
4
5
6
var isSteps: Bool { selectedStat == .steps }

NavigationStack {
    // 생략
}
.tint(isSteps ? .pink : .indigo) // new

이렇게 추가를 해주자.

실행화면은 생략 어차피 위의 파란색의 색만 변한것.

ListView 생성

이제 이렇게 전환되는 화면에 보여줄 ListView를 만들것이다.

여기에 이제 각 시간대에 걸음이나, 날짜에 해당하는 몸무게를 List 형식으로 보여줄건데.

날짜에 대한 형식의 참고글을 보면 좋을듯.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@State private var isShowingAddData = false

var body: some View {
    List(0..<28) { i in
        HStack {
            Text(Date(), format: .dateTime.month(.wide).day().year())
            Spacer()
            Text(10000, format: .number.precision(.fractionLength(metric == .steps ? 0 : 1)))
        }
    }
    .navigationTitle(metric.title)
    .sheet(isPresented: $isShowingAddData) {
        addDataView
    }
    .toolbar {
        Button("Add Data", systemImage: "plus") {
            isShowingAddData = true
        }
    }
}

여기서 짚고가면 좋을 부분은

Text(10000, format: .number.precision(.fractionLength(metric == .steps ? 0 : 1)))

이부분

CleanShot 2024-12-12 at 20 09 56CleanShot 2024-12-12 at 20 10 13

숫자를 표현하는데 뒤에 소숫점으로 0을 몇개 더 붙일지에 대한 parameter이다.

그리고 sheet를 사용해서 새로운 화면을 띄우게 된다.

UIKit으로 하면 Modal 형식으로 아래에서 위로 올리는 그런 액션.

AddDataView 생성

CleanShot 2024-12-12 at 20 00 58

이렇게 아래에서 위로 올라오는 화면을 구현하는데

특이점이라면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@State private var addDataDate: Date = .now
@State private var valueToAdd: String = ""

var addDataView: some View {
    NavigationStack {
        Form {
            DatePicker("Date", selection: $addDataDate, displayedComponents: .date)
            HStack {
                Text(metric.title)
                Spacer()
                TextField("Value", text: $valueToAdd)
                    .multilineTextAlignment(.trailing)
                    .frame(width: 140)
                    .keyboardType(metric == .steps ? .numberPad : .decimalPad)
            }
        }
        .navigationTitle(metric.title)
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Button("Add Data") {
                    
                }
            }
            ToolbarItem(placement: .topBarLeading) {
                Button("Dismiss") {
                    isShowingAddData = false
                }
            }
        }
    }
}

우리가 일반적으로 어떤 View를 만들때 Struct를 사용해서 View를 만들어 주었다.

하지만 여기서는 그렇게 하지않고 하나의 View를 변수로 담아내었다.

DatePicker("Date", selection: $addDataDate, displayedComponents: .date)

DatePicker를 사용하여 날짜를 선택하게 해주었다.

CleanShot 2024-12-12 at 20 28 58

그리고

.keyboardType(metric == .steps ? .numberPad : .decimalPad) TextField에서 값을 입력할때 패드를 다르게 해준다.

CleanShot 2024-12-12 at 20 26 08CleanShot 2024-12-12 at 20 26 18

전자가 numpad, 후자가 decimalpad

그리고 Dismiss의 경우 나는 예전에 환경변수를 사용했는데, 그때와 지금은 다른게

실행화면

Dec-12-2024 20-23-55


Github: Step-Tracker Repository

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.