0

Network Reachability in Swift

Hầu như mọi ứng dụng di động đều cần kết nối với internet vào một thời điểm nào đó để lấy dữ liệu từ một host, service hoặc upload dữ liệu. Tuy nhiên, kết nốt Internet không phải lúc nào cũng có sẵn, và tính khả dụng của nó có thể thay đổi bất cứ lúc nào. Để biết được trạng thái của mạng hiện tại của hệ thống và kiểm tra xem một máy chủ hoặc dịch vụ có thể được truy cập thông qua internet được không, chúng ta có thể sử dụng SCNetworkReachability API.

The SCNetworkReachability API

API SCNetworkReachability cung cấp một phương pháp đồng bộ để xác định khả năng truy cập. Phương pháp đồng bộ này cho phép chúng ta request trạng thái hiện tại của việc truy cập bằng cách gọi hàm SCNetworkReachabilityGetFlags. Thông số thứ hai của function này là một con trỏ tới bộ nhớ, nơi chứa các cờ mô tả trạng thái truy cập và cung cấp thêm thông tin như kết nối có khả dụng và liệu người dùng có thể thiết lập kết nối được không. Ngoài phương pháp đồng bộ, SCNetworkReachability API còn cung cấp một phương pháp không đồng bộ. Để thực hiện phương pháp tiếp cận này, chúng ta phải sắp xếp đối tượng SCNetworkReachability trong một vòng lặp chạy. Với việc cung cấp chức năng callback, chúng ta có thể gửi một notification bất cứ khi nào khả năng truy cập đến máy chủ thay đổi. Launch Xcode 8 và tạo một Swift Single View Application project. Đặt tên là ReachabilityExample. Ta tạo một file swift tên là Reachability.swift. Sau đó thêm vào file này dòng lệnh import sau:

import SystemConfiguration

Chúng ta muốn thông báo cho app khi trạng thái mạng thay đổi trong ba trường hợp sau Khi ứng dụng không được kết nối, Khi ứng dụng được kết nối thông qua Wifi, Khi ứng dụng được kết nối thông qua WWAN. Để làm được như vậy, chúng ta cần gửi notification có chứa trạng thái kết nối.

let ReachabilityDidChangeNotificationName = "ReachabilityDidChangeNotification"
 
enum ReachabilityStatus {
case notReachable
case reachableViaWiFi
case reachableViaWWAN
}

Tiếp theo là class Reachability

class Reachability: NSObject {
 
}

Sau đó chúng ta thêm vào class này một thuộc tính để lưu trữ đối tượng SCNetworkReachability

private var networkReachability: SCNetworkReachability?

Trong các trường hợp chúng ta muốn theo dõi khả năng truy cập đến host cụ thể nào đó. Chúng ta sẽ tạo một hàm init với tham số là host name và tạo một đối tượng SCNetworkReachability với function SCNetworkReachabilityCreateWithName. Hàm này có thể trả về nil nếu nó không thể tạo đối tượng SCNetworkReachability hợp lệ.

init?(hostName: String) {
    networkReachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, (hostName as NSString).UTF8String)
    super.init()
    if networkReachability == nil {
    return nil
    }
}

Chúng ta cần thêm một hàm init bổ sung cho trường hợp muốn check khả năng truy cập vào một địa chỉ mạng. Trong trường hợp này chúng ta sẽ sử dụng function SCNetworkReachabilityCreateWithAddress. Khi function này mong đợi một con trỏ tới một địa chỉ mạng, chúng ta sẽ gọi nó trong function withUnsafePointer. Cũng trong trường hợp này, chúng ta cần làm cho init cho trường hợp không thực hiện được, vì cũng có khả năng trả lại một giá trị null như đã giải thích trước đây:

init?(hostAddress: sockaddr_in) {
    var address = hostAddress
 
    guard let defaultRouteReachability = withUnsafePointer(to: &address, {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
            SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, $0)
        }
    }) else {
        return nil
    }
 
    networkReachability = defaultRouteReachability
 
    super.init()
    if networkReachability == nil {
        return nil
    }
}

Để thuận tiện, ta tạo ra một vài class method. Method đầu tiên tạo ra một instance để control việc kết nối với internet. Method thứ hai cho phép kiểm tra chúng ta kết nối được với local wifi hay không. Cả hai method này đều sử dụng các hàm khới tạo đã viết ở trên.

static func networkReachabilityForInternetConnection() -> Reachability? {
    var zeroAddress = sockaddr_in()
    zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
    zeroAddress.sin_family = sa_family_t(AF_INET)
    return Reachability(hostAddress: zeroAddress)
}
 
static func networkReachabilityForLocalWiFi() -> Reachability? {
    var localWifiAddress = sockaddr_in()
    localWifiAddress.sin_len = UInt8(MemoryLayout.size(ofValue: localWifiAddress))
    localWifiAddress.sin_family = sa_family_t(AF_INET)
    // IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0 (0xA9FE0000).
    localWifiAddress.sin_addr.s_addr = 0xA9FE0000
 
    return Reachability(hostAddress: localWifiAddress)
}

Bây giờ chúng ta cần một vài method để bắt đầu hoặc ngừng notification và một thuộc tính để lưu trữ việc chuyển đổi state

private var notifying: Bool = false

