본문 바로가기

iOS/니카내카

[니카내카] 디바이스의 네트워크 연결 상태를 확인하려면?

728x90

카카오톡과 같은 앱의 경우 네트워크가 연결되어 있어야 서비스가 제대로 작동할 수 있다. 

 

니카내카를 개발하다가 네트워크 상태에 따라서 서버 데이터가 제대로 오지 않았을 때 유저가 인터랙션을 하게 되면 앱이 강제종료가 되는 이슈가 나타난다. 

그래서 앱에 진입했을 때 현재 디바이스의 네트워크 연결 상태를 받아, 연결이 되지 않았다면 팝업창을 띄우고 그렇지 않은 경우에는 메인 화면 (지도)로 이동할 수 있도록 분기처리했다.

 

🤔 고민인 부분 

앱을 최초 실행했을 때,

✅ 스플래쉬에서 분기처리하는 것은 가능하지만

❌ background -> foreground로 이동했을 때에도 네트워크 상태를 확인하고 싶은데 .. 아직 못해서 .. 고민중 ..

 

 

Network Manager File

네트워크 상태를 하나의 화면에서 확인하는 것이 아니라, 여러 화면에서 확인을 해야 하므로 싱글톤 패턴으로 구현했다.

 

import Foundation
import Network

enum ConnectionType {
    case wifi
    case cellular
    case ethernet
    case unknown
}

final class NetworkConnectionStatus {
    
    static let shared = NetworkConnectionStatus()
    
    private let queue = DispatchQueue.global()
    private let monitor: NWPathMonitor
    
    public private(set) var isConnected: Bool = false
    public private(set) var connectionType: ConnectionType = .unknown
    
    typealias completionHanlder = (Bool) -> Void

    // monotior 초기화
    private init() {
        monitor = NWPathMonitor()
    }

    // Network Monitoring 시작
    func startMonitoring(completionHandler: @escaping completionHanlder) {
        monitor.start(queue: queue)
        monitor.pathUpdateHandler = { path in

            self.isConnected = path.status == .satisfied
            self.getConnectionType(path)

            completionHandler(self.isConnected)
        }
    }

    // Network Monitoring 종료
    func stopMonitoring() {
        monitor.cancel()
    }

    // Network 연결 타입
    private func getConnectionType(_ path: NWPath) {
        if path.usesInterfaceType(.wifi) {
            connectionType = .wifi
        } else if path.usesInterfaceType(.cellular) {
            connectionType = .cellular
        } else if path.usesInterfaceType(.wiredEthernet) {
            connectionType = .ethernet
        } else {
            connectionType = .unknown
        }
    }
}

 

Splash 

앱을 진입하면 스플래쉬 화면을 보여주게 된다.

그리고 스플래쉬 화면에서 애니메이션이 play되고, 애니메이션이 동작한 이후로 화면전환 전에 네트워크 상태를 분기처리하고 각 상태에 맞게 화면을 보여주면 된다.

 

final class SplashViewController: BaseViewController {
    
    // MARK: - UI Property
    
    private let firstLabel = UILabel().then {
        $0.text = "니"
        $0.textColor = R.Color.black200
        $0.font = NiCarNaeCarFont.title0.font
        $0.alpha = 0
    }
    
    private let secondLabel = UILabel().then {
        $0.text = "카"
        $0.textColor = R.Color.green100
        $0.font = NiCarNaeCarFont.title0.font
        $0.alpha = 0
    }
    
    private let thirdLabel = UILabel().then {
        $0.text = "내"
        $0.textColor = R.Color.black200
        $0.font = NiCarNaeCarFont.title0.font
        $0.alpha = 0
    }
    
    private let fourthLabel = UILabel().then {
        $0.text = "카"
        $0.textColor = R.Color.blue100
        $0.font = NiCarNaeCarFont.title0.font
        $0.alpha = 0
    }
    
