본문 바로가기

프로젝트/FastingTimer

03. 기능 구현 #1

기능 구현 #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