20190318のSwiftに関する記事は5件です。

円型のプログレスバー実装(MBCircularProgressBar)

はじめに

スクリーンショット 2019-03-18 16.22.36.png
こんにちはayumuです。
現在、個人で作っているアプリで円型のプログレスバーを実装する機会があり、せっかくなので記事にしようと思います。
アプリは審査中… リリースした際は是非ダウンロードしてください!
審査終了しました。リンクは一番下にあるのでよかったらダウンロードお願いします。

MBCircularProgressBar

今回はMBCircularProgressBarというライブラリを使って円型プログレスバーを実装しました。

インストール

CocoaPodsを使用してインストールしました。
pod "MBCircularProgressBar"
でインストール可能です。
https://github.com/MatiBot/MBCircularProgressBar

Viewの作成

今回はStoryBoardを使って作成していきます。
スクリーンショット 2019-03-18 16.42.21.png
まずはViewControllerにViewを貼り付けます。
Viewのサイズは適当で大丈夫です。自分の意図する円グラフのサイズに調整していけばと思います。
スクリーンショット 2019-03-18 16.38.47.png
配置したViewのViewの「Identity Inspector」を選択し、Custom ClassのClassをMBCircularProgressBarViewにします。
そうすると円型のViewが表示されます。

円型プログレスバーのデザインを設定

classを変更したらViewの「Attributes Inspector」にてデザインに関するパラメーターの調整が可能になります。コードからでも変更可能です。
スクリーンショット 2019-03-18 16.54.56.png
ここで軽くパラメーターについて説明します。
showValueString
defaultではもともと用意されているテキストが表示されます。
今回はoffにしてLabelを配置しました。
スクリーンショット 2019-03-18 17.13.00.png

Value、MaxValue
Valueはバーの現在の値、MaxValueはバーの最大値の値です。
上の写真だとValueは15、MaxValueは30です。これらのパラメーターは常に同じ値ではないのでコードに書きます。(コードに書くことでアニメーションも可能です)
progressRotationAngle
バーの回転を調整できます。
(50で真ん中から時計回り、0で真ん中から反時計回り)
progressAngle
バーの角度を調整できます。
(100で円型、0だと無くなります)
progressLine
バーの太さを調整できます
(上の写真だと黒いバーの部分)
progressColor
バーの色を調整できます。
(上の写真だと黒いバーの部分)
progressStrokeColor
バーのフレームの色を調整できます。
(上の写真だと黒いバーの部分、バーの色と同じにしているので単色になっています)
emptyLineWidth
空のバーの太さを調整できます。
(上の写真だと薄いグレーのバーの部分)
emptyLineColor
空のバーの色を調整できます。
(上の写真だと薄いグレーのバーの部分)
emptyLineStrokeColor
空のバーのフレームの色を調整できます。
(上の写真だと薄いグレーのバーの部分、空のバーの色と同じにしているので単色になっています)

他にも色々あります。詳しくは上のgithubのリンクからご覧ください。

実装

StoryBoardとプログラムコードを紐づけます。
紐づけたプログラムファイルにimport文を入力します。

ViewController.swift
import UIKit
import MBCircularProgressBar

class ViewController: UIViewController {
    @IBOutlet weak var progressView: MBCircularProgressBarView!
    @IBOutlet weak var progressValueLabel: UILabel!

//今回はshowValueStringをoffの状態です
    override func viewDidLoad() {
        super.viewDidLoad()
        progressView.value = 15
        progressView.maxValue = 30
        progressValueLabel.text = "\(progressView.value)"
    }

}

このようにvalue,maxValueに値を入れてあげることで下の図のような写真になります。
スクリーンショット 2019-03-18 17.13.00.png

しかし表示させるだけでは物足りなさを感じるのでアニメーションをつけたいと思います。

ViewController.swift
import UIKit
import MBCircularProgressBar
class ViewController: UIViewController {
    @IBOutlet weak var progressView: MBCircularProgressBarView!
    @IBOutlet weak var progressValueLabel: UILabel!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        progressView.value = 0
        progressView.maxValue = 30
        progressValueLabel.text = "\(progressView.value)"
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(true)
        UIView.animate(withDuration: 1.0) {
            self.progressView.value = 15
        }
    }
}

