NWPathMonitor로 네트워크 에러를 감지하고, UI 업데이트하기

서론

  • 앱 개발자로서 안정적이고 끊임없는 네트워크 연결을 기대합니다. 하지만, 네트워크 연결은 언제나 변수가 존재
  • 이런 예측할 수 없는 변수들 속에서 사용자에게 명확한 UI를 제공하는 것은 당연
  • 이번 글에서는 Network 프레임워크의 NWPathMonitor라는 것을 이용하여 기본 원리와 사용 방법을 소개

NWPathMonitor

  • NWPathMonitor는 Network 프레임워크에 포함된 클래스
  • 글자 그대로 Apple이 제공하는 다양한 플랫폼에서 네트워크 연결성을 모니터링할 수 있는 역할, 뿐만 아니라 인터넷 연결, 셀룰러 데이터 인지 파악
  • 각 다양한 인터페이스마다 다양한 적절히 조치를 취하는 것이 가능

Singleton 객체 만들기

final class NetworkCheck {
	static let shared = NetworkCheck()
    private let queue = DispatchQueue.global()
  	private let monitor: NWPathMonitor
  
 	private init() {
    	self.monitor = NWPathMonitor()
  	}
}
  • 먼저 NetworkCheck라는 Singleton 디자인으로 구현된 클래스를 제작. 왜냐하면 네트워크 연결 상태를 체크하는 것은 앱을 대표해서 1개로 족하기 때문
  • 네트워크를 체크하는 것은 항상 비동기로 이루어져야함. 왜냐하면 언제, 어디서 네트워크 연결이 불안정해지기 때문에 그것을 대비해서 Main Thread보다는 Global Thread에서 감지하는 것이 당연
  • 이제 NetworkCheck가 초기화되면 NWPathMonitor를 초기화

네트워크 감지 시작하기

final class NetworkCheck {
    static let shared = NetworkCheck()
    private let queue = DispatchQueue.global()
    private let monitor: NWPathMonitor
  
    // monotior 초기화
    private init() {
      self.monitor = NWPathMonitor()
    }
  
    // Network Monitoring 시작
    public func startMonitoring() {
      self.monitor.start(queue: queue)
      self.monitor.pathUpdateHandler = { path in 
	    
      }
    }
}
  • startMonitoring() 메서드에서 먼저 monitor의 start(queue:)를 호출, 여기에는 우리가 먼저 정의한 Global Queue를 주입
  • 이제 네트워크 변경이 감지되면, monitor의 pathUpdateHandler를 통해 클로저로 반환
  • 우리는 이 클로저 로직에 UI를 업데이트하는 로직을 구현

UI 관련된 프로퍼티 준비하기

final class NetworkCheck {
  	// ...
	
    public private(set) var isConnected: Bool = false
    public private(set) var topVCIsNetworkVC: Bool = false
	
    private var topVC: UIViewController? {
      let scenes = UIApplication.shared.connectedScenes
      let windowScene = scenes.first as? UIWindowScene
      let window = windowScene?.windows.first
      return window?.rootViewController
    }
  
	// ...
}
  • isConnected는 현재 네트워크 연결 상태를 저장해주는 Boolean 프로퍼티입니다. 왜냐하면 pathUpdateHandler의 path는 여러 번 호출되어서 이것을 단 한 번의 시그널로 바꿔주는 역할을 합니다.
  • topVCIsNetworkVC는 현재 가장 상위의 있는 ViewController가 NetworkVC인지 판단합니다. NetworkVC는 네트워크 연결이 불안정할 때 나타나는 ViewController입니다.
  • topVC는 현재 상위 뷰에 있는 ViewController를 참조할 수 있는 프로퍼티입니다. NetworkCheck 클래스는 UI와 완전히 독립되어 있는 객체이기떄문에, UIApplication.shared를 참조하여 다른 화면으로 이동할 수 있도록 하는 징검다리 역할을 합니다.

pathUpdateHandler 로직 구성하기

final class NetworkCheck {
    // ...
  
    // Network Monitoring 시작
    public func startMonitoring() {
      self.monitor.start(queue: queue)
      self.monitor.pathUpdateHandler = { [weak self] path in
        guard let self = self else { return }

        self.isConnected = path.status == .satisfied
      
        if self.isConnected && self.topVCIsNetworkVC {
          // 네트워크와 연결됨, 제일 위에 뷰가 NetworkVC임. -> NetworkVC 사라지기
          self.dismissNetworkVC()
        }

        if self.isConnected == false && self.topVCIsNetworkVC == false {
          // 네트워크와 연결안됨, 제일 위에 뷰가 NetworkVC가 아님. -> NetworkVC 보여주기
          self.presentNetworkVC()
        }
      }
    }
  
    // ...
}
  • path.status는 NWPath.Status라는 enum타입을 가지고 있습니다.
  • satisfied는 네트워크 연결이 정상적으로 연결되어 있는 경우를 말합니다.
  • unsatisfied는 네트워크 연결이 끊겨있는 상태입니다.
  • requiresConnection는 네트워크 연결을 시도하고 있는 상태입니다.
  • 이 세 가지 경우 중 satisfied를 제외한 나머지 경우는 모두 네트워크 연결이 정상적이기 않기때문에, satisfied 상태만 고려를 해주었습니다.
  • self.isConnected = path.status == .satisfied를 통해 self.isConnected의 상태 값을 변경해줍니다.
final class NetworkCheck { 
	// ...
	
  private func presentNetworkVC() {
    DispatchQueue.main.async {
      let networkViewController = NetworkViewController()
      networkViewController.modalPresentationStyle = .overFullScreen

      guard let topVC = self.topVC else { return }
      topVC.present(networkViewController, animated: true)
      self.topVCIsNetworkVC = true
    }
  }

  private func dismissNetworkVC() {
    DispatchQueue.main.async {
      guard let topVC = self.topVC else { return }
      topVC.dismiss(animated: true)
      self.topVCIsNetworkVC = false
    }
  }
  
  // ...
}
  • 만약 isConnected가 False고, 제일 최상위 VC가 NetworkVC가 아닐 경우에는 NetworkVC를 보여줍니다.
  • 만약 isConnected가 True고, 제일 최상위 VC가 NetworkVC면 NetworkVC를 사리지게합니다.