WKWebViewのRXデータバインディングを実装する

概要

今回はWKWebViewのRxSwiftのローディング処理について見ていきます。

実はRxSwiftにWKWebViewのものがないためかrxのプロパティがありません。

ただし公式がWKWebViewに対するライブラリを提供していますのでこれをインストールして貰えればrxが使えるようになります。

github.com

こちらをcocoapodでインストールすることでWKWebViewに対してrxが使えるようになります。

ただWKWebViewはiOS11未満をサポートしている場合storyboardにUIKitを置くとワーニングが出てしまいます。

そのためコード側でWKWebView を作成してviewに乗せるという処理をしていきます。

cocoapodのライブラリについて

今回はWKWebView用のライブラリを追加するのでcocoapodのpodfileを下のように変更します。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'RxSwitch' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for RxSwitch
    pod 'RxSwift', '~> 4.0'  # 追加する
    pod 'RxCocoa', '~> 4.0'  # 追加する
    pod 'RxWebKit' # 追加する

  target 'RxSwitchTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'RxSwitchUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

RxSwiftの他にRxWebKitを追加しました。

これをインストールしないとWKWebView に対してrxが使えません。

ViewControllerのソースコードについて

今回はViewController.swiftのコード側だけの実装となります。

ViewController.swift

import UIKit
import RxCocoa
import RxSwift
import WebKit
import RxWebKit

class ViewController: UIViewController {
    
    private var webView: WKWebView! = {
        let configuration = WKWebViewConfiguration()
        return WKWebView(frame: .zero, configuration: configuration)
    }()
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
    }
    
    private func setupWebView() {
        // Rxの実装
        webView.rx.url
            .subscribe(onNext: { url in
                if let url = url {
                    print("URL: \(url)")
                }
            })
            .disposed(by: disposeBag)
        
        // storyboard にないのでviewに乗せる
        self.view.addSubview(webView)
        
        // webViewのオートレイアウトを設定する
        webView.translatesAutoresizingMaskIntoConstraints = false
        self.webView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        self.webView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
        self.webView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        self.webView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
        
        // URLの読み込み
        if let url = URL(string: "https://www.google.com/") {
            let urlRequest = URLRequest(url: url)
            webView.load(urlRequest)
        }
    }
}

各所に補足のコメントを記載しました。

注意点は今回はWKWebViewをコード上で生成しましたのでviewにのせてオートレイアウトを張ることです。

コードとしてみてみるとRxを使わないコードと似たような感じになりますね。

WKWebViewはブラウザ内でJavaScriptを実行したりするときにデリゲートを多用するのでそのときにRxの真価が発揮されます。

挙動を確認してみよう

これでアプリをビルドしてみましょう。

画面が表示されてGoogleのページが表示されたら成功です。

f:id:qed805:20190308001434p:plain
wk_webview_2

今回はこれで終わります。

RxSwiftを使ってUIKitのRXデータバインディングを実装する

概要

今回はRxSwiftを使ってUIKitのデータバインディングの実装を見ていきます。

RxSwiftはUIKitの数だけメソッドがあります。

こちらのページがRxで実装できるクラスの一覧です。

github.com

めちゃくちゃ多いですね。大体のUIKitをRxで書ける事がわかります。

その中でもよく使いそうなものをチョイスして紹介しようと思っているのですが、

簡単な順から

  • UISwitch
  • UINotification
  • UIScrollView
  • UISession

この辺りをメインに解説していきたいと思います。

今回はUISwitchについて説明します。

Storyboardの構成について

今回のStoryboardの構成は下の画像の通りです。

f:id:qed805:20190307225748p:plain
switch_storyboard

UIButtonとUISwitchだけを使います。

ViewController側のソースコードについて

ではViewController側のソースコードをサクッと載せていきます。

Rxの書き方に慣れてくるとだいたい想像ができるようになります。

ViewController.swift

import UIKit
import RxCocoa
import RxSwift

class ViewController: UIViewController {
    
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var sw: UISwitch!
    
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // UIbuttonが押せる時は「押せます」のタイトル
        button.setTitle("押せます", for: .normal)
        // UIbuttonが押せない時は「押せません」のタイトル
        button.setTitle("押せません", for: .disabled)
        
        // UISwitchのvalueを変数でとります。
        // 返り値は RxSwift.Observable<Self.E>
        let termsValidation = sw
            .rx.value
            .share(replay: 1)
        
        // ここでObservableの変数をデータバインディングします
        termsValidation
            .bind(to: button.rx.isEnabled)
            .disposed(by: disposeBag)
    }
}