Để bắt đầu notify, đầu tiên ta check xem việc thông báo có đang thực hiện hay không. Sau đó, ta có một SCNetworkReachabilityContext và gán seft vào tham số info của nó. Tiếp theo, ta thiết lập một hàm callback, truyền đi cùng SCNetworkReachabilityContext (Khi callback được gọi, tham số info có chứa một reference đến self sẽ được truyền như con trỏ đến một data block như là một tham số thứ 3). Nếu thiết lập được callback function thành công, ta có thể schedule network reachability reference.

func startNotifier() -> Bool {
 
    guard notifying == false else {
        return false
    }
 
    var context = SCNetworkReachabilityContext()
    context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
 
    guard let reachability = networkReachability, SCNetworkReachabilitySetCallback(reachability, { (target: SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutableRawPointer?) in
        if let currentInfo = info {
            let infoObject = Unmanaged<AnyObject>.fromOpaque(currentInfo).takeUnretainedValue()
            if infoObject is Reachability {
                let networkReachability = infoObject as! Reachability
                NotificationCenter.default.post(name: Notification.Name(rawValue: ReachabilityDidChangeNotificationName), object: networkReachability)
            }
        }
    }, &context) == true else { return false }
 
    guard SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) == true else { return false }
 
    notifying = true
    return notifying
}

Để ngừng notify, ta chỉ cần unschedule network reachability reference

func stopNotifier() {
    if let reachability = networkReachability, notifying == true {
        SCNetworkReachabilityUnscheduleFromRunLoop(reachability, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode as! CFString)
        notifying = false
    }
}

Chúng ta cũng nên chắc chắn rằng chúng ta ngừng thông báo trước khi đối tượng Reachability được deallocated:

deinit {
    stopNotifier()
}

Để biết trạng thái của khả năng truy cập mạng, chúng ta có thể định nghĩa một thuộc tính nhận được các flags hiện tại của đối tượng SCNetworkReachability:

private var flags: SCNetworkReachabilityFlags {
 
    var flags = SCNetworkReachabilityFlags(rawValue: 0)
 
    if let reachability = networkReachability, withUnsafeMutablePointer(to: &flags, { SCNetworkReachabilityGetFlags(reachability, UnsafeMutablePointer($0)) }) == true {
        return flags
    }
    else {
        return []
    }
}

Bây giờ ta sẽ tạo ra một method để trả về trạng thái reachability

var currentReachabilityStatus: ReachabilityStatus {
 
    if flags.contains(.reachable) == false {
        // The target host is not reachable.
        return .notReachable
    } 
    else if flags.contains(.isWWAN) == true {
        // WWAN connections are OK if the calling application is using the CFNetwork APIs.
        return .reachableViaWWAN
    } 
    else if flags.contains(.connectionRequired) == false {
        // If the target host is reachable and no connection is required then we'll assume that you're on Wi-Fi...
        return .reachableViaWiFi
    } 
    else if (flags.contains(.connectionOnDemand) == true || flags.contains(.connectionOnTraffic) == true) && flags.contains(.interventionRequired) == false {
        // The connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs and no [user] intervention is needed
        return .reachableViaWiFi
    } 
    else {
        return .notReachable
    }
}

Cuối cùng ta có thể tạo ra một method để xác định reachability status của các cờ hiện tại trong network reachability reference, và một thuộc tính để xác minh chúng ta đã kết nối hay chưa:

var isReachable: Bool {
    switch currentReachabilityStatus {
    case .notReachable:
        return false
    case .reachableViaWiFi, .reachableViaWWAN:
        return true
    }
}

Sử dụng Reachability như thế nào?

Sử dụng lớp Reachability mới rất đơn giản: Bạn tạo một instance mới và bắt đầu notify. Bây giờ, ta sẽ build một ví dụ đơn giản để kiểm tra lớp Reachability của chúng ta. Ta sẽ thay đổi màu sắc của viewcontroller thành màu xanh lá cây khi có kết nối Internet và màu đỏ nếu không có kết nối internet Mở file ViewController.swift và thêm thuộc tính mới vào lớp này:

import UIKit
 
class ViewController: UIViewController {
 
    var reachability: Reachability? = Reachability.reachabilityForInternetConnection()

Trong method viewDidLoad (), ta thêm một observer cho reachability notification.

override func viewDidLoad() {
    super.viewDidLoad()
 
    NotificationCenter.default.addObserver(self, selector: #selector(reachabilityDidChange(_:)), name: NSNotification.Name(rawValue: ReachabilityDidChangeNotificationName), object: nil)
 
    _ = reachability?.startNotifier()
}

Trong Deinit ta stop notifier

deinit {
    NotificationCenter.default.removeObserver(self)
    reachability?.stopNotifier()
}

Trong method viewWillAppear(), ta kiểm tra khả năng truy cập

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    checkReachability()
}
 
func checkReachability() {
    guard let r = reachability else { return }
    if r.isReachable  {
        view.backgroundColor = UIColor.green
    } else {
        view.backgroundColor = UIColor.red
    }
}

Khi ta nhận được notification, viewController sẽ thực hiện method sau:

func reachabilityDidChange(_ notification: Notification) {
    checkReachability()
}

Tất nhiên bạn cũng có thể sử dụng nó với một host. Chỉ cần sử dụng initializer tương ứng, ví dụ:

var reachability = Reachability(hostName: "www.apple.com")

Hy vọng rằng bài viết của mình sẽ giúp các bạn có một sự hiểu hơn về cách thức hoạt động của SystemConfiguration Framework.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí