IOS

Timer 객체와 RunLoop에 대하여

부스트코스의 IOS 앱 프로그래밍 프로젝트를 진행하면서 Timer객체를 처음 써봤다. 개발자 문서를 봐도 처음엔 이해가 잘 되지 않아서 그냥 간단히 사용법만 알고 넘어갔었는데 부스트코스를 마치고 나서 복습을 하다보니 Timer 객체를 사용하면서 알아야 할게 많은 것 같아 이 글에 간단히 정리해보고자 한다. 음악 플레이어를 만들기 위해 Timer 객체를 사용했었으니, 그 코드 중 일부를 가져와서 분해 및 설명하는 방식이 이해하는데 더 도움이 될 것 같다.


아래 코드는 타이머를 만들고 작동시키는 기능을 하는 메소드이다.

func makeAndFireTimer(){
        
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { [unowned self] (timer: Timer) in
            
            if self.progressSlider.isTracking{ return }
            
            self.updateTimeLabelText(time: self.player.currentTime)
            self.progressSlider.value = Float(self.player.currentTime)
            
        })
        self.timer.fire()
    }

이 메소드가 정의된 클래스에 Timer 타입의 프로퍼티(timer)를 선언하고 플레이어의 재생버튼을 누를 때 마다 위 메소드가 실행되는 구조이다.  이 메소드 내부의 코드들을 하나하나 살펴보려고 하는데, 그 전에 Timer 객체와 관련해서 몇 가지 알게된 사실을 먼저 기술하겠다.

 

 

먼저 개발자 문서에 나와있는 Timer 객체에 대한 요약을 보자.

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

타이머는 시작되고나서 특정 시간 간격이 경과된 후에, 타겟 오브젝트에게 메세지를 보낸다. 즉, 우리가 타이머를 눌렀을 때(타이머가 시작됐을 때) 내가 설정한 시간 만큼이 지나면 타이머가 타겟에게 메시지를 보내고, 타겟으로 하여금 어떤 행동을 하게 만든다는 의미다.

위의 makeAndFireTimer 메소드 내부에 Timer.scheduledTimer 메소드의 block이 그 행동에 해당한다. scheduledTimer 메소드를 정확히 알기 위해선 한가지 개념이 더 필요한데, 그것이 RunLoop이다.

 

RunLoop는 윈도우 시스템이나 포트로부터 마우스 및 키보드 입력같은 Input Sources와 타이머 이벤트를 처리한다. 그런데 이 RunLoop객체는 애플리케이션에서 만들거나 관리하는 것이 아니라 필요할때 시스템(운영체제)이 메인 스레드를 포함한 각 스레드에 생성한다.

결국 정리하자면, 어떤 스레드에서 Timer 이벤트를 발생시키면 그 스레드에 있는 RunLoop객체가 그 이벤트를 처리할 수 있다는 얘기.

 

그런데 메인 스레드에서는 RunLoop를 별도로 실행시킬 필요가 없지만 다른 스레드에서는 해당 스레드의 RunLoop 객체를 얻어서 실행시켜줘야 이벤트 처리가 가능하다. 그 이유는 애플리케이션이 실행될 때 프레임워크에서 메인 스레드의 RunLoop를 자동으로 설정하고 실행하기 때문이다.  RunLoop와 관련된 자세한 내용은 다음번에 다뤄보도록 하겠다. 

 

Timer 객체의 개발자 문서를 조금 더 살펴보면 이런 내용이 나온다. 

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

타이머는 Run loop와 결합되어 동작하는데, 이 Run loop는 타이머와 강한 참조를 유지하기 때문에 타이머를 Run loop에 추가했다면 프로그래머가 타이머와 강한 참조를 유지할 필요가 없다. 여기서 강한 참조는 스위프트의 자동 참조 카운트(ARC)에 대한 내용이다. 

 

 

자 이제 다시 본론으로 돌아와서 Timer의 타입 메소드인 scheduledTimer의 설명을 보자. 

class func scheduledTimer(withTimeInterval interval: TimeInterval, 
                  repeats: Bool, 
                    block: @escaping (Timer) -> Void) -> Timer
Creates a timer and schedules it on the current run loop in the default mode.

타이머를 생성하고 기본 모드에서 현재 스레드의 RunLoop 객체에 예약한다. RunLoop 객체에는 여러 모드가 존재하는데 이건 RunLoop만을 다루는 포스팅에서 따로 설명하겠다.

아무튼 이 메소드는 인자로 들어오는 설정값들이 반영된(+ 현재 스레드의 RunLoop에 등록된) Timer 객체를 반환하는 메소드다.

  • TimeInterval 타입의 interval 매개변수는 타이머가 발동되는 사이의 시간 간격을 말한다.
  • Bool타입의 repeats 매개변수는 반복적으로 타이머가 발동되는지 여부이다.
  • 만약 interval이 10초, repeats가 True이면 10초에 한 번씩 타이머가 발동될 것이다.
  • 타이머가 발동될 때 실행되는 코드 블록이 block에 해당하는 클로저이다.

block에 해당하는 클로저는 scheduledTimer 메소드가 종료되고 나중에 실행되므로 @escaping 키워드가 붙은 걸 볼 수 있다.

 

scheduledTimer 메소드를 이용해 Timer 객체를 생성한 뒤, 타이머가 0.01초 간격으로 발동될 때 마다 AVAudioPlayer의 재생 시간에 따라 미리 만들어놓은 UISlider의 값 및 재생 시간을 사용자에게 보여주는 UILabel의 text를 갱신해준다. 따라서 makeAndFireTimer 메소드를 호출하면  설정값이 반영된 Timer 객체(timer)를 생성하고 그 타이머를 발동시킨다.

 

 

반대로 타이머를 종료하기 위해서 아래 함수를 정의했다.

func invalidateTimer(){
        self.timer.invalidate()
        self.timer = nil
    }

invalidate 메소드는 타이머를 멈추고 등록된 Run Loop에서 타이머를 제거한다. 플레이어의 재생버튼을 다시 눌렀을 때,  음악 재생을 멈추고 위 함수를 호출하여 타이머를 종료시키면 음악 플레이어의 재생과 멈춤 기능을 구현할 수 있다.