viewWillAppearでprogressView.value = 0
viewDidAppearでUIView.animateの中身をprogressView.value = 15
とすることでバーが0から設定してる値(ここでは15)までアニメーションします。

おわりに

最後まで読んでいただきありがとうございました。
簡単に実装できるのでよかったら試してみてください!
是非アプリのダウンロードよろしくお願いします!!!
https://itunes.apple.com/us/app/イマスル-今から習慣化/id1456245094?l=ja&ls=1&mt=8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[swift][iOS]RxTableViewに紐付く画像をlazy loadingする

TableViewに紐付く形でUIImageViewを設置

スクリーンショット 2019-03-18 16.34.50.png

こんな感じでUITableViewとcell、UIImageViewを設置。

lazy loadingを実装

https://stackoverflow.com/questions/28694645/how-to-implement-lazy-loading-of-images-in-table-view-using-swift
↑を参考に、swiftのバージョンを合わせて実装。

extension UIImageView {
    func downloadImageFrom(link: URL, contentMode: UIViewContentMode) {
        URLSession.shared.dataTask( with: link, completionHandler: { (data, _, _) -> Void in
            DispatchQueue.main.async {
                    self.contentMode = contentMode
                    if let data = data { self.image = UIImage(data: data) }
                }
            }
        ).resume()
    }
}

現時点で参考元と異なる点は、linkをstringではなくURLで貰っている点ぐらい。

dataSource周りは以下のように実装。

var photoURLs: [URL]! = [] // cellに表示したい画像URLの配列

private func configureData() {
    let dataSource = RxTableViewSectionedReloadDataSource<SectionData>(
        configureCell: { (_, tableView, indexPath, item) in
            let cell: HogeCell = (tableView.dequeueReusableCell(
                withIdentifier: "HogeCell",
                for: indexPath
                ) as? HogeCell)!

            cell.imageView.image = UIImage(named: "NoImage") // NoImageというLoading中を示す画像
            // ここでcellごとにlazy loadingを行う
            cell.imageView.downloadImageFrom(
                link: self.photoURLs[indexPath.row],
                contentMode: UIViewContentMode.scaleAspectFit,
            )
            return cell
    }, canEditRowAtIndexPath: { (_, _) in
        return true
    }, canMoveRowAtIndexPath: { (_, _) in
        return true
    }
    )

    self.imageItemsRelay.map {
        return [SectionData(header: "section1", items: $0)]
        }.bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: rx.disposeBag)

    self.tableView.rx.itemDeleted
        .subscribe(onNext: { [weak self] indexPath in
            guard let `self` = self else { return }
            // 何か処理
        }).disposed(by: rx.disposeBag)

    self.tableView.rx.itemMoved
        .subscribe(onNext: { [weak self] sourceIndexPath, destinationIndexPath  in
            // 何か処理
        }).disposed(by: rx.disposeBag)
}

問題点

これだとcellが画面外に出るとcellが再レンダリングされてしまい、その度にlazy loadingが走って画像を取得しに行ってしまう。(dequeueReusableCell使ってるのに。。。)
一度表示した画像はそのままに表示しておきたい。

原因

  • https://github.com/RxSwiftCommunity/RxDataSources より
  • dataSourceとして利用しているRxTableViewSectionedReloadDataSourceがSection単位でreloadData()をして表示内容を更新するようになっているため画像の再取得が行われてしまっている模様

対策

  • RxTableViewSectionedAnimatedDataSourceを使う
  • 初回にlazy loadingでDLしてきた画像を持っておいて、2回目以降はDLしてきた画像を使う

RxTableViewSectionedAnimatedDataSourceを使ってみる

  • RxTableViewSectionedAnimatedDataSourceは変化のあったcellだけ変更がかかるみたい
  • 置き換えてみたがダメだった
  • RxTableViewSectionedAnimatedDataSource内で使われている、古い配列と新しい配列を比較するアルゴリズムdifferencesForSectionedViewで比較ありと言われているっぽい?
  • アルゴリズムの中までは見れていないが、一旦こっちは諦めた

