본문 바로가기

프로젝트/FastingTimer

05. 기능 구현 #3

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