インターネット
 Computer >> コンピューター >  >> ネットワーキング >> インターネット

Swiftでインターネット接続を検出・管理する方法:実践ガイド

著者:Neo Ighodaro

モバイルアプリケーションの多くは、正常に動作するためにアクティブなインターネット接続を必要とします。しかし、ネットワーク接続が一時的に失われるのはごく自然なことです。そのような状況に備え、開発者はユーザー体験を損なわないような工夫をするか、少なくともユーザーに状態を通知する仕組みを用意しておく必要があります。

この記事では、Swiftでインターネット接続の問題を検出する方法と、それに対処するためのいくつかの実装パターンを紹介します。

まず、今回構築するサンプルアプリケーションが、さまざまなネットワーク接続シナリオにどのように対応するのかを見てみましょう。

Swiftでインターネット接続を検出・管理する方法:実践ガイド

必要な環境

この記事の内容に沿って進めるには、以下の環境が必要です。

  • マシンにインストールされたXcode
  • Swiftプログラミング言語に関する基礎知識
  • マシンにインストールされたCocoaPods

上記の準備が整ったら、さっそく始めましょう。

ワークスペースのセットアップ

まずはPlaygroundとなる新しいプロジェクトを作成します。ここにすべてのユースケースを記述し、処理を実装していきます。

Swiftには接続状態を検出するためのReachability実装が標準で用意されていますが、今回はサードパーティ製のライブラリを使用します。その理由は、組み込みのAPIよりも扱いやすく、より表現力豊かなAPIが提供されているためです。

Xcodeを開き、新しいプロジェクトを作成してください。

Swiftでインターネット接続を検出・管理する方法:実践ガイド

このプロジェクトは、自由に実験できるシンプルなPlaygroundとして使用します。

接続がオフラインになったタイミングを検出するために、Reachability.swiftパッケージを使用します。これは「AppleのReachabilityをクロージャを使ってSwiftで書き直した代替ライブラリ」です。

ターミナルを開き、以下のコマンドを実行します。

$ pod init

これにより、CocoaPodsの依存関係を宣言するためのPodfileが生成されます。Podfileを開き、内容を以下のコードに置き換えてください。

platform :ios, '9.0'

target 'project_name' do
  use_frameworks!
  pod 'ReachabilitySwift'
  pod 'Alamofire'
end

project_nameの部分は、必ず自分のプロジェクト名に置き換えてください。

ファイルを保存し、以下のコマンドを実行してPodをプロジェクトにインストールします。

$ pod install

インストールが完了したら、プロジェクトのルートにある*.xcworkspaceファイルを開きます。Xcodeが起動します。

NetworkManagerクラスの作成

次に、NetworkManagerという新しいクラスを作成します。このクラスはネットワークの状態を保持し、Reachabilityパッケージへのシンプルなプロキシとして機能します。ファイルに以下のコードを貼り付けてください。

import Foundation
import Reachability

class NetworkManager: NSObject {

    var reachability: Reachability!

    static let sharedInstance: NetworkManager = {
        return NetworkManager()
    }()

    override init() {
        super.init()

        // Reachabilityの初期化
        reachability = Reachability()!

        // ネットワーク状態変更のオブザーバーを登録
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(networkStatusChanged(_:)),
            name: .reachabilityChanged,
            object: reachability
        )