初回にlazy loadingでDLしてきた画像を持っておいて、2回目以降はDLしてきた画像を使う

タイトルのまま。
以下にような辞書を用意しておく。

var downloadedUIimages: [Int64: UIImage] = [:]

Int64は画像を一意で識別できるIDとか。
無いならURLでもいいかも。

var photoURLs: [URL]! = []
var downloadedUIimages: [Int64: UIImage] = [:]

private func configureData() {
    let dataSource = RxTableViewSectionedReloadDataSource<SectionData>(
        configureCell: { (_, tableView, indexPath, item) in
            let cell: HogeCell = (tableView.dequeueReusableCell(
                withIdentifier: "HogeCell",
                for: indexPath
                ) as? HogeCell)!            
           // DLしようとする画像が既にdownloadedUIimagesに存在していれば再利用する
            if let _downloadedImage = self.downloadedUIimages[item.imageId] {
                cell.imageView.image = _downloadedImage
            }
            else {
                cell.imageView.image = UIImage(named: "NoImage")
                cell.imageView.downloadImageFrom(
                    link: self.photoURLs[indexPath.row],
                    contentMode: UIViewContentMode.scaleAspectFit,
                    // 完了時にdownloadedUIimagesにimageIDとUIImageを紐づけて持っておく
                    completion: {
                        self.downloadedUIimages[item.imageId] = cell.imageView.image
                })
            }
            return cell
    }, canEditRowAtIndexPath: { (_, _) in
        return true
    }, canMoveRowAtIndexPath: { (_, _) in
        return true
    }
    )

    self.imageItemsRelay.map {
        return [SectionData(header: "section1", items: $0)]
        }.bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: rx.disposeBag)

    self.tableView.rx.itemDeleted
        .subscribe(onNext: { [weak self] indexPath in
            // 何か処理
        }).disposed(by: rx.disposeBag)

    self.tableView.rx.itemMoved
        .subscribe(onNext: { [weak self] sourceIndexPath, destinationIndexPath  in
            // 何か処理
            // 削除したならdownloadedUIimagesからも削除してもいいかもしれない
        }).disposed(by: rx.disposeBag)
}

extension UIImageView {
    func downloadImageFrom(link: URL, contentMode: UIViewContentMode, completion: (() -> Void)? = nil) {
        URLSession.shared.dataTask( with: link, completionHandler: { (data, _, _) -> Void in
            DispatchQueue.main.async {
                    self.contentMode = contentMode
                    if let data = data { self.image = UIImage(data: data) }
                    completion?()
                }
            }
        ).resume()
    }
}

上記のようにすることで、スクロールしてcellが画面外に行っても画像の再DLが行われることはなくなった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WKWebViewでBasic認証を実装する

WKWebViewでBasic認証を実装する

WKWebViewでBasic認証を実装するサンプルコードです。

環境
Xcode10.1

最終的なサンプルを見たいならこちら

WebViewの生成

WKWebViewを生成しdelegateの設定を行います。
認証に関するdelegateは navigationDelegate です。(WKWebViewには uiDelegateもあるので気をつけて。)

以下はコードで生成する例です(Storyboardでも問題なし)。

ViewController.swift
import UIKit
import WebKit

class ViewController: UIViewController {

    var webView: WKWebView!

    override func loadView() {
        view = UIView(frame: .zero)

        let configuration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: configuration)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.navigationDelegate = self
        view.addSubview(webView)

        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.topAnchor),
            webView.leftAnchor.constraint(equalTo: view.leftAnchor),
            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            webView.rightAnchor.constraint(equalTo: view.rightAnchor)
            ])
    }

}

extension ViewController: WKNavigationDelegate {
}

認証のdelegateを実装

認証に関するdelegateメソッドを実装します。
まずは何もしないメソッドを書きます。

ViewController.swift
extension ViewController: WKNavigationDelegate {

