19 November 2022

UIViewController에서 present, dismiss 함수는 completion Block을 통해 완료 후 어떤 동작을 수행할지 주입할 수 있습니다.

하지만 Closure 코드를 작성함에 있어 코드의 복잡도는 증가합니다.

func routeNewViewController() {
  let vc = ViewController()
  present(vc, animated: true) { [weak self] in self?.something() }
}

func dismissViewController() {
  vc.dismiss(animated: true) { [weak self] in self?.something() }
}

Swift의 Concurrency에서 Closure 코드를 async로 만들어주는 withCheckedContinuation(function:_:) 함수를 이용해 래핑한 코드를 만들 수 있습니다.

import UIKit

protocol SuspendableViewControllerProtocol {
  /// UIViewController의 present 함수를 async 형태로 구현
  ///
  /// PresentedViewController의 ViewDidAppear 호출까지 await 후 진행
  ///
  /// ```
  /// Task { @MainActor [weak self] in
  ///   let newVC = NewViewController()
  ///   await self?.presentAsync(newVC, animated: true)
  ///   print("present completion")
  /// }
  ///
  /// // Output :
  /// PresentedViewController viewDidLoad
  /// PresentedViewController viewWillAppear
  /// PresentedViewController viewDidAppear
  /// present completion
  /// ```
  @MainActor
  func presentAsync(_ viewControllerToPresent: UIViewController, animated flag: Bool) async
  /// UIViewController의 dismiss 함수를 async 형태로 구현
  ///
  /// PresentedViewController의 viewDidDisappear 호출까지 await 후 진행
  ///
  /// ```
  /// Task { @MainActor [weak self] in
  ///   await self?.dismiss(animated: true)
  ///   print("dismiss completion")
  /// }
  ///
  /// // Output :
  /// PresentedViewController viewWillDisappear
  /// PresentedViewController viewDidDisappear
  /// dismiss completion
  /// ```
  @MainActor
  func dismissAsync(animated flag: Bool) async
}

extension SuspendableViewControllerProtocol where Self: UIViewController {
  @MainActor
  func presentAsync(_ viewControllerToPresent: UIViewController, animated flag: Bool) async {
    if Task.isCancelled { return }
    return await withCheckedContinuation { continuation in
      present(viewControllerToPresent, animated: flag, completion: {
        continuation.resume(returning: ())
      })
    }
  }
  
  @MainActor
  func dismissAsync(animated flag: Bool) async {
    if Task.isCancelled { return }
    return await withCheckedContinuation { continuation in
      dismiss(animated: flag, completion: {
        continuation.resume(returning: ())
      })
    }
  }
}

extension UIViewController: SuspendableViewControllerProtocol {}

그러면 우리는 위 presentAsync, dismissAsync 함수를 이용하여, Task 내에서 다음 수행할 코드를 작성할 수 있습니다.

예제

버튼을 눌러 BlueViewController를 띄우고, 닫는 일반적인 예제 코드입니다.

class ViewController: UIViewController, BlueViewControllerDelegate {
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    
    let blueBtn = UIButton()
    blueBtn.setTitle("Show BlueViewController", for: .normal)
    blueBtn.sizeToFit()
    blueBtn.frame.origin = .init(x: 100, y: 100)
    blueBtn.addTarget(self, action: #selector(showBlueViewController), for: .touchUpInside)
    blueBtn.setTitleColor(.systemBlue, for: .normal)
    
    view.addSubview(blueBtn)
  }

  @objc func showBlueViewController() {
    let vc = BlueViewController()
    vc.listener = self
    present(vc, animated: true) { print("presented BlueViewController") }
  }
  
  func dismissBlueViewController() {
    dismiss(animated: true) { print("dismissed BlueViewController") }
  }
}

protocol BlueViewControllerDelegate: AnyObject {
  func dismissBlueViewController()
}

class BlueViewController: UIViewController {
  weak var listener: BlueViewControllerDelegate?

  override func viewDidLoad() {
    print(Self.self, #function)
    super.viewDidLoad()
    view.backgroundColor = .systemBlue
    
    let dismissBtn = UIButton()
    dismissBtn.setTitle("Show BlueViewController", for: .normal)
    dismissBtn.sizeToFit()
    dismissBtn.frame.origin = .init(x: 100, y: 100)
    dismissBtn.addTarget(self, action: #selector(close), for: .touchUpInside)
    dismissBtn.backgroundColor = .systemBlue
    
    view.addSubview(dismissBtn)
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    print(Self.self, #function)
  }
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    print(Self.self, #function)
  }
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print(Self.self, #function)
  }
  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    print(Self.self, #function)
  }
  
  @objc func close() {
    listener?.dismissBlueViewController()
  }
}



위 코드에서 BlueViewController를 띄우고, 닫으면 다음과 같이 콘솔 로그가 출력됩니다.

BlueViewController viewDidLoad()
BlueViewController viewWillAppear(_:)
BlueViewController viewDidAppear(_:)
presented BlueViewController
BlueViewController viewWillDisappear(_:)
BlueViewController viewDidDisappear(_:)
dismissed BlueViewController

present의 completion Block은 viewDidAppear이 호출된 후 호출이 되고, dismiss의 completion Block은 viewDidDisappear이 호출된 후에 호출됨을 알 수 있습니다.

present와 dismiss 함수를 presentAsync, dismissAsync로 변경하여 코드를 작성해봅시다.

class ViewController: UIViewController, BlueViewControllerDelegate {
  private var task: Task<(), Never>?
  
  deinit { task?.cancel() }

  override func viewDidLoad() {
    ...
  }

  @objc func showBlueViewController() {
    task = Task { @MainActor [weak self] in
      let vc = BlueViewController()
      vc.listener = self
      await self?.presentAsync(vc, animated: true)
      print("presented BlueViewController")
    }
  }
  
  func dismissBlueViewController() {
    task = Task { @MainActor [weak self] in
      await self?.dismissAsync(animated: true)
      print("dismissed BlueViewController")
    }
  }
}

이전에 completion Block에 코드를 작성하던 것보다 간결하게 작성할 수 있었습니다.

또한, 변경한 코드로 호출했을 때 기존과 동일한 결과가 출력됨을 확인할 수 있습니다.

BlueViewController viewDidLoad()
BlueViewController viewWillAppear(_:)
BlueViewController viewDidAppear(_:)
presented BlueViewController
BlueViewController viewWillDisappear(_:)
BlueViewController viewDidDisappear(_:)
dismissed BlueViewController

참고자료