기능 구현 #1
| FastingManager
FastingManager
| FastingState
FastingManager는 본격적인 식단 관리 기능의 구현에 해당한다.
FastingManager라는 이름의 파일을 새로 생성해 구현한다.
import Foundation
class FastingManager: ObservableObject {
}
ObservableObject 프로토콜을 채용한 클래스의 형태로,
해당 해당 클래스에 접근하는 View가 Published로 지정된 속성을 사용할 수 있게 된다.
import Foundation
enum FastingState {
case notStarted
case fasting
case feeding
}
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
}
앱은 세 가지 상태를 가진다.
각각은 enum의 case로 구현해 가독성을 높이고, FastingManager가 생성될 때의 값이 notStarted가 되도록 구현했다.
struct ContentView: View {
@StateObject var fastingManager = FastingManager()
var body: some View {
VStack(spacing: 40) {
header
ProgressRing()
times
strBtn
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
해당 클래스는 ContentView에 StateObject 인스턴스로 생성된다.
ObservedObject를 사용하는 방법도 존재하지만 값이 업데이트가 될 때마다 View를 새로 생성하게 되므로
우선은 StateObject로 동작을 확인하고, View의 동작이 정상적이지 않다면 ObservedObject로 변경한다고 강의는 설명한다.
struct ContentView: View {
@StateObject var fastingManager = FastingManager()
var title: String {
switch fastingManager.fastingState {
case .notStarted:
return "Let's get started!"
case .fasting:
return "You are now fasting"
case .feeding:
return "You are now feeding"
}
}
var body: some View {
VStack(spacing: 40) {
header
ProgressRing()
times
strBtn
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
이제 fastingManager의 fastingState에 따라 제목으로 표시할 문자열을 분기하고
extension ContentView {
var header: some View {
VStack(spacing: 40) {
// MARK: Title
Text(title)
.font(.headline)
.foregroundColor(Color(#colorLiteral(red: 0.8567120433, green: 0.5915268064, blue: 1, alpha: 1)))
// MARK: Fasting Plan
Text("16:8")
.fontWeight(.bold)
.padding(.horizontal, 24)
.padding(.vertical, 8)
.background(BlurEffect().blurEffectStyle(.systemThickMaterial))
.cornerRadius(25)
}
}
.
.
.
var times: some View {
HStack(spacing: 60) {
// MARK: Start Time
VStack(spacing: 5) {
Text(fastingManager.fastingState == .notStarted ? "Start" : "Started")
.opacity(0.7)
if #available(iOS 15.0, *) {
Text(Date(), format: .dateTime.weekday().hour().minute().second())
.fontWeight(.bold)
} else {
Text(Date(), formatter: Self.DateFormat)
.fontWeight(.bold)
}
}
// MARK: End Time
VStack(spacing: 5) {
Text(fastingManager.fastingState == .notStarted ? "End" : "Ends")
.opacity(0.7)
Text(Date().addingTimeInterval(16), formatter: Self.DateFormat)
.fontWeight(.bold)
}
}
}
.
.
.
.
.
.
var strBtn: some View {
Button {
// MARK: Button Action
} label: {
Text(fastingManager.fastingState == .fasting ? "End Fast" : "Start Fasting")
.font(.title3)
.fontWeight(.bold)
.padding(.horizontal, 24)
.padding(.vertical, 8)
.background(BlurEffect().blurEffectStyle(.systemThickMaterial))
.cornerRadius(25)
}
.foregroundColor(.primary)
}
.
.
.
적절한 문자열을 표시하도록 header와 times, strBtn을 수정한다.
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
func toggleFastingState() {
fastingState = fastingState == .fasting ? .feeding : .fasting
}
}
strBtn이 제대로 동작할 수 있도록 fastingState를 변경하는 메서드를 정의한다.
.
.
.
var strBtn: some View {
Button {
fastingManager.toggleFastingState()
} label: {
Text(fastingManager.fastingState == .fasting ? "End Fast" : "Start Fasting")
.font(.title3)
.fontWeight(.bold)
.padding(.horizontal, 24)
.padding(.vertical, 8)
.background(BlurEffect().blurEffectStyle(.systemThickMaterial))
.cornerRadius(25)
}
.foregroundColor(.primary)
}
.
.
.
정의한 메서드는 바로 Button에 연결해 동작을 확인 할 수 있다.
FastingManager
| FastingPlan
enum FastingPlan: String {
case beginner = "12:12"
case intermediate = "16:8"
case advanced = "20:4"
var fastingPeriod: Double {
switch self {
case .beginner:
return 12
case .intermediate:
return 16
case .advanced:
return 20
}
}
}
단식 난이도는 enum 으로 정의한다.
각각의 단계에 맞게 단식 시간을 반환하도록 구현하면 된다.
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
@Published private(set) var fastingPlan: FastingPlan = .intermediate
func toggleFastingState() {
fastingState = fastingState == .fasting ? .feeding : .fasting
}
}
fastingState과 같은 방식으로 FastingPlan 인스턴스를 생성한다.
.
.
.
var header: some View {
VStack(spacing: 40) {
// MARK: Title
Text(title)
.font(.headline)
.foregroundColor(Color(#colorLiteral(red: 0.8567120433, green: 0.5915268064, blue: 1, alpha: 1)))
// MARK: Fasting Plan
Text(fastingManager.fastingPlan.rawValue)
.fontWeight(.bold)
.padding(.horizontal, 24)
.padding(.vertical, 8)
.background(BlurEffect().blurEffectStyle(.systemThickMaterial))
.cornerRadius(25)
}
}
.
.
.
임시 문자열을 표시하던 ContentView의 header도 fastingPlan을 따르도록 코드를 수정한다.
FastingManager
| 시작, 종료 시간 인디케이터
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
@Published private(set) var fastingPlan: FastingPlan = .intermediate
@Published private(set) var startTime: Date
@Published private(set) var endTime: Date
func toggleFastingState() {
fastingState = fastingState == .fasting ? .feeding : .fasting
}
}
시작시간과 종료시간을 표시할 수 있도록 필요한 속성을 선언한다.
class FastingManager: ObservableObject {
@Published private(set) var fastingState: FastingState = .notStarted
@Published private(set) var fastingPlan: FastingPlan = .intermediate
@Published private(set) var startTime: Date
@Published private(set) var endTime: Date
init() {
let calendar = Calendar.current
var components = calendar.dateComponents([.year, .month, .day, .hour], from: Date())
components.hour = 20
print(components)
let scheduledTime = calendar.date(from: components) ?? Date()
print("scheduledTime", scheduledTime)
startTime = scheduledTime
endTime = scheduledTime.addingTimeInterval(FastingPlan.intermediate.fastingPeriod)
}
func toggleFastingState() {
fastingState = fastingState == .fasting ? .feeding : .fasting
}
}
FastingManager 인스턴스가 생성될 때 calendar를 사용해 지정된 시간을 할당하도록 구현한다.
FastingManager가 초기화될 때 위와 같은 내용을 콘솔에서 확인할 수 있고, 제대로 시간을 불러오고 있다.
단, 지금 상태로는 한 가지 문제가 있다.
앱이 시작 된 시간이 오후 8시 이전이라면 문제가 없지만, 8시 이후라면 지금 시간보다 이른 시간이 시작 시간으로 할당된다.
이를 해결하기 위해 날짜를 해당되는 경우라면 날짜를 넘겨주는 방법을 생각해 봐야 한다.
let components = DateComponents(hour: 20)
let scheduledTime = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .nextTime)!
print("ScheduledTime", scheduledTime.formatted(.dateTime.month().day().hour().minute().second()))
startTime = scheduledTime
endTime = scheduledTime.addingTimeInterval(FastingPlan.intermediate.fastingPeriod)
calendar의 nextDate 메서드를 사용해 특정 시간 이후로는 다음 날짜로 변경해 줄 수 있다.
'프로젝트 > FastingTimer' 카테고리의 다른 글
05. 기능 구현 #3 (0) | 2023.02.20 |
---|---|
04. 기능 구현 #2 (0) | 2023.02.17 |
02. 인터페이스 디자인 #2 (0) | 2023.02.16 |
01. 인터페이스 디자인 #1 (0) | 2023.02.14 |
00. 시작하며 (0) | 2023.02.09 |