    public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        switch challenge.protectionSpace.authenticationMethod {
        default:
            completionHandler(.performDefaultHandling, nil)
        }
    }

}

Basic認証の処理を実装

認証に関するdelegateメソッドは、Basic認証に限らず他の認証でも呼ばれます。
Basic認証の場合の処理と、それ以外の処理を分けて実装します。

URLCredentialにIDとPasswordを設定して、completionHandlerを呼び出します。

ViewController.swift
extension ViewController: WKNavigationDelegate {

    public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        switch challenge.protectionSpace.authenticationMethod {
        case NSURLAuthenticationMethodHTTPBasic:
            let credential = URLCredential(user: "user", password: "password", persistence: URLCredential.Persistence.forSession)
            completionHandler(.useCredential, credential)

        default:
            completionHandler(.performDefaultHandling, nil)
        }
    }

}

サンプル

UIAlertControllerでユーザー人入力を求める場合の最終的なコードは以下になります。

IDとPasswordを入力したとき、キャンセルしたとき、そのそれぞれでcompletionHandlerを呼び出します。

ViewController.swift
import UIKit
import WebKit

class ViewController: UIViewController {

    var webView: WKWebView!

    override func loadView() {
        view = UIView(frame: .zero)

        let configuration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: configuration)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.navigationDelegate = self
        view.addSubview(webView)

        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.topAnchor),
            webView.leftAnchor.constraint(equalTo: view.leftAnchor),
            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            webView.rightAnchor.constraint(equalTo: view.rightAnchor)
            ])
    }

    override func viewDidLoad() {
        let request = URLRequest(url: URL(string: "https://hogehoge.com/")!)
        webView.load(request)
    }
}

extension ViewController: WKNavigationDelegate {

    public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        switch challenge.protectionSpace.authenticationMethod {
        case NSURLAuthenticationMethodHTTPBasic:

            let alert = UIAlertController(title: "Basic認証", message: "ユーザ名とパスワードを入力してください", preferredStyle: .alert)
            alert.addTextField {
                $0.placeholder = "user"
            }
            alert.addTextField {
                $0.placeholder = "password"
                $0.isSecureTextEntry = true
            }

            let login = UIAlertAction(title: "ログイン", style: .default) { (_) in
                guard
                    let user = alert.textFields?[0].text,
                    let password = alert.textFields?[1].text
                    else {
                        completionHandler(.cancelAuthenticationChallenge, nil)
                        return
                }
                let credential = URLCredential(user: user, password: password, persistence: URLCredential.Persistence.forSession)
                completionHandler(.useCredential, credential)
            }
            let cancel = UIAlertAction(title: "キャンセル", style: .cancel) { (_) in
                completionHandler(.cancelAuthenticationChallenge, nil)
            }
            alert.addAction(login)
            alert.addAction(cancel)

            present(alert, animated: true, completion: nil)

        default:
            completionHandler(.performDefaultHandling, nil)
        }
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nginx,Redis,MySQLを使ってほんの少し実践的なRails ActionCableと、iOS/Androidのサンプルアプリを作って全体像を学ぶ〜iOS編〜

前置き

「Rails ActionCableで双方向通信してみたい」「モバイルアプリでリアルタイム通信アプリ作りたい」と思いサンプルアプリを作ってみました。個々の詳細については既に解説してくださっている記事はありますので、大まかに環境構築やソースコードを記事にします。以下の3部構成になっています。

お遊びサンプルの紹介

以下のアニメーションGIFをご覧ください。各ユーザーのアクティブ状況を表示して、メッセージをやりとります。もっと砕けた表現をするならば、筆者の愛犬たちが寝起きして、鳴いたり、遠吠えしたり、唸ったりします。
ios_demo.gif

このアプリでは大きく2つのActionCableの使い方があります。

