HealthKit (9)
Chart Drag시 Haptic 기능 추가.
차트를 드래그할때 Haptic 기능을 제공하여 기기와 접촉할 때 촉감을 자극해 실제로 뭔가를 만지고 있다는 느낌을 전달하려 한다.
sensoryFeedback Modifier를 사용한다.
Step Chart View에서 적용을 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
@State private var selectedDay: Date?
.background {
RoundedRectangle(cornerRadius: 4)
.fill(Color(.secondarySystemBackground))
.shadow(color: .secondary.opacity(0.3), radius: 2, x: 2, y: 2)
}
.sensoryFeedback(.selection, trigger: selectedDay) // new
.onChange(of: rawSelectedDate) { oldValue, newValue in
if oldValue?.weekdayInt != newValue?.weekdayInt {
selectedDay = newValue
}
}
차트들의 x축은 날짜로 되어있기에 유져가 드래그를 하면 (날짜가 계속 바뀌면) selectedDay의 값이 계속 바뀌기에, Haptic이 작동.
실제 기기에서 테스트를 해보니 잘 되는걸 확인.
이때 Haptic의 강도를 더 세게하려면
.sensoryFeedback(.impact(flexibility: .solid, intensity: 10), trigger: selectedDay)
이런식으로 줄 수 있다.
실기기에서 테스트를 해보니 강도가 더 세진걸 확인할 수 있다.
이걸 나머지 ChartView에도 그대로 적용을 한다.
하나 다른점은 stepPieChartView이다.
다른건 드래그를 통해 날짜의 변화 였으나,
pieChartView는 선택한 요일이 변화하므로
1
2
3
4
5
6
.onChange(of: selectedWeekday) { oldValue, newValue in
guard let oldValue, let newValue else { return }
if oldValue.date.weekdayInt != newValue.date.weekdayInt {
selectedDay = newValue.date
}
}
그전에는 selectedDay type이 Date였기에, 바로 접근이 가능했으나, 이번엔 selectedWeekday
의 type은 우리가 만든 WeekdayChartData
이기에,
oldValue, newValue에 date를 통해, weekdayInt 접근이 가능 한것.
이렇게 하고나니 발생하는에러 WeekdayChartData
가 Equatable 프로토콜을 준수하고 있지않다고한다.
추가해주자.
1
2
3
struct WeekdayChartData: Identifiable, Equatable {
// 내용 생략
}
실행하면 이렇게 된다.
소리가 작으니 키우면 들린다.
Empty State View 만들기
앱을 실행했을때 지금은
이젠 Empty State View를 만들어 관리를 한다.
1
2
3
4
5
6
7
if chartData.isEmpty {
ContentUnavailableView.search
} else {
Chart {
// 생략
}
}
실행해 보면 위와 같다.
물론 search 뒤에 (Text: " ")
를 사용하여 어떤 특정한것에 대해 찾을 수 없다고 알려줄수도 있다.
1
ContentUnavailableView.search(text: "Step Data")
이렇게 기본적으로 제공이 되지만 Customizing을 해보도록 한다.
ContentUnavailableView Customizing
with Description
여러가지중 이녀석으로 커스터마이징을 진행한다.
1
2
3
4
5
ContentUnavailableView(
"No Data",
systemImage: "chart.bar",
description: Text("There is no step count data from the Health App.")
)
without Description, with Label
Description 없이 한다고 하면
1
2
3
ContentUnavailableView {
Label("No Mail", systemImage: "tray.fill")
}
이런식으로도 가능!
Customize whatever we want
아니면 애초에 우리가 원하는대로 디자인도 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ContentUnavailableView {
Image(systemName: "chart.bar")
.resizable()
.frame(width: 32, height: 32)
.foregroundStyle(.secondary)
.padding(.bottom, 8)
Text("No Data")
.font(.callout.bold())
Text("There is no step count data from the Health App.")
.font(.footnote)
}
.foregroundStyle(.secondary)
ContentUnavailableView
안에 VStack이 내장되어 있으므로, UI 디자인 하듯 작성해주면 된다.
그럼 이렇게 디자인이 된다.
다른 차트에도 사용해야하므로 이부분만 별도로 새롭게 View파일로 만든다.
그리고 SystemImageName, Title, Description이 필요하므로 변수를 만들고 위치에 맞게 넣어주자.
해당 코드는 생략.
1
2
3
let systemImageName: String
let title: String
let description: String
ChartView로 가서
1
2
3
if chartData.isEmpty {
ChartEmptyView(systemImageName: "chart.bar", title: "No Data", description: "There is no step count data from the Health App.")
}
이렇게 적용해준다.
나머지 chartView에도 그대로 적용하자. (과정은 생략)
적용하면 다음과 같다.
Text Input Validation
문제점
만약 사용자가 우리가 원하는 대로 값을 입력하지 않는 경우엔 어떤일이 발생할까?
예를 들면
이렇게 숫자만 입력을 해야하는데, 글자를 입력하는경우엔 App Crash가 발생하고 앱이 강제 종료 될것이다.
왜냐 강제 언래핑을 했기 때문.
왜냐하면 현재 우리는
1
try await hkManager.addStepData(for: addDataDate, value: Double(valueToAdd)!)
이렇게 값을 강제로 Double로 형변환을 하면서 숫자만 입력하게 했기때문.
물론 이건 어거지 느낌이긴 하다 왜냐면 입력을 할때
1
2
3
4
TextField("Value", text: $valueToAdd)
.multilineTextAlignment(.trailing)
.frame(width: 140)
.keyboardType(metric == .steps ? .numberPad : .decimalPad)
실제로 NumPad가 나오기 때문.
그렇다면 이런 케이스는?
소수를 입력해야하는데 일부러 올바른 형식을 따르지 않고 등록을 시도할 경우?
위와 같이 또 App Crash가 발생할것이다.
이제 이런부분에 대해 Exception Handling을 하도록 한다.
문제해결
우선 강제 언래핑을 했던 값에 대해서 옵셔널 바인딩을 해주도록 하자.
1
2
3
4
Button("Add Data") {
guard let value = Double(valueToAdd) else {
return
}
이때 이렇게 return으로 끝나는게 아니라 Error Case를 추가하여, Alert를 띄워본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum STError: LocalizedError {
// case 생략
case invalidValue // new
var errorDescription: String? {
switch self {
// 중략
case .invalidValue: // new
"Invalid Value"
}
}
var failureReason: String {
switch self {
// 중략
case .invalidValue: // new
"Must be a numeric value with a maximum of one decimal place."
}
}
}
그리고 ListView에 적용
1
2
3
.alert(isPresented: $isShowingAlert, error: writeError, actions: { writeError in
switch writeError {
case .authNotDetermined, .noData, .unableToCompleteRequest, .invalidValue: // modified
이후 옵셔널 바인딩한 부분 수정
1
2
3
4
5
6
guard let value = Double(valueToAdd) else {
writeError = .invalidValue
isShowingAlert = true
valueToAdd = ""
return
}
실행하면?
이렇게 Alert가 뜨는걸 알 수 있다.
Github: Step-Tracker Repository