    // MARK: - Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setAnimation() 1️⃣
    }
    
    // MARK: - UI Method
    
    override func configureUI() {
        super.configureUI()
        view.backgroundColor = R.Color.white
    }
    
    override func setLayout() {
        view.addSubviews(firstLabel, secondLabel, thirdLabel, fourthLabel)

        firstLabel.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).inset(321)
            make.leading.equalTo(view.safeAreaLayoutGuide).inset(32)
        }

        secondLabel.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).inset(303)
            make.leading.equalTo(firstLabel.snp.trailing)
        }

        thirdLabel.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).inset(321)
            make.leading.equalTo(secondLabel.snp.trailing)
        }

        fourthLabel.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).inset(303)
            make.leading.equalTo(thirdLabel.snp.trailing)
        }
    }
    
    // MARK: - Custom Method
    
    private func checkDeviceNetworkStatus() {
        NetworkConnectionStatus.shared.startMonitoring { isConnected in 3️⃣
            if isConnected { 4️⃣
                print("🟢 네트워크 연결")
                DispatchQueue.main.async {
                    if UserDefaults.standard.bool(forKey: Constant.UserDefaults.isNotFirst) {
                        let viewController = UINavigationController(rootViewController: MainMapViewController())
                        self.transition(viewController, transitionStyle: .presentCrossDissolve)
                    } else {
                        let viewController = UINavigationController(rootViewController: OnboardingViewController())
                        self.transition(viewController, transitionStyle: .presentCrossDissolve)
                    }
                }
                
            } else { 5️⃣
                print("🟠 네트워크 연결 해제!")
                DispatchQueue.main.async {
                    self.presentAlert()
                }
            }
        }
    }
    
    private func setAnimation() {
        showLabel(firstLabel) {
            self.showLabel(self.secondLabel) {
                self.showLabel(self.thirdLabel) {
                    self.showLabel(self.fourthLabel) {
                        
                        self.checkDeviceNetworkStatus() 2️⃣
                        
                    }
                }
            }
        }
    }
    
    private func showLabel(_ component: UILabel, completion: @escaping () -> Void) {
        UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) {
            component.transform = CGAffineTransform(translationX: 0, y: -16)
            component.alpha = 1
        } completion: { _ in
            completion()
        }
    }
    
    private func presentAlert() {
        let alertController = UIAlertController(
            title: "네트워크에 접속할 수 없습니다.",
            message: "네트워크 연결 상태를 확인해주세요.",
            preferredStyle: .alert
        )
        
        let endAction = UIAlertAction(title: "종료", style: .destructive) { _ in
            // 앱 종료
            UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                exit(0)
            }
        }
        
        let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in
            // 설정앱 켜주기
            guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url)
            }
        }
        
        alertController.addAction(endAction)
        alertController.addAction(confirmAction)
        self.present(alertController, animated: true, completion: nil)
    }
}

 

위의 코드를 통해서 알 수 있듯, 

  1. 스플래쉬 화면에 진입해서
  2. 스플래쉬 애니메이션이 실행된 다음
  3. 네트워크 상태를 받아와서
  4. 만약 연결이 되어 있다면 메인화면으로 이동하고
  5. 그렇지 않다면 팝업창을 띄우도록 한다. 

 

여기서 팝업창을 띄우는 코드는 UIAlertAction을 통해서 구현했다.

    private func presentAlert() {
        let alertController = UIAlertController(
            title: "네트워크에 접속할 수 없습니다.",
            message: "네트워크 연결 상태를 확인해주세요.",
            preferredStyle: .alert
        )
        
        let endAction = UIAlertAction(title: "종료", style: .destructive) { _ in
            // 앱 종료
            UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                exit(0)
            }
        }
        
        let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in
            // 설정앱 켜주기
            guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url)
            }
        }
        
        alertController.addAction(endAction)
        alertController.addAction(confirmAction)
        self.present(alertController, animated: true, completion: nil)
    }

 

 


저거 위에 고민은 어케 저케 해보고 있긴한데 제법 ... 어렵다 ..