以上となります。

各所、処理の部分にコメントを記載していますので行数が少し増えています。

        let termsValidation = sw
            .rx.value
            .share(replay: 1)

このコードでUISwtich のObservableな状態を変数にします。

そのあとにObservableなUISwitchのvalueとUIBbuttonの状態(UIButton#isEnable)をデータバイディングしますので

        termsValidation
            .bind(to: button.rx.isEnabled)
            .disposed(by: disposeBag)

このようにコードを書きました。

これでアプリを起動させるとUISwitchをタップする度にUIButtonの状態を変化させることができます。

UISwitchがonの時

f:id:qed805:20190307232826p:plain
switch_on_2

UISwitchがoffの時

f:id:qed805:20190307232857p:plain
swtich_off_2

このように変化します。

UISwitchはon/offの切り替えぐらいしか実装が思いつかないのでRxを使うとなると上記のパターンぐらいしか思いつきません。

これでUISwitchのRxの書き方がわかりました。

3年後のスマホ業界の時代の遷移を想像してみた

普及しているプログラミング言語Google言語だけとなった世界

おそらく皆が同意すると思うが2010年から2018年までの間はスマホ業界はApple一強の時代であると言い切れる。

5年前なら20代後半がiOSアプリを開発するだけでも圧倒的な有利を取れていた。

だがここ2~3年で10代~20代前半の子らがこの市場に参入し始めてきた。

少なくともiOS/Androidアプリ開発で20代前半のエンジニアは1人は必ずいる。

それだけ若い人がiOSアプリ開発がそれなりのステータスになることを認知し始めたのだ。

5年前はそもそもプログラマ業界は3Kとして学生に不人気だったのに世の中変わったものである。

だいぶ話が脱線したがこれだけiOS(というかiPhone)が人気になってくるとGoogleとしてはちょっと面白くないはずである。

そんなGoogleは次の5年を圧勝するために今この時間とお金を投資しまくっているのではないかな。

だからこれから来る2020年の代はもしかするとGoogleが開発するプラットフォームでソフトウェアを開発しているかもしれない。

僕は去年の夏頃からブロックチェーンのリサーチのためにブロックチェーン業界のコミュニティに入って内部調査をしていたが、

その時に初めてGoogle製のサーバーサイド言語のGoとiOS/Androidマルチプラットフォーム開発のFlutterの存在を知った。

Flutterに関してはReact Nativeの2番線じ感を最初は感じていた。

だがGoの方をリサーチしてみるとこれがどうもプログラマ業界に人気らしかった。

フリーランス案件で単価の高い案件にGo言語が入った事もそれを物語っていた。

僕の中ではすでにGoはプログラミング言語のトレンドとしては成功していると思っている。

GoはGoogle製のサーバーサイド言語である。

そしてまたFireBaseの存在もある。これもまたGoogle製のMBaasだ。

アクセス解析ツールであるGoogle Analyticsはもちろんgoogle製。

あれ、Google製のモノ増えて来てない?

と思うと最近のgoogleは結構伸びて来ている感がある。

そのため私は密かに高確率でGoogle製のプラットフォームであるFlutterが今後5年の未来を勝利を獲得するのかもと予想するようになった。

さらにAndroidの開発用プログラミング言語であるJavaはオラクルに裁判で負けてしまったため新OSを開発せざるを得ない状況もありますからね。

ASCII.jp:Google-Oracle訴訟はOracleに有利な判断 判決からAndroid登場時の裏が見えてくる (1/2)|末岡洋子の海外モバイルビジネス最新情勢

これでもうGoogleは次の5年のスマホ業界でAppleに逆転するために本気になると思う。

これまでの5年間はずっとAppleに負け続けて来たから。

任天堂が昔長期間ソニーに負け続けて最近復活して来たようにGoogleは次の5年間をAppleに勝つ施策を出して来るはずである。

逆にまあAppleジョブズの財産を消化しきったのでこれから先はゆっくり劣化していくと予想する。

iOSアプリエンジニアもAndroidアプリエンジニアも同じコードをメンテする

本当は日本のキャリア(ソフトバンク・ドコモ、AU)みたいに競合相手が複数になる世界が面白いんだけど、

GoogleのプラットフォームがAppleに勝利したら今後のスマホアプリの業界は

iOSエンジニアもAndroidエンジニアも同じコードをメンテナンスする未来が見えて来たかもと思っている。

www.gizmodo.jp

japanese.engadget.com

このあたりを見るとGoogleの本気度が伺える。

Googleが新OSを開発している辺りAndroidのプラットフォームを無くしそうである。

2010年はiOSによってFlashのプラットフォームが衰退した時のようにこれから先はAndroidのプラットフォームが衰退するのかもしれない。

同僚のAndroidエンジニアはこの点に関しては悲観的である。

今僕はAndroidの開発や勉強もやっているが、

Androiderからは「Androidは今後廃れるからやる必要ないと思うよ」という忠告を何度も頂きます。

こう考えるとやはり十何年前にAppleジョブズ

は本当に凄い事だと思う。

実際の技術選定はウォズの方だと思うけど、

ジョブズは将来流行る技術の目利きが出来るからそれにOKを出したのはやっぱり凄い。

それに比べてMicroSoftと来たら

Dartは未来の言語となり得るか

Dartに関してはどうだろうか。

この世界、プログラミング言語の仕様がいいからと行って技術が流行るとは限らないのが技術選定の難しいところである。

なんせiOSで懲りたけどObjective-Cは文法構文上みんなに嫌われてたしなかなか難しかったけどiOSの普及で一気に

流行った言語なので。

特にブラウザ言語であるJavaScriptに関しては昔はクソ言語の一つだし言語仕様も複雑怪奇だけど

使われているプログラミング言語ランキングナンバーワンという地位を獲得しているのである。

もちろん僕はあまりJavaScriptは触りたくないし書きたくない。

変数のスコープとか鬼畜すぎて共同開発に向いてないんじゃないかってくらいよくわからない言語である。

それに比べたらDartはまだ良心的な言語仕様だと思う。

JavaJavaScriptを足して2で割ったような言語仕様らしいけど。

なんか、今後Dartが流行った暁には

JavaJavaScriptの違いのネタにDartで例えられそうな感があるね。

Swiftのデリゲートメソッドでデフォルト引数ぽく書く方法

概要

コードレビューの時にデリゲートメソッドの部分で指摘を受けて修正しようと思ったのですが、

なかなかすぐに思いつかなかったので記事として残してみることにしました。

自分への備忘録です。

記事にまとめてみると復習の意味でも結構いい勉強になりました。

問題となっているポイントについて

Swiftで書いているときにprotocolのメソッドで引数にデフォルト値を設定したくなる時があると思います。

例えば、こんな感じのコードですね。

protocol CustomContentViewDelegate: AnyObject {
    func customContentViewDidTouchUp(_ view: CustomView, text: String? = nil) // コンパイルエラー
}

現状の仕様だとこのようにtext: String? =nilというのはprotocolでは持てない仕様である。

ですが仮にこれができた場合にはdelegateを呼び出し側で

protocol CustomContentViewDelegate: AnyObject {
    func customContentViewDidTouchUp(_ view: CustomView, text: String? = nil)
}

class CustomView: UIView {
    weak var delegate: CustomContentViewDelegate?

    func sampleMethod() {
        // デフォルト引数発動
        delegate?.customContentViewDidTouchUp(self) 
        // 引数指定
        delegate?.customContentViewDidTouchUp(self, text: "テスト実装")
    }
}

という風にtextの引数を省略できて保守性が上がるというわけです。

この命題について考えたいと思います。

回答

いきなり回答に入りますがextensionでprotocolを拡張すれば言い訳です。

protocol CustomContentViewDelegate: AnyObject {
    func customContentViewDidTouchUp(_ view: CustomView, text: String?)
}

// protocol extension でデフォルト引数を設定する
extension CustomContentViewDelegate {
    func customContentViewDidTouchUp(_ view: CustomView) {
        customContentViewDidTouchUp(view, text: nil)
    }
}

class CustomView: UIView {
    weak var delegate: CustomContentViewDelegate?

    func sampleMethod() {
        // デフォルト引数発動
        delegate?.customContentViewDidTouchUp(self) 
        // 引数指定
        delegate?.customContentViewDidTouchUp(self, text: "テスト実装") 
    }
}

これも一つのprotocol extensionの使い方かなと思い記事にしてみました。

ただ、コードレビューで指摘された時だと実際どんな風にしたら丁寧で見易いようになるかを2~3時間ぐらいかかりましたので

このような指摘をする場合には「protocol extensionで実装したらデフォルト引数ぽくなりますのでいいと思いますよ」と

指摘して頂ければすんなり解決した話しだったりします。

RxDataSourceを使ってTableViewのHeaderとFooterを生成してみる

概要

前回はRxDataSourceを使って基本的なUITableViewのテンプレートを作ってみました。

RxDataSource

github.com

今回はこのRxDataSourceについて弄ってみてどんな事ができるのかについてより詳しくみていこうと思います。

基本的なstoryboardの配置やソースコードの配置は前回のままですので念の為コピーしたものを載せておきます。

Main.storyboard

f:id:qed805:20190224145732p:plain
rxdatasource

ViewController.swift

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

/// TableViewCellに紐付けるDataModel
struct CustomCellModel {
    var name: String
    var email: String
}

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
}

