iOS/iOS 지식

[iOS] 네트워크 연결 감지 해보기

홍복치 2024. 8. 3. 14:47

프로젝트 중에 네트워크가 끊어지는 상황에 대해서 생각해 보게 되었음

 

네트워크가 켜져 있지 않거나 비행기모드라면, 네트워크 연결이 필요한 부분(API 콜을 한다던가, 사진을 다운받는다던가) 하는 곳에서 에러가 날 것임. 하지만 이 부분에 대해서 예외처리가 되지 않는다면 사용자 경험측면에서 좋지 않다는 생각을 했음

 

그래서 네트워크 단절에 대한 처리를 해보게 되었음

일단 아래와 같은 NetworkView를 만들고 네트워크 통신이 일어나는 모든 뷰 컨트롤러에 추가해놓았음

그리고 아래와 같이 별도로 NetworkMonitor 클래스를 만들어서 startMonitoring을 호출하는 시점부터 계속해서 네트워크 상태를 감지하도록 했음

import Foundation
import Network

final class NetworkMonitor {
    static let shared = NetworkMonitor()
    
    private let queue = DispatchQueue.global()
    private let monitor = NWPathMonitor()
    
    var isConnected = false
    
    private init(){ }
    
    func startMonitoring(){
        monitor.start(queue: queue)
     
        monitor.pathUpdateHandler = { [weak self] path in
            
            if path.status == .satisfied {
                self?.isConnected = true
            }else{
                self?.isConnected = false
            }
        }
    }
    
     func stopMonitoring(){
        monitor.cancel()
    }
 
}

AppDelegate에서 처음 앱이 실행될 때마다 startMonitoring으로 네트워크 상태를 감지하게 하였음

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        NetworkMonitor.shared.startMonitoring()
        
        return true
    }

각 뷰컨트롤러에서는 NetworkMonitor의 의 isConnected를 확인해서 서버통신이 일어나기 전 확인을 하고, 만약 false라면 네트워크 단절에 대한 화면이 나오도록 하였음

 if NetworkMonitor.shared.isConnected {
 	네트워크 뷰 띄우기
} else{  API 호출  }

후 ... 길다. 여기까지 다 잘 되었네? 라고 생각할 수 있음

근데 여기서 문제가 발생함

 

1. 실시간적인 처리가 안됨

네트워크 통신하기 전에 항상 isConnected 값을 받아서 처리하고 있는데, 항상 네트워크 통신을 할때만 네트워크 상태를 감지할 수 있다는 것임. 그게 아니라 사용자가 중간에 네트워크를 꺼버려도, 토스트라던지 얼럿으로 알려주고 싶음

 

2.  모든 뷰 컨트롤러에 뷰를 얹어주어야 함

통신이 일어나는 뷰 컨트롤러가 100개면 100개를 다 얹어주어야 하는데, 이게 과연 효율적인 방법일까? 라는 생각을 하게 되었음

 

문제를 해결해보겠음

1번을 해결하기 위해서는 어떻게 해야할까?NWPathMonitor의 pathUpdateHandler는 연결상태를 가져옴

주석을 살펴보면 start를 호출하기 이전에는 호출되지 않다가, 그 이후에 네트워크 경로가 변경되면 호출된다고 나와있음

그렇기 때문에 이부분에서 네트워크가 없을때, 있을 때의 처리를 해주면 실시간으로 반영할 수 있지 않을까 생각을 하게되었음

 

2번을 해결해야 하는데, 어떻게 모든 뷰컨 위에 네트워크가 없을 때마다 표시해줄 수 있을 까 생각해보았음. 이것저것 찾아보다가 window를 사용해보자는 생각을 하게됨.

아래는 JDStatusBarNotification 라이브러리 샘플인데 뷰컨 위에 노티피케이션뷰를 띄우는 형태임. 대충 아래처럼 생김

코드를 몇 부분만 살펴보았는데, windowLevel을 statusBar로 설정하고 window의 rootViewController를 지정해서 사용하고 있음

이 코드와 비슷하게 네트워크 윈도우를 만들어서 띄워볼 수 있지 않을까 라는 생각을 하게 되었음

이제 코드를 수정해보겠음.

위의 코드처럼 윈도우를 상속받는 네트워크 윈도우를 만듬. 

import UIKit

final class NetworkWindow: UIWindow {
    private let networkViewController = NetworkViewController()
    
    override init(windowScene: UIWindowScene) {
        super.init(windowScene: windowScene)
        
        rootViewController = networkViewController
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        windowLevel = .statusBar
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

윈도우의 레벨을 statusBar로 설정한 이유는 레벨이 높을수록 위에 쌓이기 때문임. 설정하지 않으면 normal이기 때문에 statusBar로 레벨을 높여서 기존 윈도우 위에 쌓이도록 함

 

NetworkMonitor 클래스에서 네트워크 상태에 따라 window를 추가하고 제거하는 함수인 presentNetworkWindow(), dismissNetworkWindow()를 만듬

 

네트워크가 연결되어있지 않으면

 - NetworkWindow 생성

 - 키윈도우로 설정해서(makeKeyAndVisible) 화면을 표시하고, 같은레벨 or 그보다 낮은 레벨의 가장 위의 나오도록 함

 

네트워크가 연결되어있고, 다시시도 버튼을 누르면

- NetworkWindow 인스턴스를 nil로 설정함

final class NetworkMonitor {
    static let shared = NetworkMonitor()
    
    private let queue = DispatchQueue.global()
    private let monitor = NWPathMonitor()
    
    var isConnected = false
    
    private init(){ }
    
    func startMonitoring(){
        monitor.start(queue: queue)
        
        monitor.pathUpdateHandler = { [weak self] path in
            if path.status == .satisfied {
                self?.isConnected = true
            }else{
                self?.isConnected = false
                self?.presentNetworkWindow()
            }
        }
    }
    
    func stopMonitoring(){
        monitor.cancel()
    }
    
    func presentNetworkWindow(){
        DispatchQueue.main.async {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                let networkWindow = NetworkWindow(windowScene: windowScene)
                networkWindow.makeKeyAndVisible()
                
                let sceneDelegate = windowScene.delegate as? SceneDelegate
                sceneDelegate?.networkWindow = networkWindow
            }
        }
    }
    
    func dismissNetworkWindow(){
        DispatchQueue.main.async {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                let sceneDelegate = windowScene.delegate as? SceneDelegate
                sceneDelegate?.networkWindow = nil
            }
        }
    }
    
}
    @objc private func retryButtonClicked(){
        if NetworkMonitor.shared.isConnected {
            NetworkMonitor.shared.dismissNetworkWindow()
        }
    }

아래처럼 동작하는 것을 확인할 수 있음