특정 시간이 지난 후에 이벤트를 발생시킨다거나, 반복적인 주기로 특정 작업을 수행하는 등의 행위는 프로그램에서 흔하게 필요로 하는 요구사항입니다. 이번 포스트에서는 이러한 상황에서 유용하게 사용할 수 있는 Timer에 대해서 알아보도록 하겠습니다.

Timer Reference

  • Timer란?
    Timer는 일정 시간 간격 이후에 지정된 메시지를 원하는 객체에 전달하기 위한 매커니즘을 제공해주는 타입입니다. 이는 RunLoop에서 Timer Source에 해당하는 것으로, RunLoop 객체에 Timer를 등록하면 RunLoop 객체는 이 Timer를 강한 참조로 가지고 있게 됩니다. Timer의 특성에 대한 사항은 이전 포스트의 Timer Source 파트를 참조하시면 됩니다. 이번 포스트에서는 Timer 객체 사용에 집중하겠습니다.

  • Timer Tolerance
    Timer 객체는 tolerance라는 프로퍼티를 제공합니다. 이는 Timer가 이벤트를 발생시키는 것에 여유를 허용함으로써 시스템이 전원 관리와 반응성을 위한 최적화를 할 수 있게 해줍니다. 기본 값은 0으로 이는 시간이 되면 가능한 바로 이벤트를 발생시킨다는 뜻입니다. tolerance가 설정되면 timer가 이벤트를 발생하는 시간은 (지정된 시간) ~ (지정된 시간 + tolerance) 사이가 됩니다. 만약 주기적인 Timer라면, 다음 이벤트 발생 시간은 실제 이벤트 발생 시간이 아니라 원래 지정된 시간을 기준으로 설정됩니다. Apple 공식 문서에서 권장하는 Tolerance 값은 최소 원래 간격의 10% 이상입니다.

  • Timer 사용법
    Timer를 사용하는 방법은 크게 두가지가 있습니다. (NSObject를 통해 사용하는 것은 제외합니다.)

    1. 타입 메서드 이용
      Timer 클래스는 scheduledTimer(timeInterval:target:selector:userInfo:repeats:), scheduledTimer(withTimeInterval:repeats:block:)의 2가지 메소드를 제공합니다. (NSInvocation을 받는 메소드도 있으나, Swift에서는 NSInvocation을 사용하지 않으므로 제외했습니다.) 이 메소드는 Timer 객체를 만든 뒤 현재 RunLoop에 default모드로 이 Timer를 추가해줍니다. 이 메소드의 인자로 넘어가는 셀렉터 혹은 클로저는 (Timer) -> Void 형태여야 합니다. 또한 여기서 인자로 넘어가는 userInfo는 Timer 객체의 인스턴스 프로퍼티로 제공이 되어 실행될 메소드 내에서 참조가 가능합니다. 클로저를 받는 메소드의 경우는 userInfo 인자가 없는데, 이는 클로저가 외부 변수를 캡쳐할 수 있는 기능이 있어 굳이 인자로 넘기지 않아도 되기 때문입니다.

    2. Timer 객체 생성 후 수동으로 RunLoop에 추가
      셀렉터 버전의 init(timeInterval:target:selector:userInfo:repeats:), init(fireAt:interval:target:selector:userInfo:repeats:)와 클로저 버전의 init(timeInterval:repeats:block:), init(fire:interval:repeats:block:) 가 존재합니다. 같은 버전의 두 메소드의 차이는 첫 이벤트를 발생시키는 시간을 지정하는가 아닌가의 차이입니다. fire 인자가 없는 버전은 Timer가 등록된 시점에서 timeInterval 이후에 첫 이벤트가 발생하고, fire인자가 있는 버전은 첫 인자로 넘긴 시점에서 첫 이벤트가 발생합니다.(만약 현재 시각보다 과거를 지목하면 바로 이벤트가 발생합니다.)

    이렇게 만들어진 Timer 객체는 원하는 RunLoop 객체에 add(_:forMode:) 메소드를 이용해 추가합니다. 이 방법은 1번 방법과 다르게 RunLoop와 Mode를 지정할 수 있다는 점이 있습니다.

    timer가 시간이 되면 자동으로 이벤트를 발생시키고 주기적인 timer가 아니면 이벤트 발생 후 바로 무효화되기는 하지만, 수동으로 이 과정을 할 수도 있도록 fire() / invalidate() 메소드를 제공합니다. 단발성 timer의 경우는 fire() 이후 지정된 시간이 되지 않아도 자동으로 invalidate 되고, invalidate된 timer는 RunLoop에서 제거됩니다.