        do {
            // ネットワーク状態の監視を開始
            try reachability.startNotifier()
        } catch {
            print("Unable to start notifier")
        }
    }

    @objc func networkStatusChanged(_ notification: Notification) {
        // グローバルに行いたい処理をここに記述!
    }

    static func stopNotifier() -> Void {
        do {
            // ネットワーク状態の監視を停止
            try (NetworkManager.sharedInstance.reachability).stopNotifier()
        } catch {
            print("Error stopping notifier")
        }
    }

    // ネットワークに到達可能な場合
    static func isReachable(completed: @escaping (NetworkManager) -> Void) {
        if (NetworkManager.sharedInstance.reachability).connection != .none {
            completed(NetworkManager.sharedInstance)
        }
    }

    // ネットワークに到達不可能な場合
    static func isUnreachable(completed: @escaping (NetworkManager) -> Void) {
        if (NetworkManager.sharedInstance.reachability).connection == .none {
            completed(NetworkManager.sharedInstance)
        }
    }

    // モバイル回線(WWAN)経由で到達可能な場合
    static func isReachableViaWWAN(completed: @escaping (NetworkManager) -> Void) {
        if (NetworkManager.sharedInstance.reachability).connection == .cellular {
            completed(NetworkManager.sharedInstance)
        }
    }

    // Wi-Fi経由で到達可能な場合
    static func isReachableViaWiFi(completed: @escaping (NetworkManager) -> Void) {
        if (NetworkManager.sharedInstance.reachability).connection == .wifi {
            completed(NetworkManager.sharedInstance)
        }
    }
}

上記のクラスでは、ネットワーク状態の監視をすぐに始められるよう、いくつかのヘルパー関数を定義しています。sharedInstanceはシングルトンとして定義されており、NetworkManagerクラスのインスタンスを複数生成したくない場合に利用できます。

initメソッド内では、Reachabilityのインスタンスを生成した後、NotificationCenterクラスを使って通知を登録しています。これにより、ネットワーク状態が変化するたびに、NotificationCenterに指定したコールバック(networkStatusChanged)が呼び出されるようになります。この仕組みを利用すれば、ネットワークが切断された際に発動させるグローバルな処理を実装できます。

さらに、インターネット接続の状態に応じてコードを実行する際に便利なヘルパー関数も定義しました。isReachableisUnreachableisReachableViaWWANisReachableViaWiFiの4つです。

これらのヘルパーの使い方は、一般的に以下のようになります。

NetworkManager.isReachable { networkManagerInstance in
    print("Network is available")
}

NetworkManager.isUnreachable { networkManagerInstance in
    print("Network is Unavailable")
}

注意:これはイベントリスナーではなく、一度だけ実行されます。ネットワークの変化をリアルタイムに検知したい場合は、NetworkManager.sharedInstance.reachability.whenReachableを使用する必要があります。具体的な例は記事の後半で紹介します。

それでは、マネージャークラスが用意できたので、アプリケーションでの使い方を見ていきましょう。

アプリ起動時のネットワーク状態への対応

アプリケーションによっては、インターネット接続に強く依存しており、起動時に接続状態を判定する必要がある場合があります。ここでは、NetworkManagerクラスを使ってこの処理を実現する方法を紹介します。

LaunchViewControllerという新しいコントローラーを作成してください。ストーリーボード上の最初のビューコントローラーを、この起動用コントローラーとして扱います。ユーザーのデバイスがオンラインかどうかを判定し、オフラインの場合は専用のオフライン画面を表示して、ユーザーがアプリ本体に入れないようにします。

LaunchViewControllerの内容を、以下のコードに置き換えてください。

import UIKit

class LaunchViewController: UIViewController {

    let network: NetworkManager = NetworkManager.sharedInstance

    override func viewDidLoad() {
        super.viewDidLoad()

        NetworkManager.isUnreachable { _ in
            self.showOfflinePage()
        }
    }

    private func showOfflinePage() -> Void {
        DispatchQueue.main.async {
            self.performSegue(
                withIdentifier: "NetworkUnavailable",
                sender: self
            )
        }
    }
}

このクラスでは、NetworkManagerisUnreachableメソッドを使い、ネットワークが利用できない場合にshowOfflinePageメソッドを呼び出しています。次に、その表示先となるビューコントローラーを作成しましょう。OfflineViewControllerという新しいビューコントローラーを作成します。

Main.storyboardファイルを開き、最初のビューのカスタムクラスにLaunchViewControllerを設定します。

次に、ストーリーボード上にもうひとつビューコントローラーを追加し、そのカスタムクラスにOfflineViewControllerを設定します。そして、LaunchViewControllerから新しいビューコントローラーへ、NetworkUnavailableという名前の手動セグエ(manual segue)を作成してください。完了すると、以下のような構成になっているはずです。

Swiftでインターネット接続を検出・管理する方法:実践ガイド