  • 同じルーム内の全ユーザーにブロードキャスト
    • ルームに入る
      現在ルーム内にいるユーザー(以下、アクティブユーザー)にルームに入ったことを通知し、アクティブユーザーを取得します。そして、各ユーザーのアクティブ状況を表示します。
    • 「ワンワン」ボタンと「ワオーーン」ボタン
      文字入力で任意の文字が送れないだけで、チャットでいうところの「メッセージ」とほぼ同義です。ボタンに対応したメッセージを送信します。
      ※アニメーションGIFでは「ワオーン」とか「ワオォーン」になっています。途中から文字を修正しました:bow:
    • ルームから出る
      「キャンセル」をタップしたり、アプリを閉じたりするとルームから出たことにします。ルームに入るときと同様にアクティブユーザーを取得して、各ユーザーのアクティブ状況を表示します。
  • 自分にブロードキャスト
    • 「独り言」
      「独り言」ということで自分のみメッセージを受信します。

構成

名前 バージョン
macOS Mojave 10.14.3
Xcode 10.1
Swift 4.2.1
iOS 12.1
シミュレーター iPhone 6 / 7 / 8
  • hosts(macOS)

    hosts
    127.0.0.1       devnokiyo.example.com
    

ソースコードはGitHubに公開しています。よろしければご覧ください。

ActionCableClientを導入する

ライブラリを利用して開発します。少し変則的な導入をしているので説明します。

まず通常どおりCocoaPodsで導入する

公式の説明どおり一般的な導入をまず行います。

Podfile
pod "ActionCableClient"
$ pod install

ビルドエラーを解消する

残念ながら現行バージョン(0.2.3)ではSwift4.2に対応しきれていないようで、筆者の環境ではビルドエラーが発生してしまいます。また、開発中と思われる最新のコードはビルドは通りますが動作が不安定です。(バージョンのタグ付けがないので不安定なのは当たり前ですね。)
同件と思われるissueに便乗しつつ、今回はビルドエラーの箇所を修正することにしました。

  • Pods/ActionCableClient/Source/Classes/RetryHandler.swift 01.png
  • noshilan/Pods/Starscream/Source/WebSocket.swift 02.png

今回はこの修正がありますのでCocoaPodsでインストールしたライブラリもGitの管理対象に含めました。

ソースコードの説明

サンプルアプリはActionCableと本質的に関係ない部分も多いので、GitHubに公開しているソースコードの原形を保ちながら主要な部分を抽出しました。ソースコードの中にコメントで説明します。

ActionCableApp/ViewControllers/BarkVc.swift
class BarkVc: UIViewController {
    private let CableUrl = "ws://devnokiyo.example.com/cable" // ActionCableのコネクションのエンドポイント
    private let ChannelIdentifier = "RoomChannel"             // チャンネル名

    private var client: ActionCableClient!                    // ActionCableのコネクションに関連するインスタンス
    private var channel: Channel!                             // ActionCableのチャンネルを関連するインスタンス

    // 【補足】「ユーザーをRDBから取得して、その分だけ動的に表示して・・・」と
    // 要件が大きくなるとサンプルの目的が反れるので、3ユーザーのみに限定して作成しました。
    @IBOutlet weak var chiyoUsv: UserStatusView!  // 上段ユーザーのステータス
    @IBOutlet weak var eruUsv: UserStatusView!    // 中段ユーザーのステータス
    @IBOutlet weak var otomeUsv: UserStatusView!  // 下段ユーザーのステータス

    override func viewDidLoad() {
        super.viewDidLoad()
        // ActionCable関連のインスタンスを初期化・接続する。
        initClient()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // 画面を閉じるときはコネクションを切断する。
        // サンプルではアプリの中断は考慮しない。
        client.disconnect()
    }

    @IBAction func tapBawBawButton(_ sender: Any) {
        // 「ワンワン」ボタン押下時はRoomChannelのbarkアクションを呼出す。
        // 送信情報) "content":"bawbaw"
        bark(bark: "bawbaw")
    }

    @IBAction func tapWaooonButton(_ sender: Any) {
        // 「ワオーーン」ボタン押下時はRoomChannelのbarkアクションを呼出す。
        // 送信情報) "content":"waooon"
        bark(bark: "waooon")
    }

