기능 구현 #3
| Upcoming time, Percentage, 실제 시간 반영하기
Upcoming time
타이머의 상태가 notStarted이면 elapsedTime과 remainingTime을 표시하는 것은 부자연스럽다.
선택된 난이도에 따라서 현재시간을 기준으로 다음 식사 시간을 나타내도록 수정한다.
// MARK: Timer
VStack(spacing: 30) {
// MARK: Upcoming Time
if fastingManager.fastingState == .notStarted {
VStack(spacing: 5) {
Text("Upcoming fast")
.opacity(0.7)
Text("\(fastingManager.fastingPlan.fastingPeriod, specifier: "%0.f") Hours")
.font(.title)
.fontWeight(.bold)
}
} else {
// MARK: Elapsed Time
VStack(spacing: 5) {
Text("Elapsed Time")
.opacity(0.7)
Text(fastingManager.startTime, style: .timer)
.font(.title)
.fontWeight(.bold)
}
.padding(.top)
// MARK: Remaining Time
VStack(spacing: 5) {
if !fastingManager.elapsed {
Text("Remaing Time")
.opacity(0.7)
} else {
Text("Extra Time")
.opacity(0.7)
}
Text(fastingManager.endTime, style: .timer)
.font(.title2)
.fontWeight(.bold)
}
}
}
fastingState를 기준으로 분기해 Timer의 표시 내용을 변경한다.
notStarted 상태에서 표시될 fastingPeriod는 Double 형식이기 때문에 소수점 단위의 절삭이 필요하다.
이를 위한 방식은 다음의 두 가지 방법이 존재한다.
// ~iOS15
Text("\(fastingManager.fastingPlan.fastingPeriod, specifier: "%0.f") Hours")
.font(.title)
.fontWeight(.bold)
위의 방법은 문자열 생성자를 사용하는 방식으로 상당히 오래된 iOS도 사용하는데 문제가 없으며,
이러한 방식은 다른 여러 언어에서도 공통으로 사용하는 경우가 많기 때문에 가장 유용할 수 있다.
// iOS15~
Text("\(fastingManager.fastingPlan.fastingPeriod.formatted()) Hours")
.font(.title)
.fontWeight(.bold)
iOS15 이상부터 사용할 수 있는 이 방식은 formatted 메서드를 사용한다.
문법상 가장 깔끔하지만 호환성에 주의해 사용해야 한다.
iOS15를 타겟으로 하는 강의와는 다르게 지금은 iOS14와의 호환성을 염두에 두고 만들기 때문에 전자의 방식을 사용한다.
Percentage
| elapsing
진행상태를 ProgressRing으로 표시하고는 있지만, 퍼센트로 수치화하면 더 직관적일 수 있다.
진행 상태와 동기화 되는 ElapsedTime과 RemainingTime의 퍼센트 수치를 함께 나타나도록 코드를 수정한다.
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
@Published private(set) var fastingPlan: FastingPlan = .intermediate
@Published private(set) var startTime: Date {
didSet {
if fastingState == .fasting {
endTime = startTime.addingTimeInterval(fastingTime)
} else {
endTime = startTime.addingTimeInterval(feedingTime)
}
}
}
@Published private(set) var elapsed: Bool = false
@Published private(set) var endTime: Date
@Published private(set) var elpasedTime: Double = 0.0
.
.
.
계산을 위해 FastingManager에 새로운 변수를 정의한다.
Double 형식의 elpasedTime 변수는 0.0을 초기값으로 갖고
func toggleFastingState() {
fastingState = fastingState == .fasting ? .feeding : .fasting
startTime = Date()
elpasedTime = 0.0
}
때문에 FastingState가 변할 때 0으로 초기화해 줘야 한다.
func track() {
guard fastingState != .notStarted else {
return
}
if endTime >= Date() {
elapsed = false
} else {
elapsed = true
}
elpasedTime += 1
}
Track 메서드는 단위시간마다 1씩 증가시킨다.
Percentage
| Calculating
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
@Published private(set) var fastingPlan: FastingPlan = .intermediate
@Published private(set) var startTime: Date {
didSet {
if fastingState == .fasting {
endTime = startTime.addingTimeInterval(fastingTime)
} else {
endTime = startTime.addingTimeInterval(feedingTime)
}
}
}
@Published private(set) var elapsed: Bool = false
@Published private(set) var endTime: Date
@Published private(set) var elpasedTime: Double = 0.0
@Published private(set) var progress: Double = 0.0
.
.
.
계산된 값을 저장하기 위한 progress 변수를 하나 정의한다.
func track() {
guard fastingState != .notStarted else {
return
}
if endTime >= Date() {
elapsed = false
} else {
elapsed = true
}
elpasedTime += 1
let totalTime = fastingState == .fasting ? fastingTime : feedingTime
progress = (elpasedTime / totalTime * 100).rounded() / 100
}
track 메서드에서는 단위시간마다 타이머의 상태에 맞는 시간을 사용해 progress를 계산한다.
elapsedTime을 전체 시간으로 나누고, 100을 곱한 뒤, 반올림하고, 다시 100으로 나눠 비율로 변환한다.
Percentage
| View와 연결하기
이렇게 계산 된 progress 변수는 여러 곳에 적용할 수 있다.
Circle()
.trim(from: 0, to: min(fastingManager.progress, 1.0))
.stroke(AngularGradient(colors: [Color(#colorLiteral(red: 0.8567120433, green: 0.5915268064, blue: 1, alpha: 1)), Color(#colorLiteral(red: 1, green: 0.5821906924, blue: 0.729834497, alpha: 1)), Color(#colorLiteral(red: 0.7210034728, green: 0.9851679206, blue: 0.5617409945, alpha: 1)), Color(#colorLiteral(red: 0.5453777909, green: 0.9893621802, blue: 0.850672543, alpha: 1)), Color(#colorLiteral(red: 0.8567120433, green: 0.5915268064, blue: 1, alpha: 1))], center: .center), style: StrokeStyle(lineWidth: 15, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270))
.animation(.easeInOut(duration: 1.0), value: fastingManager.progress)
progressRing의 더미데이터였던 progress를 대신해 실제 진행 상태를 적용할 수 있고,
이제는 실제 데이터가 있으므로 onAppear도 삭제할 수 있다.
// MARK: Elapsed Time
VStack(spacing: 5) {
Text("Elapsed Time (\(fastingManager.progress * 100, specifier: "%.0f")%)")
.opacity(0.7)
Text(fastingManager.startTime, style: .timer)
.font(.title)
.fontWeight(.bold)
}
.padding(.top)
progress 자체는 Double의 소수점 형태이기 때문에 우리가 아는 퍼센트로 변환하기 위해 다시 100을 곱해 변환하고,
소수점을 제거하기 위해 문자열 생성자를 활용한다.
// MARK: Remaining Time
VStack(spacing: 5) {
if !fastingManager.elapsed {
Text("Remaining Time (\((1 - fastingManager.progress) * 100, specifier: "%.0f")%)")
.opacity(0.7)
} else {
Text("Extra Time")
.opacity(0.7)
}
Text(fastingManager.endTime, style: .timer)
.font(.title2)
.fontWeight(.bold)
}
ElapsedTime의 반대에 해당하는 RemainingTime은 간단하게 1에서 progress를 빼 준 다음 변환하면 된다.
실제 시간 반영하기
지금까지는 Test용으로 각각의 난이도에 맞는 '초'로만 결과를 빠르게 확인했다.
이제는 실제 시간을 대입해 정상적인 기능을 하도록 돌려놓을 차례다.
var fastingTime: Double {
return fastingPlan.fastingPeriod * 60 * 60
}
var feedingTime: Double {
return 24 - fastingPlan.fastingPeriod * 60 * 60
}
fastingTime과 feedingTime에 3600을 곱해 시간 단위로 변환한다.
init() {
let calendar = Calendar.current
let components = DateComponents(hour: 20)
let scheduledTime = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .nextTime)!
startTime = scheduledTime
endTime = scheduledTime.addingTimeInterval(FastingPlan.intermediate.fastingPeriod * 60 * 60)
}
이는 endTime에도 동일하게 적용한다.
강의 내용은 여기까지다.
'프로젝트 > FastingTimer' 카테고리의 다른 글
06. 더 나아가기 (0) | 2023.02.21 |
---|---|
04. 기능 구현 #2 (0) | 2023.02.17 |
03. 기능 구현 #1 (0) | 2023.02.16 |
02. 인터페이스 디자인 #2 (0) | 2023.02.16 |
01. 인터페이스 디자인 #1 (0) | 2023.02.14 |