/// RxDataSourceを使ってDataModelとdataSourceを紐づけるため
extension SectionOfCustomData: SectionModelType {
    // Item に CellのDataModelを紐づける
    typealias Item = CustomCellModel
    
    // ほぼテンプレで可能
    init(original: SectionOfCustomData, items: [Item]) {
        self = original
        self.items = items
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!

    let disposeBag = DisposeBag()
    
    let dataSource = RxTableViewSectionedReloadDataSource<SectionOfCustomData>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item.name
            return cell
    })
    
    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    // SectionOfCustomData の
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")]),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }
}

これが前回までのソースコードとなります。

今回はこれの変更を加えます。

HeaderViewの追加

試しにHeaderViewを追加してみます。RxDataSourceのライブラリのReadmeを確認すると

RxDataSource.swift

/// HeaderViewの作成
dataSource.titleForHeaderInSection = { dataSource, index in
  return dataSource.sectionModels[index].header
}

/// FooterViewの作成
dataSource.titleForFooterInSection = { dataSource, indexPath in
  return dataSource.sectionModels[index].footer
}

このようになっています。 そのため、viewDidLoad()のコードを次のように修正してみます。

ViewController.swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // HeaderViewを追加する
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }

このように修正してアプリをビルドしてみます。

f:id:qed805:20190224150449p:plain
tableView_headerview

