--- 핵심만 일단 정리하였다.

Delaying an asynchronous Swift Task

사용 예제

Task {
    try await Task.sleep(nanoseconds: 1_000_000_000)
}

언제 사용할까?

  • 사용자가 무언가를 검색할 때, debouncing 을 주고, 현재까지 입력한 텍스트로 일정 시간 두었을 때?

검색 로직이 실행되게끔 하기! -> 바로 검색하게 하면, 매 텍스트마다 호출이 되기 때문.

  • 로딩 화면을 보여줄 때! - 바로 로딩화면을 보여주지 말고, 데이터를 가져오는 시간이 일정 시간이 걸릴때부터 보여주는 것이 사용자 입장에서 더 좋음! 이때, delay Task를 사용하자.

가장 간단한 방법은 위에서 설명한 Task.sleep(nanosecond: ) 를 사용하는 방법 (이 때 await를 사용해야 함)

class VideoViewController: UIViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let loadingSpinnerTask = Task {
            try await Task.sleep(nanoseconds: 150_000_000)  // 여기서 작업 지연!
            showLoadingSpinner()
        }

        Task {
            await prepareVideo()
            loadingSpinnerTask.cancel()
            hideLoadingSpinner()
        }
    }
}

참고로 위 코드는 뷰컨을 빠르게 왔다갔다 하면 loading Task가 중복으로 실행되거나, 뷰를 떠났는데도 백그라운드에서 실행이 될 수 있으므로, viewWillDisAppear 에서 Task를 cancel 하는 작업이 필요하다.


간단하게 작업에 지연을 주는 방법은 TimeInterval 이다.

TimeInterval은 아래와 같이 정의되어있다.

extension Task where Failure == Error {
    static func delayed(
        byTimeInterval delayInterval: TimeInterval,
        priority: TaskPriority? = nil,
        operation: @escaping @Sendable () async throws -> Success
    ) -> Task {
        Task(priority: priority) {
            let delay = UInt64(delayInterval * 1_000_000_000)
            try await Task<Never, Never>.slepp(nanoseconds: delay)
            return try await operation()
        }
    }
}

아직 이해가 완전히 되지 않거나, 지금 당장 사용하지 않을 부분은 스킵하였으나, 중요한 부분은 꼭 이해하고 넘어가야한다.

  • @escaping: 보통 비동기 메서드에서 completion 부분에 붙이는 attribute 이다. 기본적으로는 클로저가 끝날 때 completion을 실행하면 오류가 나지만 (non - escaping 이기 때문), 그러나, @escaping 이 붙으면 클로저의 실행이 끝난 뒤에도 completion 클로저를 호출할 수 있다.

위 예제에서는 Task 타입의 operation 을 호출한다.

  • @Sendable: 여러 concurrent Context 에서도 실행할 수 있게 함. 그리고 데이터 레이스 방지를 위해 필요하다.

값 타입(struct, enum)은 기본적으로 Sendable 하지만, 참조타입(class)은 Sendable 프로토콜을 준수해야 한다.

그럼 아까 코드가 아래와 같이 된다.

class VideoViewController: UIViewController {
    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let loadingSpinnerTask = Task.delayed(byTimeInterval: 0.15) {
            self.showLoadingSpinner()
        }

        Task {
            await prepareVideo()
            loadingSpinnerTask.cancel()
            hideLoadingSpinner()
        }
    }
    
    ...
}