それではアプリを実行してみましょう。ただし、実行前に開発マシンをオフラインにしておいてください。iOSシミュレーターはマシンのインターネット接続を共有するためです。アプリを実行すると、先ほど作成したオフラインページが表示されるはずです。

次に、接続が確立している場合に表示されるビューコントローラーを作成しましょう。

デバイスがオンラインに戻った際のイベント処理

オフライン時に正しく動作するオフライン画面が完成したので、次はデバイスがオンラインに復帰したときの処理を実装します。

ストーリーボード上で、オフライン画面の下に新しいNavigation View Controllerを配置してください。ここには、Redditの最新投稿を表示するコントローラーを作成します。PostsTableViewControllerという新しいビューコントローラークラスを作成し、Navigation View Controllerに紐づくビューコントローラーのカスタムクラスとして設定しましょう。

続いて、Navigation View ControllerからLaunch View ControllerおよびOffline View Controllerへの手動セグエとして、MainControllerという名前のセグエを作成します。以下のような構成になっていればOKです。

Swiftでインターネット接続を検出・管理する方法:実践ガイド

次に、LaunchViewControllerクラスを開き、viewDidLoadメソッドの末尾に以下のコードを追加します。

NetworkManager.isReachable { _ in
    self.showMainPage()
}

そして、コントローラーに以下のメソッドを追加してください。

private func showMainPage() -> Void {
    DispatchQueue.main.async {
        self.performSegue(
            withIdentifier: "MainController",
            sender: self
        )
    }
}

これで、アプリ起動時に接続状態がチェックされ、接続が利用可能ならPostsTableViewControllerが、そうでなければOfflineViewControllerが表示されるようになります。

いいですね! しかし、ユーザーがOfflineViewControllerを表示している最中に、ネットワークが回復したらどうなるでしょうか? 次はこのシナリオに対応しましょう。

OfflineViewControllerを開き、コードを以下のように書き換えてください。

import UIKit

class OfflineViewController: UIViewController {

    let network = NetworkManager.sharedInstance

    override func viewDidLoad() {
        super.viewDidLoad()

        // ネットワークが利用可能になったらメインコントローラーを表示
        network.reachability.whenReachable = { _ in
            self.showMainController()
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        navigationController?.setNavigationBarHidden(true, animated: animated)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        navigationController?.setNavigationBarHidden(false, animated: animated)
    }

    private func showMainController() -> Void {
        DispatchQueue.main.async {
            self.performSegue(withIdentifier: "MainController", sender: self)
        }
    }
}

上記のコントローラーでは、viewDidLoadメソッド内でwhenReachableのコールバックにメインコントローラーの表示処理を設定しています。つまり、オフラインである限りデバイスのオンライン復帰を監視し続け、復帰した時点でPostsTableViewControllerを表示するというわけです。

また、viewWillAppearviewWillDisappearをオーバーライドすることで、オフライン画面ではナビゲーションバーが表示されないようにしています。

Reddit APIから投稿を取得する

次に、Redditからデータを取得してPostsTableViewControllerに表示するロジックを追加しましょう。ファイルを開き、内容を以下のコードに置き換えてください。

import UIKit
import Alamofire

struct RedditPost {
    let title: String!
    let subreddit: String!
}

class PostsTableViewController: UITableViewController {

    var posts = [RedditPost]()

    let network = NetworkManager.sharedInstance

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Latest Posts"

        // 投稿を取得し、テーブルを再読み込み
        fetchPosts { posts in
            self.posts = posts
            self.tableView.reloadData()
        }
    }