    @IBAction func tapMumblingButton(_ sender: Any) {
        // 「独り言」ボタン押下時はRoomChannelのmumblingアクションを呼出す。
        // 送信情報) 無し。アクションを呼出すのみ。
        channel?.action("mumbling")
    }

    private func initClient() {
        // コネクションのエンドポイントを指定する。
        // 送信情報) 呼出し元ViewControllerから取得したaccount
        // 【補足】ユーザーの割出しと認証が必要ならOpenID Connectのアクセストークンなどになると思います。
        client = ActionCableClient(url: URL(string: "\(CableUrl)/?account=\(account!)")!)
        client.onConnected = {
            // 【補足】このサンプルではチャンネルをサブスクライブした直後にRoomChannelのgreetingアクションを呼出します。
            // コネクションの接続完了を待たずにチャンネルを作成してしまうと非同期の問題でRoomChannelのgreetingアクションが
            // 呼ばれないことがありました。そのため、コネクションの接続が完了してからチャンネルをサブスクライブします。
            self.initChannel()
        }

        // 【補足】onConnectedの他にも以下のコールバックが用意されています。
        // client.willConnect = {}
        // client.onDisconnected = { (error: ConnectionError?) in }
        // client.willReconnect {}

        // 【補足】以下のプロパティで再接続のポリシーを設定出来るようです。(厳密に確認していません。)
        // client.reconnectionStrategy

        // コネクションに接続する。
        client.connect()
    }

    private func initChannel() {
        // チャンネルを作成する。サブスクライブは手動で行う。
        // 送信情報) "room":ルーム名(ID)
        self.channel = self.client.create(ChannelIdentifier, identifier: ["room": room], autoSubscribe: false)

        self.channel.onSubscribed = {
            // サブスクライブしたら、ルームに入ったことになる。
            // 同じルームのアクティブユーザーに通知するのでRoomChannelのgreetingアクションを呼出す。
            self.channel?.action("greeting")
        }

        self.channel.onReceive = {(data: Any?, error: Error?) in
            // 自他問わずアクティブユーザーより送信された情報をこのコールバックで受信する。
            if let response = RoomChannelResponse(data: data) {
                // 誰に関する情報か判定する。自身も含まれる。
                // findUserStatusViewメソッドは以下のいずれかのクラス変数を返却する。
                // chiyoUsv
                // eruUsv
                // otomeUsv
                // 受信情報) "account":ユーザーのアカウント
                let userStatusView = self.findUserStatusView(account: response.account)
                switch response.type {
                case .roomIn:
                    // 受信情報) "type":"in"
                    // 受信情報) "roommate":"[アクティブユーザーのアカウント...]"
                    // updateUserStatusメソッドはアクティブユーザーを以下のようにする。
                    // 表示内容:(^○^)
                    // オンラインの色(緑)
                    self.updateUserStatus(accounts: response.roommate, type: response.type)

                    // ルームに入ったユーザーは挨拶する。
                    // 受信情報) "type":"in"            表示内容: (^○^) (言語:日本語)
                    userStatusView.bark.text = NSLocalizedString(response.type.rawValue, comment: self.defaultComment)
                    break
                case .roomOut:
                    // ルームから出たユーザーは挨拶する。
                    // 受信情報) "type":"out"           表示内容: ( ˘ω˘ ) (言語:日本語)
                    userStatusView.bark.text = NSLocalizedString(response.type.rawValue, comment: self.defaultComment)
                    // オフラインの色(赤)に変更する。 
                    userStatusView.online.backgroundColor = UIColor.red
                    break
                case .mumbling:
                    // 受信情報)
                    //  "type":"mumbling"
                    //  "content":"(゚Д゚;)"             表示内容: (゚Д゚;) (言語:不問) バックエンドの固定値なので言語設定に依存しない
                    // 【補足】「独り言」は自身が送信した情報をActionCableを経由して自身のみが受信します。
                    if let content = response.content {
                        userStatusView.bark.text = content
                    }
                    break
                case .bark:
                    // 受信情報)
                    //  "type":"bark"
                    //  "content":"bawbaw" / "wooon"  表示内容: ワンワン / ワオーーン (言語:日本語)
                    if let content = response.content {
                        userStatusView.bark.text = NSLocalizedString(content, comment: self.defaultComment)
                    }
                    break
                }
            }
        }

        // チャンネルをサブスクライブする。
        self.channel.subscribe()
    }

