[SwiftUI] 생명주기를 활용한 백그라운드 타이머

Page·2022년 7월 20일
0

SwiftUI

목록 보기
14/18
post-thumbnail

서론

현재 진행 중인 프로젝트에서 회원 가입 인증에 시간 제한을 두고있다. 5분으로 제한을 두었는데 백그라운드에서는 타이머가 동작하지 않는 것을 늦게 알았다. 찾아보니 Apple에서 백그라운드 동작을 막았다는 듯 하다.

어떻게 할까 고민을 했는데 마침 스터디에서 iOS 생명주기를 공부하고 있었고 이것을 잘 활용하면 구현할 수 있어 보였다.

생명주기 요약

기존에 사용하던 AppDelegate부터 살펴보자.

총 5가지 AppDelegate 메소드들, 여기서 SceneDelegate 메소드들까지 SwiftUI의 iOS14로 넘어오면서 간단히 변했다. 이것은 @Environment 프로퍼티 래퍼에 있는 ScenePhase에서 확인할 수 있다.

ScenePhase에는 active, inactive, background 3가지 enum 타입이 있다. 각각 확인해보자.

import SwiftUI

struct BackgroundTimer: View {
    
    @Environment(\.scenePhase) var scenePhase
    
    var body: some View {
        Text("Hello, World!")
            .onChange(of: scenePhase) { newValue in
                switch newValue {
                case .active:
                    print("Active")
                case .inactive:
                    print("Inactive")
                case .background:
                    print("Background")
                default:
                    print("scenePhase err")
                }
            }
    }
}



각 첫번째가 active, 두번째가 inactive, 세번째가 background 상태다.

어플을 내린 후 다시 실행 시켰을 때의 결과다. 특정 뷰에서 onChange를 이용하고 있기때문에 active는 출력되지 않았다. 만약 보고 싶으면 실행의 @main이되는 struct에서 이용하면 된다.
참고로 3번 이미지에서 2번처럼 되게 해도 inactive 상태가 되지는 않았다.

타이머

백그라운드에서 동작하지 않는 타이머를 만들어보자.

import SwiftUI

struct BackgroundTimer: View {
    
    @Environment(\.scenePhase) var scenePhase
    @State private var timeRemaining: Double = 4*60
    
    var body: some View {
        Text(getTimeString(time:timeRemaining))
            .onChange(of: scenePhase) { newValue in
                switch newValue {
                case .active:
                    print("Active")
                case .inactive:
                    print("Inactive")
                case .background:
                    print("Background")
                default:
                    print("scenePhase err")
                }
            }
            .onAppear {
                Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { Timer in
                    if self.timeRemaining > 0 {
                        self.timeRemaining -= 1
                    } else {
                        Timer.invalidate()
                    }
                })
            }
        
    }
    
    func getTimeString(time: Double) -> String {
        let minutes = Int(time) / 60 % 60
        let seconds = Int(time) % 60
        return String(format: "%02i:%02i", minutes, seconds)
    }
}

남은 시간을 Int 타입으로 할 수도 있지만 나중을 위해서 Double로 만들어주었다. 저 타이머는 백그라운드에서 동작하지 않는다.

백그라운드 타이머

백그라운드 타이머라고 말은 하고 있지만 정확히 백그라운드 타이머는 아니다. 백그라운드에서 동작한 것 처럼 만들어주는 것 뿐이다. 우선 타이머가 백그라운드에서 동작하는지 출력을 통해 알아보았다.

Inactive 상태에서도 정상적으로 타이머는 동작한다. 쓰레드에 따라 처리되는게 다를거 같은데 백그라운드 상태로 넘어가면 시간이 바뀌지 않거나 딱 한번만 출력되고 끝난다.

import SwiftUI

struct BackgroundTimer: View {
    
    @Environment(\.scenePhase) var scenePhase
    @State private var timeRemaining: Double = 4*60
    @State private var startTime = Date.now
    
    var body: some View {
        Text(getTimeString(time:timeRemaining))
            .onChange(of: scenePhase) { newValue in
                switch newValue {
                case .active:
                    bgTimer()
                case .inactive:
                    print(".inactive")
                case .background:
                    print("Background")
                default:
                    print("scenePhase err")
                }
            }
            .onAppear {
                Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { Timer in
                    if self.timeRemaining > 0 {
                        self.timeRemaining -= 1
                    } else {
                        Timer.invalidate()
                    }
                })
               
            }
        
    }
    
    func getTimeString(time: Double) -> String {
        let minutes = Int(time) / 60 % 60
        let seconds = Int(time) % 60
        return String(format: "%02i:%02i", minutes, seconds)
    }
    
    func bgTimer() {
        let curTime = Date.now
        let diffTime = curTime.distance(to: startTime)
        let result = Double(diffTime.formatted())!
        timeRemaining = 4*60 + result
        
        if timeRemaining < 0 {
            timeRemaining = 0
        }
    }
}

로직은 간단하다. 타이머를 시작한 시간을 저장한다. 그리고 scene이 active 상태가 된다면 현재 시간과의 차이를 구한다. 그 값을 최초 설정 시간(여기서는 4분)과 연산을 해서 남은 시간을 구해줄 수 있다. timeRemaining을 Int로 할 경우 bgTimer()를 실행시킬 때마다 계속 오차가 커진다. Double로 해주어도 완벽하지는 않겠지만 눈에 띄게 문제가 생기지는 않는다.

Reference

https://huniroom.tistory.com/entry/iOS14SwfitUI-SwiftUI-life-cycle-에서-딥링크-처리

3개의 댓글

comment-user-thumbnail
2024년 6월 4일

안녕하세요 마지막에 잘못된 접근방식이라고 적으셨는데 수정한 다른 방법은 어떤걸까요?

2개의 답글