このように無事にヘッダーが表示されたら成功です。

FooterViewの追加

では、次にCustomなFooterViewの追加を実装してみます。

まずはSectionOfCustomDataを次のように修正してみます。

ViewController.swift

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
    /// セクションフッターの名前
    var footer: String
}

セクションフッターの名前を追加しました。

これによって変数sectionsの修正が必要になります。

ViewController.swift

    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")],
                            footer: "1st section footer"),  // footer: を追加しました
    ]

これによってsectionsのデータ構造が変わってfooterも追加されます。
ですが、これだけではUITableViewには反映されません。

最後にviewDidLoad()をこのように修正します。

ViewController.swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // HeaderViewを追加する
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        // FooterViewを追加する
        dataSource.titleForFooterInSection = { dataSource, index in
            return dataSource.sectionModels[index].footer
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }

これでアプリをビルドしてみましょう。

次のようにfooterが追加表示されていたら成功です。

f:id:qed805:20190224152517p:plain
tableView_footer

Footerが表示されるようになりました。

このようにRxDataSourceを上手く使えば普通のUITableViewのようにデータを扱えるようになります。

Qiitaの記事ではRxDataSourceの使い方よりもそれを意識した設計方針の方が難しく感じますね。

RxDataSourceに苦手意識を持っている僕のような方であれば今回の記事を参考にして最低限の実装方法を理解すれば自ずと 設計方針が見えてくると思います。

とりあえず、これでUITableViewの実装は一通り経験できたので頑張れそうだと思います。

全体のソースコード

一応、念の為、全体のソースコードを乗せておきます。

ViewController.swift

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

/// TableViewCellに紐付けるDataModel
struct CustomCellModel {
    var name: String
    var email: String
}

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
    /// セクションフッターの名前
    var footer: String
}

/// RxDataSourceを使ってDataModelとdataSourceを紐づけるため
extension SectionOfCustomData: SectionModelType {
    // Item に CellのDataModelを紐づける
    typealias Item = CustomCellModel
    
    // ほぼテンプレで可能
    init(original: SectionOfCustomData, items: [Item]) {
        self = original
        self.items = items
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!

    let disposeBag = DisposeBag()
    
    let dataSource = RxTableViewSectionedReloadDataSource<SectionOfCustomData>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item.name
            return cell
    })
    
    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")],
                            footer: "1st section footer"),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // HeaderViewを追加する
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        // FooterViewを追加する
        dataSource.titleForFooterInSection = { dataSource, index in
            return dataSource.sectionModels[index].footer
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }
}

なんやかんやでちょっとずつ複雑になってきましたね。