본문 바로가기

프로젝트/FastingTimer

04. 기능 구현 #2

기능 구현 #2
| Timer 구현


Timer 구현
| fastingTime, feedingTime 계산하기

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

    var fastingTime: Double {
        return fastingPlan.fastingPeriod
    }
    var feedingTime: Double {
        return 24 - fastingPlan.fastingPeriod
    }

    init() {
    .
    .
    .

정해진 난이도에 따라 식사시간과 단식시간을 계산한다.
단순하게 24시간에서 fastingPeriod로 정해진 시간을 뺀 시간이 식사시간이 된다.

Fasting인 상태와 Feeding인 상태의 startTime에 따라 endTime도 달라야 한다.
이때 유용하게 사용할 수 있는 게 didSet이다.

.
.
.
@Published private(set) var startTime: Date {
    didSet {
        if fastingState == .fasting {
            endTime = startTime.addingTimeInterval(fastingTime)
        } else {
            endTime = startTime.addingTimeInterval(feedingTime)
        }
    }
}
.
.
.

fastingState가 fasting인 경우 endTime은 startTime에 fastingTime을 더한 값이 된다.
이외의 경우 feedingTime을 더해 현재 상태에 따른 올바른 endTime을 계산하도록 구현한다.

Timer 구현
| ProgressRing 연동

struct ProgressRing: View {
    @EnvironmentObject var fastingManager: FastingManager
    @State var progress = 0.0

    var body: some View {
        ZStack {
            // MARK: Place Holder
            Circle()
            .
            .
            .

앞서 구현한 FastingManager를 EnvironmentObject 인스턴스로 선언한다.
EnvironmentObject 인스턴스를 사용하는 경우

struct ProgressRing_Previews: PreviewProvider {
    static var previews: some View {
        ProgressRing()
            .environmentObject(FastingManager())
    }
}

잊지 말고 Preview에도 추가해 줘야 결과를 확인할 수 있다.

var body: some View {
    VStack(spacing: 40) {
        header

        ProgressRing()
            .environmentObject(FastingManager())

        times

        strBtn
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}

또한 ContentView의 ProgressRing에도 추가해 줘야 한다.
ProgressRing과 FastingManager는 ContentView의 child 관계이기 때문이다.

    .
    .
    .
    VStack(spacing: 30) {
        // 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) {
            Text("Remaing Time")
                .opacity(0.7)

            Text(fastingManager.endTime, style: .timer)
                .font(.title2)
                .fontWeight(.bold)
        }
    }
    .
    .
    .

ElapsedTime과 RemainingTime에 각각 startTime과 endTime을 연결하고,
style을 'timer'로 지정한다.

.timer

 

Apple Developer Documentation

 

developer.apple.com

timer style을 사용하면 전달된 Date를 기반으로 count down 되고,
0이 된 이후에는 다시 count 되는 timer를 굉장히 간단하게 구현할 수 있다.

단, 다시 count 되는 동안에는 이미 종료됐다는 것을 표시해야 할 필요가 있다.

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

    var fastingTime: Double {
        return fastingPlan.fastingPeriod
    }
    var feedingTime: Double {
        return 24 - fastingPlan.fastingPeriod
        .
        .
        .

FastingManager에 Bool 형식의 변수를 하나 생성한다.
elapsed라는 이름의 이 변수는 정해진 시간을 넘어 다시 카운트되기 시작할 때 이를 표시할 Flag로서의 기능을 할 것이다.

    // 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)
    }

ProgressRing의 RemainingTime 부분을 위와 같이 수정한다.
elapsed 속성에 맞춰 각각 Remaing Time과 Extra Time을 표시한다.

이젠 0이 됐는지를 확인하고 elapsed 속성을 변경해 줘야 한다.

    .
    .
    .
    func track() {
        guard fastingState != .notStarted else {
            return
        }

        if endTime >= Date() {
            elapsed = false
        } else {
            elapsed = true
        }
    }
}

FastingManager에 새로운 메서드를 정의한다.
track이라는 이름의 메서드로 endTime이 지금 현재의 시간보다 크면 elapsed를 false로,
현재의 시간과 작거나 같으면 종료 이후의 시간으로 판단해 elapsed를 true로 변경한다.
앱의 상태가 feeding 혹은 fasting 상태가 아니라면 굳이 시간을 확인할 필요는 없다. 따라서 guard문을 사용해 즉시 반환한다.

struct ProgressRing: View {
    @EnvironmentObject var fastingManager: FastingManager
    @State var progress = 0.0

    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        ZStack {
            // MARK: Place Holder
            .
            .
            .
        }
        .onReceive(timer) { _ in
            fastingManager.track()
        }
    }
}

ProgressRing에서는 자동으로 시작되는 Timer 인스턴스를 생성한다.
Timer 인스턴스는 1초 간격으로 main thread에서 common 방식으로 동작한다.
1초 이후에는 항상 완료가 됐다는 notification을 발생하는데, 이 notification을 onReceive에서 받게 돼 track 메서드를 호출한다.

결과적으로 1초 간격으로 track 메서드를 호출해 elapsed flag를 확인하게 된다.

'프로젝트 > FastingTimer' 카테고리의 다른 글

06. 더 나아가기  (0) 2023.02.21
05. 기능 구현 #3  (0) 2023.02.20
03. 기능 구현 #1  (0) 2023.02.16
02. 인터페이스 디자인 #2  (0) 2023.02.16
01. 인터페이스 디자인 #1  (0) 2023.02.14