    private func fetchPosts(completion: @escaping (_ posts: [RedditPost]) -> Void) -> Void {
        // Reddit APIにGETリクエストを送信
        Alamofire.request("https://api.reddit.com").validate().responseJSON { response in
            switch response.result {
            case .success(let JSON):
                let data = JSON as! [String:AnyObject]
                guard let children = data["data"]!["children"] as? [AnyObject] else { return }
                var posts = [RedditPost]()

                // Redditの投稿をループ処理してposts配列に格納
                for child in 0...children.count-1 {
                    let post = children[child]["data"] as! [String: AnyObject]

                    posts.append(RedditPost(
                        title: post["title"] as! String,
                        subreddit: "/r/" + (post["subreddit"] as! String)
                    ))
                }

                DispatchQueue.main.async { completion(posts) }
            case .failure(let error):
                print(error)
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    // 投稿の件数を返す
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.posts.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell", for: indexPath)
        let post = posts[indexPath.row] as RedditPost
        cell.textLabel?.text = post.title
        cell.detailTextLabel?.text = post.subreddit
        return cell
    }
}

fetchPostsメソッドでは、Alamofireを使ってReddit APIにGETリクエストを送信しています。その後、レスポンスを解析し、ファイル冒頭で定義したRedditPost構造体にデータを格納していきます。これにより、テーブルビューに渡すデータの一貫性が保たれます。

デバイスがオフラインになった際のイベント処理

最後にもうひとつのシナリオに対応しましょう。Redditの最新投稿を閲覧中に、突然ネットワーク接続が失われたらどうなるでしょうか? この場合も、オフライン画面を再度表示するようにします。

先ほどと同様に、PostsTableViewControllerからOfflineViewControllerへの手動セグエとして、NetworkUnavailableという名前のセグエを作成してください。そして、viewDidLoadメソッドの末尾に以下のコードを追加します。

network.reachability.whenUnreachable = { reachability in
    self.showOfflinePage()
}

さらに、コントローラーに以下のメソッドを追加してください。

private func showOfflinePage() -> Void {
    DispatchQueue.main.async {
        self.performSegue(withIdentifier: "NetworkUnavailable", sender: self)
    }
}

これで、デバイスがオフラインになった瞬間を監視し、実際にオフラインになった場合にshowOfflinePageが呼び出されるようになりました。

以上で完成です!NetworkManagerを使って、Swiftでオンライン・オフライン両方のイベントを適切にハンドリングできるようになりました。

まとめ

この記事では、アプリケーションがオンライン・オフラインの各イベントに確実に対応できるようにする方法について解説しました。実装のアプローチは、プロジェクトの要件に合わせて自由にカスタマイズできます。質問やフィードバックがあれば、ぜひコメント欄でお聞かせください。

なお、このPlaygroundのソースコードはGitHubで公開されています。

この記事は、当初Pusherで公開されたものです。

無料でプログラミングを学びませんか? freeCodeCampのオープンソースカリキュラムは、これまでに40,000人以上の人々が開発者として就職する手助けをしてきました。今すぐ学習を始めましょう。

  1. PDFファイルとは何?PDF形式のメリット・デメリットと正しい使い分け

    インターネットから資料をダウンロードした際、ファイルが「.pdf」形式になっていることに気づいたことはありませんか? PDFは編集が難しく(場合によってはほぼ不可能!)、開くには専用のビューアが必要です。パソコンやスマホにあまり慣れていない方にとっては、「なぜわざわざPDFを使うのか」「.docxなどのWordファイルの方が便利なのでは?」と感じるかもしれません。 しかし実のところ、PDFはその用途において非常に優れたフォーマットなのです。デジタル文書のやり取り手段として長年使われており、今後もなくなる気配はありません。では、なぜ人々は文書の送付にPDFを選ぶのでしょうか? そして、日常のど

  2. ティム・バーナーズ=リー氏が手掛ける「Solid」とは?プライバシーとデータ主権のためのWeb再分散化

    著者:Arnav Bansal最近、Webの発明者であるティム・バーナーズ=リー教授が「Solid」というプロジェクトを発表しました。筆者は早速このプロジェクトを調べてみました。本記事では、Solidが目指すものと、その始め方について解説します。Solidとは?Solidは、Webを「再分散化」する試みです。「再」分散化とは?かつてWebに対するビジョンは、分散型で協働的な読み書き可能な空間でした。世界初のブラウザ(WorldWideWeb)は、実はエディタでもあったのです。しかし、Webの発展に伴い、さまざまな理由からWebアプリケーションの設計は中央集権化していきました。ユーザーのデータは