    private func bark(bark: String) {
        // 「ワオーーン」ボタン押下時はRoomChannelのbarkアクションを呼出す。
        // 送信情報) "content":bark
        channel?.action("bark", with: ["content": bark])            
    }

筆者がハマったところ

  • ActionCableClientのビルドエラー
    前述のとおり修正しました。

  • チャンネルの初期化
    コネクションに接続した直後にチャンネルのアクションへ送信するとき、送信できないときがありました。チャンネルの初期化はコネクションの接続が完了してから行います。非同期処理のタイミングによる問題だと思います。

    失敗例
    client.connect()
    channel = self.client.create(ChannelIdentifier, identifier: ["room": room], autoSubscribe: false)
    channel.subscribe()
    
    成功例
    client.onConnected = {
        channel = self.client.create(ChannelIdentifier, identifier: ["room": room], autoSubscribe: false)
        channel.subscribe()
    }
    client.connect()
    

参考

Rails 5 Action CableチャットアプリのiOSクライアント側を作る

終わりに

チャットアプリのサンプルが定番なので、少し違うアプローチでサンプルを作っていたはずなのですが、結局仕組みは似たり寄ったりになってきました。筆者はiOS、Androidの順で実装しているのでバックエンドとの主な仕様調整はiOS版で行なっています。その意味ではiOS版のほうが壁に当たることが多いです。細かい作込みはしていませんが、バックエンドとアプリの双方を実装して、やりたいことの表現と仕組みを概ね理解することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NotificationCenterでの複数の通知をコールバックで待ちあわせる

はじめに

NotificationCenterでの複数の通知を待ちたいユースケースの実装例です。
複数の通知を待ち合わせ、すべての通知を受信した場合にコールバックを実行します。

使用例

NotificationWaiter.wait(for .someNotification) {
    // .someNotificationの通知を受信した時
}

NotificationWaiter.wait(for [.fooNotification, .barNotification]) {
    // .fooNotification, .barNotificationの通知をすべて受信した時
}

サンプルコード

/// NotificationCenterでの通知を1度だけコールバックで待つためのクラス
public enum NotificationWaiter {
    /// 引数で指定したNotification.Nameが通知されるまで待ち、通知されたタイミングでcompletionを実行する
    ///
    /// - Parameters:
    ///   - name: 対象となる通知名
    ///   - notificationCenter: 通知センター
    ///   - completion: 通知を受信した際に実行するクロージャー
    static func wait(for name: Notification.Name,
                     notificationCenter: NotificationCenter = NotificationCenter.default,
                     completion: @escaping () -> Void) {
        var token: NSObjectProtocol?
        token = notificationCenter.addObserver(forName: name,
                                               object: nil,
                                               queue: nil) { _ in
                                                guard let token = token else { return }
                                                notificationCenter.removeObserver(token)
                                                completion()
        }
    }

    /// 引数で指定したNotification.Nameが全て通知されるまで待ち、全て通知されたタイミングでcompletionを実行する
    ///
    /// - Parameters:
    ///   - names: 対象となる通知名の配列
    ///   - notificationCenter: 通知センター
    ///   - completion: 全ての通知を受信した際に実行するクロージャー
    static func wait(for names: [Notification.Name],
                     notificationCenter: NotificationCenter = NotificationCenter.default,
                     completion: @escaping () -> Void) {
        let dispatchGroup = DispatchGroup()
        let dispatchQueue = DispatchQueue(label: "", attributes: .concurrent)

        names.forEach { name in
            dispatchGroup.enter()
            dispatchQueue.async(group: dispatchGroup) {
                wait(for: name) {
                    dispatchGroup.leave()
                }
            }
        }

        dispatchGroup.notify(queue: .main) {
            completion()
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む