2차 업데이트
1차 업데이트 후 또 다른 피드백을 받게 되었는데, 바로 집중모드 중단 시 메인에서 보이는 시간과 상세페이지에서 확인할 수 있는 총 집중시간의 시간이 일치하지 않는다는 피드백이었다. 진짜 상상도 못한 문제라 곧바로 수정에 들어갔다.
일단 수정 전 코드를 보면,
private func pauseTimerData() {
// 생략
let currentDate = Date()
let elapsedTime = currentDate.timeIntervalSince(startTime) + totalTimeElapsed
totalTimeElapsed = elapsedTime
let day = getCurrentFormattedDate()
formatter.dateFormat = "HH:mm:ss"
let lastTime = formatter.string(from: currentDate)
let formattedTotalTime = formatTime(elapsedTime)
let data: [String: Any] = [
"isStudy": false,
"marimo-state": currentGroup,
"marimo-name": profileImageName,
"last-time": lastTime,
"total-time": formattedTotalTime
]
firebaseMainManager.updateData(path: path, day: day, data: data) { result in
switch result {
case .success:
print("상태 업데이트 성공")
self.totalTimeElapsed = 0.0
case .failure(let error):
print("상태를 업데이트하는 중에 오류 발생: \(error.localizedDescription)")
}
}
}
이렇게 집중 시간을 저장하도록 했는데 문제가 발생한 원인에 대해 분석해 보았다.
문제의 원인 분석
이전 코드에서는 여러 번 시작과 중단을 반복할 때 정확한 총시간을 계산하는 방법인데 elapsedTime을 계산할 때 currentDate.timeIntervalSince(startTime) + totalTimeElapsed을 사용했는데, 이 방식은 현재 시간을 시작 시간으로부터의 시간 차에 totalTimeElapsed를 더하는 방식이었다.
그런데 이렇게 했을 때 formatTime(elapsedTime)에서 변환된 시간이 UI에 표시된 시간과 일치하지 않을 수 있고, 이는 elapsedTime이 실시간으로 업데이트되지 않거나 포맷팅 과정에서 차이가 발생할 수 있기 때문이라고 생각했다.
그래서 아래와 같이 코드를 수정했다.
private func pauseTimerData() {
// 생략
let day = getCurrentFormattedDate()
let currentDate = Date()
formatter.dateFormat = "HH:mm:ss"
lastTime = formatter.string(from: currentDate)
let data: [String: Any] = [
"isStudy": false,
"marimo-state": currentGroup,
"marimo-name": profileImageName,
"last-time": lastTime,
"total-time": stopwatchView.timeLabel.text
]
firebaseMainManager.updateData(path: path, day: day, data: data) { result in
switch result {
case .success:
print("상태 업데이트 성공")
case .failure(let error):
print("상태를 업데이트하는 중에 오류 발생: \(error.localizedDescription)")
}
}
}
수정 후 코드의 개선점
수정된 코드는 stopwatchView.timeLabel.text를 직접 사용하여 시간을 저장하도록 했다. 이렇게 함으로써 UI에 포시 된 시간과 저장되는 시간이 일치하도록 했다.
3차 업데이트
2차 업데이트 후, 고질적인 문제였던 자정이 지나면 바로 타이머를 리셋시키는 기능을 추가적으로 구현해 업데이트를 진행했다. 처음 이 앱을 구현하기 시작했을 때부터 이 기능을 넣고 싶었는데 어떻게 해도 구현되지 않고, 능력의 한계치를 느끼고 있었지만 늦게라도 원하는 대로 구현해 업데이트를 하게 되어 기쁘다.
기존 초기화 코드
MainViewController.swift
private var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
private func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(checkAndResetTimer), userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: .common)
}
@objc private func checkAndResetTimer() {
if shouldResetTimer() {
DispatchQueue.main.async {
self.resetSessionData()
print("Timer 초기화")
}
} else {
print("Timer 초기화 안 됨")
}
}
private func shouldResetTimer() -> Bool {
let now = Date()
let calendar = Calendar.current
let currentHour = calendar.component(.hour, from: now)
let currentMinute = calendar.component(.minute, from: now)
let resetHour = 16
let resetMinute = 25
if currentHour == resetHour && currentMinute == resetMinute {
let lastResetDate = userDefaults.object(forKey: LAST_RESET_DATE_KEY) as? Date
if lastResetDate == nil || calendar.startOfDay(for: lastResetDate!) != calendar.startOfDay(for: now) {
userDefaults.set(now, forKey: LAST_RESET_DATE_KEY)
return true
}
}
return false
}
func resetSessionData() {
print("세션 데이터 재설정")
}
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let mainViewController = window?.rootViewController as? MainViewController {
mainViewController.checkAndResetTimer()
}
return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
if let mainViewController = window?.rootViewController as? MainViewController {
mainViewController.checkAndResetTimer()
}
}
실패 원인
1. 타이머가 백그라운드에서 제대로 동작하지 않음:
- iOS는 앱이 백그라운드에 있을 때 타이머 실행을 보장하지 않는데,
이는 배터리 수명 및 리소스 관리를 위해 시스템에서 타이머 실행을 제한하기 때문
2. 사용자가 앱을 열지 않으면 타이머가 리셋되지 않음:
- 타이머 리셋 로직이 applicationWillEnterForeground메서드에 의존하고 있기 때문에 사용자가 앱을 재실행해야만 타이머 리셋
n차 시도 - NotificationCenter를 사용한 리셋
MainViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handleResetTimerNotification), name: Notification.Name("ResetTimerNotification"), object: nil)
}
@objc private func handleResetTimerNotification() {
DispatchQueue.main.async {
self.resetSessionData()
}
}
private func resetSessionData() {
print("Timer 초기화")
}
AppDelegate.swift
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleBackgroundTask()
}
private func scheduleBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
guard let self = self else { return }
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = .invalid
}
DispatchQueue.global().async {
self.checkAndResetTimerIfNeeded()
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = .invalid
}
}
private func checkAndResetTimerIfNeeded() {
let currentDate = Date()
let calendar = Calendar.current
let hour = calendar.component(.hour, from: currentDate)
let minute = calendar.component(.minute, from: currentDate)
if hour >= 17 || (hour == 17 && minute >= 6) {
DispatchQueue.main.async {
self.resetTimer()
print("background 실행")
}
}
}
private func resetTimer() {
NotificationCenter.default.post(name: Notification.Name("ResetTimerNotification"), object: nil)
}
실패 원인
1. 백그라운드에서 타이머 작업이 안정적으로 실행되지 않음:
- UIApplication.shared.beginBackgroundTask를 사용했으나 백그라운드 타이머 작업이 시스템에 의해 제한될 수 있음
- iOS는 배터리 수명 및 리소스 관리를 위해 백그라운드 작업을 엄격하게 제한
2. 정확한 시간에 작업 실행 실패:
- DispatchQueue.global().async를 사용한 백그라운드 작업이 설정된 시간에 정확히 실행되지 않았음
3차 시도 - 일정 시간마다 타이머 설정(성공)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
AnimationHelper.addBouncingAnimation(to: imageView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
AnimationHelper.removeBouncingAnimation(from: imageView)
}
func scheduleMidnightTimer() {
print("scheduleMidnightTimer 호출")
let now = Date()
let calendar = Calendar.current
var midnightComponents = calendar.dateComponents([.year, .month, .day], from: now)
midnightComponents.hour = 1
midnightComponents.minute = 6
midnightComponents.second = 0
guard let midnight = calendar.date(from: midnightComponents) else {
print("자정 시간을 계산할 수 없음")
return
}
let nextMidnight: Date
if now > midnight {
nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
} else {
nextMidnight = midnight
}
let timeInterval = nextMidnight.timeIntervalSince(now)
Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(midnightTimerFired), userInfo: nil, repeats: false)
}
@objc func midnightTimerFired() {
checkAndResetTimerIfNeeded()
Timer.scheduledTimer(timeInterval: 86400, target: self, selector: #selector(checkAndResetTimer), userInfo: nil, repeats: true)
}
@objc func checkAndResetTimer() {
checkAndResetTimerIfNeeded()
print("checkAndResetTimerIfNeeded 호출")
}
func checkAndResetTimerIfNeeded() {
guard let path = studySessionDocumentPath else {
print("집중 모드 데이터 저장 실패: deleteTodayStudySessionData / 사용자 정보를 확인할 수 없음")
return
}
let userDefaults = UserDefaults.standard
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let currentDateString = formatter.string(from: Date())
guard let savedStartTime = userDefaults.object(forKey: START_TIME_KEY) as? Date else {
print("저장된 시작 시간이 없음")
return
}
let savedDateString = formatter.string(from: savedStartTime)
if currentDateString != savedDateString {
print("날짜가 일치하지 않음, 타이머를 재설정 중")
if timerIsCounting {
DispatchQueue.global().async {
self.firebaseMainManager.deleteData(path: path, day: savedDateString) { result in
switch result {
case .success:
print("집중 모드 데이터 삭제 성공")
DispatchQueue.main.async {
self.resetUI()
self.clearUserDefaults()
self.currentGroup = 1
self.updateImageView()
}
case .failure(let error):
print("집중 모드 데이터 삭제 오류: \\(error.localizedDescription)")
}
}
}
} else {
DispatchQueue.main.async {
self.resetUI()
self.clearUserDefaults()
self.currentGroup = 1
self.updateImageView()
}
}
} else {
print("시작 시간이 없음")
}
}
func resetUI() {
stopTime = nil
userDefaults.set(stopTime, forKey: STOP_TIME_KEY)
setStartTime(date: nil)
stopwatchView.timeLabel.text = makeTimeString(hour: 0, min: 0, sec: 0)
stopwatchView.successView.isHidden = true
stopwatchView.cheeringLabel.isHidden = false
}
성공 원인
1. 정확한 시간에 타이머 설정:
- scheduleMidnightTimer메서드를 통해 자정에 타이머를 설정하고, 그 이후 매일 한 번씩 타이머를 리셋하여 문제를 해결했다.
- 자정에 실행되는 타이머 설정은 정확한 시간에 리셋 작업이 수행되도록 했다.
2. UI 업데이트 및 데이터 일관성 유지:
- checkAndResetTimerIfNeeded메서드를 통해 데이터 일관성을 유지하고 타이머 상태를 점검한다.
- 메인 스레드에서 UI 업데이트를 수행하여 UI 관련 오류를 방지한다.
차이점 및 요약
- 첫 시도: 타이머가 백그라운드에서 제대로 동작하지 않고, 사용자가 앱을 다시 열지 않으면 타이머가 리셋되지 않았다.
- 두 번째 시도: 백그라운드 작업을 시도했으나, 백그라운드에서 타이머 작업이 안정적으로 실행되지 않았다.
- 최종 성공: 자정에 초기화되도록 타이머를 설정하고 이후 매일 한 번씩 리셋되도록 구현하여 문제를 해결했고,
이는 iOS의 백그라운드 작업 제약을 우회하고 타이머가 정확한 시간에 실행되도록 구현했다.
'개발자가 상팔자 > [ Team ] 리모리모, 집중하란 마리모' 카테고리의 다른 글
| Team Project : 리모리모 1차 업데이트 - 피드백 수정 (0) | 2024.07.02 |
|---|---|
| Team Project : 리모리모의 리젝 수난기 (1) | 2024.07.01 |
| Team Project : 리모리모, 집중하란 마리모 (2) | 2024.06.24 |