20190627のSwiftに関する記事は12件です。

RxSwiftを使ったTableViewの実装(Delegate/reloadDataは使わない!)

Delegateで実装したくない!

TableViewを実装しようとすると、Delegateで結構ソースの幅を取るのが嫌いです。
しかもTableViewの元データに変更が会った時、わざわざ.reloadData()をしなければならない。。

そんなことしなくても、元データを監視して自動でTableViewをリロードして欲しい!

そんな方法を RxSwift を用いて紹介しちゃう会。

 実装

// 元データ
var list: Variable<[String]> = Variable([])

// viewDidLoadとかで読み込み
private func observeList() {
  list.asDriver()
    .drive(rx.items(cellIdentifier: "cell", cellType: CustomCell.self)) { (row, element, cell) in
      // cellの描画処理
      cell.setup(element)
    }.disposed(by: disposeBag)
}

これでlistの値が更新されると、TableViewがリロードしてくれます。
ソースも減って、元データの監視もできて、この書き方覚えたら元には戻れない。

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

Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。

はじめに

本記事で分かること

  • Firebaseで新規プロジェクトの作成〜アプリにFirebaseを組み込むまで
  • Firebase Realtime Databaseを用いた超簡易的なチャットアプリ

本記事で分からないこと

  • Firebaseのアカウント作成
  • いい感じのUIのアプリ

完成形

1. Firebaseで新規プロジェクトの作成

まず、Firebase へアクセスして、コンソールへ移動。
下記の赤枠箇所より、新規プロジェクトを追加。
スクリーンショット 2019-06-26 21.22.02.png
下記のように、プロジェクト名、アナリティクスの地域を選択してプロジェクトを作成。
スクリーンショット 2019-06-27 20.55.41.png
これで作成完了。

2. アプリにFirebaseを導入

CocoapodsでFirebaseを導入します。
詳細は、公式ドキュメントを参照してください。

コマンド実行後は、拡張子が.xcworkspaceとなっている方からプロジェクトを開くようにしてください。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
スクリーンショット 2019-06-27 21.09.33.png

2. Firebaseコンソール画面でiOSアプリの作成

ここからはFirebase公式の手順に従ってください。
先程作成したプロジェクトのページから、以下の赤枠をクリック
スクリーンショット 2019-06-27 21.16.19.png

バンドルIDはXcode上の赤枠の項目を参照
スクリーンショット 2019-06-27 21.16.31.png

スクリーンショット 2019-06-27 21.22.26.png

後はFirebaseの画面上のステップ通りに進めてください。

スクリーンショット 2019-06-27 21.16.58.png

スクリーンショット 2019-06-27 21.17.22.png

こちらについては、
今回導入するのは、Firebase/core と Firebase/Database になります。

以下コマンドでpodfileの作成。

shell
pod init

podfileの内容は下記になります。プロジェクト名は各々変更してください。

podfile
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '10.0'
use_frameworks!
target 'firebase-chatting' do
  pod 'Firebase/Core'
  pod 'Firebase/Database'
end

そしたら、以下のコマンドを実行しましょう。

shell
pod install

スクリーンショット 2019-06-27 21.17.32.png

スクリーンショット 2019-06-27 21.17.41.png

はい!これでFirebaseの導入は完了です!!
ここまでだいたい10秒ですかね、、www笑

3. RealTimeDatabaseのルールを変更

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

ルールを上記のように設定して、読み込み権限、書き込み権限を与えます。
設定する場所はこちら↓↓↓
スクリーンショット 2019-06-27 21.32.22.png

4. UI画面の作成

Xcodeの右上の赤枠の箇所をクリックして、UI部品の選択画面を表示する。
スクリーンショット 2019-06-27 21.44.57.png

テキスト表示用のUITextViewをドラッグ&ドロップ
スクリーンショット 2019-06-27 21.37.29.png

UITextViewにConstraints(制約)を付与
今回はSafeAreaに対して、余白15の制約をつけます。
スクリーンショット 2019-06-27 21.38.04.png

画面下部にテキスト入力用のUIViewをドラッグ&ドロップ
スクリーンショット 2019-06-27 21.38.47.png

UIViewの制約をSafeAreaに対して、左0,右0,下0で付与する
スクリーンショット 2019-06-27 21.56.05.png

UIViewの中に入力用のUITextFieldを配置(2つ)
スクリーンショット 2019-06-27 21.40.35.png

UIViewの中にコメント送信用のボタンを配置
スクリーンショット 2019-06-27 21.40.47.png

各部品に制約を付与
スクリーンショット 2019-06-27 21.42.45.png
スクリーンショット 2019-06-27 21.43.25.png
スクリーンショット 2019-06-27 21.43.58.png

これで一旦画面は作成完了です。
スクリーンショット 2019-06-27 22.01.37.png

5.配置したUI部品とコードの紐づけ

Xcode右上の赤枠の箇所を選択して、画面分割させます。
スクリーンショット 2019-06-27 22.03.38.png

左側がstoryboard。右側がViewControlle.swiftが開かれている状態にします。
スクリーンショット 2019-06-27 22.05.28.png

左のstoryBoardから、controlを押しながらドラッグ&ドロップします。
名前は適当につけてください。
UITextViewとUITextFieldを紐づけてください。
スクリーンショット 2019-06-27 22.09.14.png

ボタンはActionを指定して同じように紐づけてください。
スクリーンショット 2019-06-27 22.13.55.png

最後に、画面下部のView.bottomとSafeArea.bottomにつけられている制約も同じくドラッグ&ドロップで紐づけます。
スクリーンショット 2019-06-27 22.15.50.png

6. データの読み込み処理

初期設定

Database.database().reference()でインスタンスを取得します。

ViewController.swift
    var databaseRef: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()

        databaseRef = Database.database().reference()

データの読み込み

observeでイベントの監視を行います。
.childAddedを指定することで、子要素が追加されたときにwithで与えた処理が実行されます。
データに追加があると自動で呼ばれるので、更新処理を実装する必要はありません。

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })

    }

7. データの書き込み

ボタンを押した時に、UITextField2つに入力されている値を送信します。

ViewController.swift
    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            let messageData = ["name": name, "message": message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

ここまでで、一旦動かしてみてください。
入力時に入力域がキーボードで隠れてしまいますよね、、
なので、ここからはおまけ的にキーボードに入力域が隠れないようにする方法を紹介します。

8. 入力域がキーボードで隠れないように動的に入力域の位置を変更

ViewDidload内に以下を追記。
こちらは、キーボードが表示されるタイミングと非表示になるタイミングを監視してくれるものになります。

ViewController.swift
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)

そしたら、上記によって呼び出されるメソッドにて入力域の末端の制約を動的に変更させます。

ViewController.swift
    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

9. 完成

これで完成です!!

最後にViewControllerの全貌を貼っておきます。

ViewController.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var nameInputView: UITextField!
    @IBOutlet weak var messageInputView: UITextField!
    @IBOutlet weak var inputViewBottomMargin: NSLayoutConstraint!

    var databaseRef: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()

        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            dump(snapshot)
            if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })

        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)

    }

    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            let messageData = ["name": name, "message": message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

}

最後に

実際にだいたい1時間くらいで実装できてしまったので、ほんとに驚きです。
今後Firebaseの諸々を調査していく予定です。

よかったらいいねください!!

参考記事

Firebase を使って30分でiOSのチャットアプリを作ってみる(新SDK対応版)
【Swift】リアルタイムチャットを実現するFirebaseでCRUD(データ作成、読み込み、更新、削除)をやってみる

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

【Swift】Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。

はじめに

本記事で分かること

  • Firebaseで新規プロジェクトの作成〜アプリにFirebaseを組み込むまで
  • Firebase Realtime Databaseを用いた超簡易的なチャットアプリ

本記事で分からないこと

  • Firebaseのアカウント作成
  • いい感じのUIのアプリ

完成形

1. Firebaseで新規プロジェクトの作成

まず、Firebase へアクセスして、コンソールへ移動。
下記の赤枠箇所より、新規プロジェクトを追加。
スクリーンショット 2019-06-26 21.22.02.png
下記のように、プロジェクト名、アナリティクスの地域を選択してプロジェクトを作成。
スクリーンショット 2019-06-27 20.55.41.png
これで作成完了。

2. アプリにFirebaseを導入

CocoapodsでFirebaseを導入します。
詳細は、公式ドキュメントを参照してください。

コマンド実行後は、拡張子が.xcworkspaceとなっている方からプロジェクトを開くようにしてください。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
スクリーンショット 2019-06-27 21.09.33.png

2. Firebaseコンソール画面でiOSアプリの作成

ここからはFirebase公式の手順に従ってください。
先程作成したプロジェクトのページから、以下の赤枠をクリック
スクリーンショット 2019-06-27 21.16.19.png

バンドルIDはXcode上の赤枠の項目を参照
スクリーンショット 2019-06-27 21.16.31.png

スクリーンショット 2019-06-27 21.22.26.png

後はFirebaseの画面上のステップ通りに進めてください。

スクリーンショット 2019-06-27 21.16.58.png

スクリーンショット 2019-06-27 21.17.22.png

こちらについては、
今回導入するのは、Firebase/core と Firebase/Database になります。

以下コマンドでpodfileの作成。

shell
pod init

podfileの内容は下記になります。プロジェクト名は各々変更してください。

podfile
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '10.0'
use_frameworks!
target 'firebase-chatting' do
  pod 'Firebase/Core'
  pod 'Firebase/Database'
end

そしたら、以下のコマンドを実行しましょう。

shell
pod install

スクリーンショット 2019-06-27 21.17.32.png

スクリーンショット 2019-06-27 21.17.41.png

はい!これでFirebaseの導入は完了です!!
ここまでだいたい10秒ですかね、、www笑

3. RealTimeDatabaseのルールを変更

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

ルールを上記のように設定して、読み込み権限、書き込み権限を与えます。
設定する場所はこちら↓↓↓
スクリーンショット 2019-06-27 21.32.22.png

4. UI画面の作成

Xcodeの右上の赤枠の箇所をクリックして、UI部品の選択画面を表示する。
スクリーンショット 2019-06-27 21.44.57.png

テキスト表示用のUITextViewをドラッグ&ドロップ
スクリーンショット 2019-06-27 21.37.29.png

UITextViewにConstraints(制約)を付与
今回はSafeAreaに対して、余白15の制約をつけます。
スクリーンショット 2019-06-27 21.38.04.png

画面下部にテキスト入力用のUIViewをドラッグ&ドロップ
スクリーンショット 2019-06-27 21.38.47.png

UIViewの制約をSafeAreaに対して、左0,右0,下0で付与する
スクリーンショット 2019-06-27 21.56.05.png

UIViewの中に入力用のUITextFieldを配置(2つ)
スクリーンショット 2019-06-27 21.40.35.png

UIViewの中にコメント送信用のボタンを配置
スクリーンショット 2019-06-27 21.40.47.png

各部品に制約を付与
スクリーンショット 2019-06-27 21.42.45.png
スクリーンショット 2019-06-27 21.43.25.png
スクリーンショット 2019-06-27 21.43.58.png

これで一旦画面は作成完了です。
スクリーンショット 2019-06-27 22.01.37.png

5.配置したUI部品とコードの紐づけ

Xcode右上の赤枠の箇所を選択して、画面分割させます。
スクリーンショット 2019-06-27 22.03.38.png

左側がstoryboard。右側がViewControlle.swiftが開かれている状態にします。
スクリーンショット 2019-06-27 22.05.28.png

左のstoryBoardから、controlを押しながらドラッグ&ドロップします。
名前は適当につけてください。
UITextViewとUITextFieldを紐づけてください。
スクリーンショット 2019-06-27 22.09.14.png

ボタンはActionを指定して同じように紐づけてください。
スクリーンショット 2019-06-27 22.13.55.png

最後に、画面下部のView.bottomとSafeArea.bottomにつけられている制約も同じくドラッグ&ドロップで紐づけます。
スクリーンショット 2019-06-27 22.15.50.png

6. データの読み込み処理

初期設定

Database.database().reference()でインスタンスを取得します。

ViewController.swift
    var databaseRef: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()

        databaseRef = Database.database().reference()

データの読み込み

observeでイベントの監視を行います。
.childAddedを指定することで、子要素が追加されたときにwithで与えた処理が実行されます。
データに追加があると自動で呼ばれるので、更新処理を実装する必要はありません。

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })

    }

7. データの書き込み

ボタンを押した時に、UITextField2つに入力されている値を送信します。

ViewController.swift
    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            let messageData = ["name": name, "message": message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

ここまでで、一旦動かしてみてください。
入力時に入力域がキーボードで隠れてしまいますよね、、
なので、ここからはおまけ的にキーボードに入力域が隠れないようにする方法を紹介します。

8. 入力域がキーボードで隠れないように動的に入力域の位置を変更

ViewDidload内に以下を追記。
こちらは、キーボードが表示されるタイミングと非表示になるタイミングを監視してくれるものになります。

ViewController.swift
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)

そしたら、上記によって呼び出されるメソッドにて入力域の末端の制約を動的に変更させます。

ViewController.swift
    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

9. 完成

これで完成です!!

最後にViewControllerの全貌を貼っておきます。

ViewController.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var nameInputView: UITextField!
    @IBOutlet weak var messageInputView: UITextField!
    @IBOutlet weak var inputViewBottomMargin: NSLayoutConstraint!

    var databaseRef: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()

        databaseRef = Database.database().reference()

        databaseRef.observe(.childAdded, with: { snapshot in
            dump(snapshot)
            if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"] {
                let currentText = self.textView.text
                self.textView.text = (currentText ?? "") + "\n\(name) : \(message)"
            }
        })

        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)

    }

    @IBAction func tappedSendButton(_ sender: Any) {
        view.endEditing(true)

        if let name = nameInputView.text, let message = messageInputView.text {
            let messageData = ["name": name, "message": message]
            databaseRef.childByAutoId().setValue(messageData)

            messageInputView.text = ""
        }
    }

    @objc func keyboardWillShow(_ notification: NSNotification){
        if let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            inputViewBottomMargin.constant = keyboardFrameInfo.cgRectValue.height
        }

    }

    @objc func keyboardWillHide(_ notification: NSNotification){
        inputViewBottomMargin.constant = 0
    }

}

最後に

実際にだいたい1時間くらいで実装できてしまったので、ほんとに驚きです。
今後Firebaseの諸々を調査していく予定です。

よかったらいいねください!!

参考記事

Firebase を使って30分でiOSのチャットアプリを作ってみる(新SDK対応版)
【Swift】リアルタイムチャットを実現するFirebaseでCRUD(データ作成、読み込み、更新、削除)をやってみる

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

【swift】UIButtonのタイトルラベルに下線を付けた上で色を調整する方法

実現したいこと

UIButtonのタイトルラベルに下線を付けた上で色を調整する

経緯

下線を付ける方法を調べていたら

let AttributedString = NSAttributedString(string: "text", attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue
と書けば下線を弾けることが分かった。

しかし、色が青色になってしまう。

button.setTitleColor(ColorManager().white(), for: .normal)

と書いても色を変更できないため、ハマった。

結論

let AttributedString = NSAttributedString(string: "text", attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue, NSAttributedString.Key.foregroundColor : UIColor.white])
button.setAttributedTitle(AttributedString, for: .normal)

と書けば良い。

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

[SWIFT4]お絵描きペイントペンの色変え方法

swiftの学習を始めてペイントツールを作成しているのですが、

ペンの色を変える事ができません。

// 描画処理
func drawLine(path:UIBezierPath){
    UIGraphicsBeginImageContext(canvas.frame.size)
    if let image = self.lastDrawImage {
        image.draw(at: CGPoint.zero)
    }
    let lineColor = UIColor.blue // blueをデフォルトに設定
    lineColor.setStroke()
    path.stroke()
    self.canvas.image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
}

最初の指定はできるのですが、色変更ボタンを設置してblackやredに変更したいのです。

描画はできています。
ボタンで別Viewを表示し、そこで色を選択して前の画面に戻ると
ペンの色が選択した状態にするのが目標です。

ご教授いただけると助かります

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

WKWebViewでwindow.open()をオーバーライドして自画面遷移にした話

経緯

WKWebViewを使用したWebページの表示・遷移をしていた際にアプリが落ちた。

window.open().location.hrefで別ウィンドウを表示しているのが悪いらしい。

一律で自画面遷移にしてしまえwってことでやってみた。

やったこと

以下のスクリプトをWKNavigationDelegatefunc webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)内で実行。

window.open()windowを返すようにすることで、window.open().location.hrefwindow.location.hrefとなるようにしたら自画面遷移になった。

スクリプト

window.open = function (open) {
  return function(){
    return window;
  };
}(window.open);

コード

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  webView.evaluateJavaScript("window.open = function (open) { return function(){return window;};}(window.open);")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

こんな書き方もできる三項演算子

よく見る三項演算子

let data = flg == true ? self.toData("1") : self.toData("0")

応用

let data = self.toData(flg? "1" : "0")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

App Extension使用時に発生したエラー「embedded binary is not signed with the same certificate as the parent app. Verify the embedded binary target's code sign settings match the parent app's.」の解決法

[前提]iOSの開発でApp Extension (https://qiita.com/motokiee/items/1f9147e6eb18f51937af)
を利用していた。
アプリ配布のためApp本体のProvisioning Profileなどを整えた所、タイトルのエラーが発生した。

[原因]App本体とApp ExtensionでProvisioning Profileや開発チームが異なっていた事(App本体のProvisioning Profileと開発チームだけ手動で設定したのに、App Extension側は自動設定のままで放置していた事)が原因だった。

[解決法]App Extension用にProvisioning Profileを作成し、Xcodeの TARGETS > (Extensionの名前) > Signing でAutomatically manage signingを外し(手動設定にし)、同メニューのSigning(Release)> Provisioning Profile からProvisioning Profileを設定した。

同時に、TARGETS > (Extensionの名前) >Build Settings > Signing > Code Signing Identity > Release より、開発チームを親Appの開発チームと合わせた。

[参考資料]https://stackoverflow.com/questions/25927604/xcode6embedded-binary-is-not-signed-with-the-same-certificate-as-the-parent-app/29848788

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

【Swift】鉄道指向プログラミング(Railway Oriented Programming)でResultの使い方を学ぶ

少し前に「これは何だろう?」と思ったことについて調べてみました。

SwiftのResultとは?

SuccessFailureの2つのケースを持ったenumです。

多くの方が自前で今まで実装してきましたが
Swif5.0で標準ライブラリに導入されました。

https://github.com/apple/swift/blob/master/stdlib/public/core/Result.swift

鉄道指向プログラミング(Railway Oriented Programming)とは?

2014年にScott Wlaschinさんが提唱された
関数型プログラミングを行っていくなかで
エラーハンドリングをどう扱っていくかに主に焦点を当てたプログラミング手法です。

https://fsharpforfunandprofit.com/rop/

Many examples in functional programming assume
that you are always on the “happy path”.
But to create a robust real world application
you must deal with validation, logging, network
and service errors, and other annoyances.

So, how do you handle all this in a clean functional way?

This talk will provide a brief introduction to this topic,
using a fun and easy-to-understand railway analogy.

-

多くの関数型プログラミングの例は
いつも「ハッピーパス(いわゆる正常系のこと)」を通っていることを想定しているが
現実にがバリデーションやログ、ネットワーク通信やサービスのエラー
その他の腹立たしいことを扱わなければならない。

で、それをこれらをどうやってクリーンに関数型の手法で対処できるだろうか?

今回のトークでは、楽しく、わかりやすい鉄道との共通点を使ってこの問題について
簡単な導入部分を紹介する。

とあり

抽象的な概念というよりは
より具体的な問題に焦点を当てており
すぐに実践に応用できるようになっています。

Resultと鉄道指向プログラミング

鉄道指向プログラミングでは
Resultを用いたエラーハンドリングを行います。

Resultを用いることでそのままの型を用いる以上の
多くのメリットを得ることができます。

今回は
鉄道指向プログラミングはどういうものなのかが気になったのと
Resultがどのように機能するのかを理解するために
記録として残してみました。


記事の中で出てくる図はScottさんの講演スライドから拝借しています。
ブログの中で「自由にして良い」という記載がございましたので
活用させていただきました。

The powerpoint slides are also available from Github. Feel free to borrow from them!

今回の例

以下の処理を行います。

  1. リクエストを受け取る
  2. バリデーションチェックをする
  3. リクエストをDBに保存(更新)する
  4. 認証済みのメールに送信する
  5. ユーザに結果を返す

命令型プログラミングで書いた例

下記の例を見てみます

命令型プログラミングの実装
struct DB {
    func updateDb(from request: Request) throws -> Bool{
        return true
    }
}

struct SmtpServer {
    func sendEmail(to request: Request) throws -> Bool {
        return true
    }
}

func validateRequest(_ request: Request) -> Bool {
    return true
}

func executeUseCase(request: Request, db: DB, stmpServer: SmtpServer) -> String {
    if !validateRequest(request) {
        return "validation error"
    }
    do {
        let result = try db.updateDb(from: request)
        if !result {
            return "Record not found"
        }

        if !stmpServer.sendEmail(to: request) {
            return "Fail to send mail"
        }
    } catch {
        return "Fail to update"
    }
    return "OK"
}

これはいわゆる命令型と呼ばれるような形で書かれています。

特に問題はないのですが
こうすると
ifで判定をしたり
do catch文が途中で入ってくるため
本来のやりたいことが見えづらくなってしまいます。

では
このようなエラーハンドリングを
どうやって関数型プログラミングを使って
綺麗な形で処理できるでしょうか?

結果のパターンを考えてみる

上記の例の処理の流れを考えてみます。

スクリーンショット 2019-06-01 5.09.47.png

Requestを受け取り
処理が成功した場合は次の処理へ
エラーの場合はエラーになった時点で
レスポンスを返しています。

次に関数型プログラミングの形で考えてみます。

スクリーンショット 2019-06-01 5.09.20.png

関数型では処理を上から下へ向かう
データの流れとして捉えます。

ここで上記の図のように処理の結果は
4つのパターンが考えられます。

このようなレスポンスをどうやって表現することができるでしょうか?

enum Result {
    case Success
    case ValidationError
    case UpdateError
    case SendMailError
}

パターンということでenumとして捉えました。

これですべてのケースを網羅できていますが
他の処理でも同じように使えるようにしたいですね。

それがSwiftのResultです。

public enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

※ F#にもResultはありますがSwiftのFailureはErrorプロトコルに適合する必要があります。

Resultを使うと処理の流れは下記のようになります。

スクリーンショット 2019-06-01 5.21.22.png

こうすることで各関数が同じレスポンスを返すようになります。
下記にResultを返す関数を提示してみます。

Resultを返す関数
struct ValidationError: LocalizedError {
    let field: String
    let value: Any
    let reason: String

    var localizedDescription: String {
        return "\(field) \(value) is not valid because \(reason)"
    }
}

enum UseCaseError: LocalizedError {
    case validation(ValidationError)
    case update(Error)
    case sendMail(Error)

    var localizedDescription: String {
        switch self {
        case .validation(let error):
            return error.localizedDescription
        case .update(let error):
            return "update error \(error)"
        case .sendMail(let error):
            return "sendMail error \(error)"
        }
    }
}

struct DB {
    func updateDb(from request: Request) -> Result<Request, UseCaseError> {
        return Result { try updateDb(request) }.mapError(UseCaseError.update)
    }

    private func updateDb(_ request: Request) throws -> Request {
        return request
    }
}

struct SmtpServer {
    func sendEmail(to request: Request) -> Result<Request, UseCaseError> {
        return Result {
            try sendEmail(request.email)
            return request
        }.mapError(UseCaseError.sendMail)
    }

    private func sendEmail(_ email: String) throws -> Void {
        return
    }
}

func validateRequest(_ request: Request) -> Result<Request, UseCaseError> {
    if request.name.isEmpty {
        let error = ValidationError(field: "name", value: request.name, reason: "name should not be empty")
        return .failure(.validation(error))
    }
    return .success(request)
}

同じレスポンスを返すということは
各関数を一つのワークフローとして組み合わせて
全体の処理を構成できそうですね。


値はなんでも良いsuccess
またはUseCaseErrorを持ったfailure
を返す

しかし
それぞれの型を見ると

(Request) -> Result<Request, UseCaseError>

ということで型が合いません。

どうやって各処理を連結できるようになるでしょうか?

鉄道の車線をイメージしてみる(鉄道指向プログラミング)

下記の図を見てください。

スクリーンショット 2019-06-01 5.58.31.png

一つのインプットを与えると一つのアウトプットを出力する関数を
鉄道の車線に例えています。

次の図を見てください。

スクリーンショット 2019-06-01 9.44.17.png
スクリーンショット 2019-06-01 9.46.21.png
スクリーンショット 2019-06-01 9.46.28.png

もう一つ車線が出てきました。
左の関数のアウトプットと右の関数のインプットが一致しているため
この二つの車線を繋げることができます。

このような場合は
シンプルですぐに理解できると思います。

Resultの場合はアウトプットが2つの可能性があります。
これを表現するためには車線の分岐が必要になります。

スクリーンショット 2019-06-01 9.52.25.png

Success車線とFailure車線ができます。

このような分岐を生じる関数を
スイッチ関数
と呼びます。

ではスイッチ関数を連結した場合の動きはどうなるでしょうか?

スクリーンショット 2019-06-01 10.02.15.png
スクリーンショット 2019-06-01 10.02.23.png

上記の例で説明すると
Validate関数が
成功した場合 -> Success車線を通りUpdateDbを実行する
失敗した場合 -> Failure車線を通りUpdateDb実行せずFailure車線を通り続ける

となります。

言い換えると

処理が成功している場合のみ処理を継続し
エラーが発生した場合は以降の処理を行わずに最終的なアウトプットまで進む

ことになります。

なんとなくイメージはできたでしょうか?

では
問題のResultの連結方法について車線で考えてみます。

上記で示した1つの車線の関数の連結はシンプルでした。

同様にインプットとアウトプットが2車線同士の関数の場合もシンプルです。

スクリーンショット 2019-06-01 10.21.28.png

インプットとアウトプットが一致すればそのまま繋げることができます。

スクリーンショット 2019-06-01 10.27.22.png

しかしResultを返すような関数は通常の値をインプットとして受け取るため
インプットとアウトプットが一致するため連結できません。

ではどうすれば良いのか?

車線の数を合わせれば良い

のです。

1車線インプット、2車線アウトプットの関数から
2車線インプット、2車線アウトプットの関数へ変換する
ことで2車線関数同士の関数を繋ぎ合わせることと同じになります。

スクリーンショット 2019-06-01 10.27.33.png

スクリーンショット 2019-06-01 10.27.47.png

それを実現するのがflatMapです。
Scottさんの講演ではアダプターブロックと読んでいました。

flatMapの実装
public enum Result<Success, Failure: Error> {
    case success(Success)    
    case failure(Failure)

    public func flatMap<NewSuccess>(
        _ transform: (Success) -> Result<NewSuccess, Failure>
        ) -> Result<NewSuccess, Failure> {
        switch self {
        case let .success(success):
            return transform(success)
        case let .failure(failure):
            return .failure(failure)
        }
    }
}

https://github.com/apple/swift/blob/master/stdlib/public/core/Result.swift#L96

引数に
ResultSuccess型を引数にして
変換してResult<NewSuccess, Error>を返す関数を受け取り
Result<NewSuccess, Error>を返します。

このtransformの形に注目すると

(Success) -> Result<NewSuccess, Failure>

これはまさにスイッチ関数と同じ形です。

Scottさんの講演では下記のようなbind関数を定義しています。

func bind<A,B>(_ switchFuntion: @escaping (A) -> Result<B, Error>) -> (Result<A, Error>) -> Result<B, Error> {
    return { (a: Result<A, Error>) in
        switch a {
        case .success(let x):
            return switchFuntion(x)

        case .failure(let error):
            return .failure(error)
        }
    }
}

これを活用することもできますが
Swiftの標準で使われているメソッドで考えていきたいと思います。

Resultを用いた処理の例

最初の方で命令型で書いた例をResultを使った形で考えてみます。

Resultを使った実装例
...

struct DB {
    func updateDb(from request: Request) -> Result<Request, UseCaseError> {
        return Result { try updateDb(request) }.mapError(UseCaseError.update)
    }

    func updateDb(_ request: Request) throws -> Request {
        return request
    }
}

struct SmtpServer {
    func sendEmail(to request: Request) -> Result<Request, UseCaseError> {
        return Result {
            try sendEmail(request.email)
            return request
        }.mapError(UseCaseError.sendMail)
    }

    func sendEmail(_ email: String) throws -> Void {
        return
    }
}

func validateRequest(_ request: Request) -> Result<Request, UseCaseError> {
    if request.name.isEmpty {
        let error = ValidationError(field: "name", value: request.name, reason: "name should not be empty")
        return .failure(.validation(error))
    }
    return .success(request)
}

func executeUserCase(request: Request, db: DB, stmpServer: SmtpServer) -> String {

    switch validateRequest(request)
        .flatMap(db.updateDb)
        .flatMap(stmpServer.sendEmail(to:)) {
    case .success:
        return "OK"
    case .failure(let error):
        return error.localizedDescription
    }
}

flatMapを使うことで連結ができるようになりました。

それでは動作を確認してみます。

let request = Request(userId: 1, name: "hoge", email: "hoge@hoge.com")
let result = executeUseCase(request: request, db: DB(), stmpServer: SmtpServer())
print(result) // success("OK")

値がきちんと設定されている場合はsuccessになります。

ではnameを空文字にしてみたいと思います。

let request = Request(userId: 1, name: "", email: "hoge@hoge.com")
let result = executeUseCase(request: request, db: DB(), stmpServer: SmtpServer())

print(result) 
// failure(UseCaseError.validation(ValidationError(field: "name", value: "", reason: "name should not be empty")))

UseCaseError.validationが出力されました。

想定だとそれ以降のメソッドは呼ばれていないはずですので確認をします。

struct DB {
    func updateDb(from request: Request) -> Result<Request, UseCaseError> {
        print("updateDb")
        return Result { try updateDb(request) }.mapError(UseCaseError.update)
    }
}

struct SmtpServer {
    func sendEmail(to request: Request) -> Result<Request, UseCaseError> {
        print("sendEmail")
        return Result {
            try sendEmail(request.email)
            return request
        }.mapError(UseCaseError.sendMail)
    }
}

この状態でもう一度successを出力すると

// updateDb
// sendEmail
// success("OK")

と出ますが
nameを空文字すると

// failure(UseCaseError.validation(ValidationError(field: "name", value: "", reason: "name should not be empty")))

とエラーのみ出力され
その先のメソッドが呼ばれていないことが確認できました。

他のメソッドと組み合わせるには?

下記のメソッドを追加したいとします。

func canonicalizeEmail(_ request: Request) -> Request {
    let canonicalized = request.email.trimmingCharacters(in: .whitespaces).lowercased()
    return Request(userId: request.userId, name: request.name, email: canonicalized)
}

これはResultは登場しない1車線関数です。

これも連結して処理できるようにしたいですが

スクリーンショット 2019-06-01 15.44.01.png

1車線関数のアプトプットと2車線関数のインプットをそのまま繋げることはできません。
同様に2車線関数のインプットと1車線関数のアウトプットをそのまま繋げることはできません。

ではどうするか?

これもflatMapの時と同じように考えます。

スクリーンショット 2019-06-01 15.48.19.png

つまり1車線関数を2車線関数に変換するようにします。

これを実現するのはmapです。

Mapの実装
public enum Result<Success, Failure: Error> {

    public func map<NewSuccess>(
        _ transform: (Success) -> NewSuccess
        ) -> Result<NewSuccess, Failure> {
        switch self {
        case let .success(success):
            return .success(transform(success))
        case let .failure(failure):
            return .failure(failure)
        }
    }
}

https://github.com/apple/swift/blob/master/stdlib/public/core/Result.swift#L41

transformを見てみると1車線関数を渡してResult型に変換して返します。

これを使用すると

func executeUserCase(request: Request, db: DB, stmpServer: SmtpServer) -> String {    

    switch validateRequest(request)
        .map(canonicalizeEmail) // ← ここ
        .flatMap(db.updateDb)
        .flatMap(stmpServer.sendEmail(to:)) {

    case .success:
        return "OK"
    case .failure(let error):
        return error.localizedDescription
}

と上記のように連結することができました。

次に下記のメソッドを考えてみたいと思います。

struct DB {
    func updateDbVoid(_ request: Request) throws -> Void {
        return
    }
}

これはまず1車線関数のため連結できません。
さらにアウトプットがないためmapを使っても連結ができません。

※こういう関数はデッドエンド関数と呼ばれているそうです。

スクリーンショット 2019-06-01 16.33.01.png

ではどうするのか?

インプットで受け取った値を内部で関数を実行した後に
そのままインプットの値を返すようにします。

スクリーンショット 2019-06-01 16.35.56.png

今回はメソッドを一つ追加します。

extension Result {
    static func tee(_ f: @escaping (Success) -> ()) -> (Success) -> Result<Success, Failure> {
        return { a in
            f(a)
            return .success(a)
        }
    }

    static func tee(_ f: @escaping (Success) throws -> ()) -> (Success) -> Result<Success, Error> {
        return { a in
            do {
                try f(a)
                return .success(a)
            } catch {
                return .failure(error)
            }
        }
    }
}

これを先ほどのupdateDbVoidに適用します。

struct DB {

    func updateDb(_ request: Request) -> Result<Request, UseCaseError> {
        return Result<Request, UseCaseError>
            .tee(self.updateDbVoid)(request).mapError(UseCaseError.update)
    }

    func updateDbVoid(_ request: Request) throws -> Void {
        return
    }
}

こうすると今までと同じように扱うことができます。

func executeUserCase(request: Request, db: DB, stmpServer: SmtpServer) -> String {

    switch validateRequest(request)
        .map(canonicalizeEmail)
        .flatMap(db.updateDb)
        .flatMap(stmpServer.sendEmail(to:)) {
    case .success:
        return "OK"
    case .failure(let error):
        return error.localizedDescription
    }
}

Resultを使うことで得られること

これまで見てきたように
Resultを使うことで
全体で統一的にエラーハンドリングを行えるようになりました。

さらに各メソッドの型を見てみると

executeUserCase: (Request, DB, SmtpServer) -> String

validateRequest: (Request) -> Result<Request, UseCaseError>
canonicalizeEmail: (Request) -> Request
updateDb: (Request) -> Result<Request, UseCaseError>
sendEmail(Request) -> Result<Request, UseCaseError>

となっています。

Resultを使ったことで
メソッドが失敗するかもしれないということを
目で見てわかるようになりました。

具体的なエラーの内容はenumで確認できます。

enum UseCaseError {
    case validation(ValidationError)
    case update(Error)
    case sendMail(Error)
}

... 一部省略

こうすることで
他の人が見てもどういう処理をしているのかを型で伝えやすくなり
いわゆる自己文書化(Self-documenting)につながります。

※ 自己文書化については下記のサイトなどに詳しく書かれています
https://www.webprofessional.jp/self-documenting-javascript/

その他の鉄道指向プログラミング

この他にも色々な場合に関しての
鉄道指向プログラミングのアプローチが紹介されています。

いくつか挙げたいと思います。

複数のエラーが欲しい場合は?

これまで見てきたケースですと
エラーは一つしか扱うことができません。

スクリーンショット 2019-06-02 5.12.58.png

例えばバリデーションチェックのエラーは
発生した全てのエラーが欲しい場合があるかもしれません。

そのような場合
講演の中では詳細には触れていませんが
それぞれの処理をPairとして組み合わせていけば
いくつでも組み合わせることができる
といったことをおっしゃっています。

スクリーンショット 2019-06-02 5.15.09.png

講演で紹介されていたブログの内容はこちら↓
https://fsharpforfunandprofit.com/posts/monoids-without-tears/

SwiftではiOS13よりCombineというフレームワークが増え
その中でZipが定義されています。
https://developer.apple.com/documentation/combine/publishers/zip
※2019/6/22現在のベータ版の情報です。

また
zipを定義しているライブラリがあります。

例えば下記のライブラリではzipというメソッドで複数の処理結果をペアの組み合わせにしています。
https://github.com/pointfreeco/swift-validated

※ちなみにzipはHaskellなどの関数型プログラミングでも定義されており
同様の使われ方をされています。

RxSwiftでも使用されています。
https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Observables/Zip.swift
https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Observables/Zip+arity.swift#L23

ドメインイベントなどのメッセージを渡したい場合

処理結果以外に他の機能にメッセージを送りたい場合もあるかもしれません。
(講演ではメールの送信に成功したことをCRMに伝えるなど)

スクリーンショット 2019-06-02 5.35.12.png

このような場合は
successの場合にメッセージをリストとして持ち
処理結果とメッセージのリストの組み合わせを伝達するようにします。

スクリーンショット 2019-06-02 5.33.47.png

Githubを参考にSwiftでも実装してみました(結構無理あり:sweat_smile:)
struct Request {
    let userId: Int
    let name: String
    let email: String
}

struct ValidationError: LocalizedError {
    let field: String
    let value: Any
    let reason: String

    var localizedDescription: String {
        return "\(field): \(value) is not valid because \(reason)"
    }
}

enum UseCaseMessage: LocalizedError {
    case validation(ValidationError)
    case update(Error)
    case sendMail(Error)


    case UpdateSuccess
    case SendMailSuccess

    var localizedDescription: String {
        switch self {
        case .validation(let error):
            return error.localizedDescription
        case .update(let error):
            return "update error \(error)"
        case .sendMail(let error):
            return "sendMail error \(error)"
        case .UpdateSuccess:
            return "Update Success"
        case .SendMailSuccess:
            return "Send Mail Success"
        }
    }
}

extension Array: Error where Element: Error {}

enum ROPResult<Success, Message> {
    case success(Success, [Message])
    case failure([Message])

    func map<NewSuccess>(_ f: (Success) -> NewSuccess) -> ROPResult<NewSuccess, Message> {
        switch self {
        case .success(let x, let msgs):
            return .success(f(x), msgs)
        case .failure(let errors):
            return .failure(errors)
        }
    }

    func flatMap<NewSuccess>(
        _ transform: (Success) -> ROPResult<NewSuccess, Message>
        ) -> ROPResult<NewSuccess, Message> {
        switch self {
        case .success(let x, let msgs):
            do {
                let result = try transform(x).get()
                return .success(result.0, result.1 + msgs)
            } catch let errors as [Error] {
                return .failure(errors as! [Message])
            } catch {
                return .failure([error] as! [Message])
            }
        case .failure(let errors):
            return .failure(errors)
        }
    }

    func get() throws -> (Success, [Message]) {
        switch self {
        case .success(let x, let msgs):
            return (x, msgs)
        case .failure(let errors):
            throw errors as! [Error]
        }
    }

    func mapError(
        _ transform: (Message) -> Message
        ) -> ROPResult<Success, Message> {
        switch self {
        case .success(let x, let msgs):
            return .success(x, msgs)
        case .failure(let errors):
            let newErrors = errors.map(transform)
            return .failure(newErrors)
        }
    }

    static func tee(_ f: @escaping (Success) throws -> (), msgs: [Message] = []) -> (Success) -> ROPResult {
        return { a in
            do {
                try f(a)
                return .success(a, msgs)
            } catch {
                return .failure([error] as! [Message])
            }
        }
    }
}

extension ROPResult where Message == Swift.Error {
    init(catching body: () throws -> (Success, Message)) {
        do {
            let result = try body()
            self = .success(result.0, [result.1])
        } catch {
            self = .failure([error])
        }
    }
}

struct DB {
    func updateDb(_ request: Request) -> ROPResult<Request, UseCaseMessage> {
        return ROPResult<Request, UseCaseMessage>
            .tee(self.updateDbVoid, msgs: [UseCaseMessage.UpdateSuccess])(request)
            .mapError(UseCaseMessage.update)
    }

    func updateDbVoid(_ request: Request) throws -> Void {
        return
    }
}

struct SmtpServer {
    func sendEmail(to request: Request) -> ROPResult<Request, UseCaseMessage> {
        do {
            try sendEmail(request.email)
            return .success(request, [UseCaseMessage.SendMailSuccess])
        } catch {
            return .failure([UseCaseMessage.sendMail(error)])
        }
    }

    func sendEmail(_ email: String) throws -> Void {
        return
    }
}

func validateRequest(_ request: Request) -> ROPResult<Request, UseCaseMessage> {
    if request.name.isEmpty {
        let error = ValidationError(field: "name", value: request.name, reason: "name should not be empty")
        return .failure([.validation(error)])
    }
    return .success(request, [])
}

func canonicalizeEmail(_ request: Request) -> Request {
    let canonicalized = request.email.trimmingCharacters(in: .whitespaces).lowercased()
    return Request(userId: request.userId, name: request.name, email: canonicalized)
}

func executeUserCase(request: Request, db: DB, stmpServer: SmtpServer) -> String {

    switch validateRequest(request)
        .map(canonicalizeEmail)
        .flatMap(db.updateDb)
        .flatMap(stmpServer.sendEmail(to:)) {
    case .success(let x, let messages):
        // Request(userId: 1, name: "hoge", email: "hoge@hoge.com")
        print("\(x)")

        // Messages are [UseCaseMessage.SendMailSuccess, UseCaseMessage.UpdateSuccess]
        print("Messages are \(messages)")

        return "OK"
    case .failure(let errors):
        return errors.reduce("", { total, error in
            return total + error.localizedDescription
        })
    }
}

let request = Request(userId: 1, name: "hoge", email: "hoge@hoge.com")
let result = executeUserCase(request: request, db: DB(), stmpServer: SmtpServer())
print("result is \(result)") // result is OK

let errorRequest = Request(userId: 1, name: "", email: "hoge@hoge.com")
let errorResult = executeUserCase(request: errorRequest, db: DB(), stmpServer: SmtpServer())
print("errorResult is \(errorResult)") // errorResult is name:  is not valid because name should not be empty


※ 講演の中では一覧ができるから見やすいとのことで
エラーとドメインのメッセージを一緒に扱っていますが
エラーとメッセージは別に違う型でも
良いのではないかなと個人的には思っています。
そもそもResultで分岐しているので分かるから良いのかもしれませんが。

Failureを分けてみた例(これも結構無理あり:sweat_smile:)
struct Request {
    let userId: Int
    let name: String
    let email: String
}

struct ValidationError: LocalizedError {
    let field: String
    let value: Any
    let reason: String

    var localizedDescription: String {
        return "\(field): \(value) is not valid because \(reason)"
    }
}

enum UseCaseError: LocalizedError {
    case validation(ValidationError)
    case update(Error)
    case sendMail(Error)

    var localizedDescription: String {
        switch self {
        case .validation(let error):
            return error.localizedDescription
        case .update(let error):
            return "update error \(error)"
        case .sendMail(let error):
            return "sendMail error \(error)"
        }
    }
}

enum UseCaseMessage {
    case UpdateSuccess
    case SendMailSuccess
}

extension Array: Error where Element: Error {}

enum ROPResult<Success, Failure: Error, Message> {
    case success(Success, [Message])
    case failure([Failure])

    func map<NewSuccess>(_ f: (Success) -> NewSuccess) -> ROPResult<NewSuccess, Failure, Message> {
        switch self {
        case .success(let x, let msgs):
            return .success(f(x), msgs)
        case .failure(let errors):
            return .failure(errors)
        }
    }

    func flatMap<NewSuccess>(
        _ transform: (Success) -> ROPResult<NewSuccess, Failure, Message>
        ) -> ROPResult<NewSuccess, Failure, Message> {
        switch self {
        case .success(let x, let msgs):

            do {
                let result = try transform(x).get()
                return .success(result.0, result.1 + msgs)
            } catch let errors as [Error] {
                return .failure(errors as! [Failure])
            } catch {
                return .failure([error] as! [Failure])
            }
        case .failure(let errors):
            return .failure(errors)
        }
    }

    func get() throws -> (Success, [Message]) {
        switch self {
        case .success(let x, let msgs):
            return (x, msgs)
        case .failure(let errors):
            throw errors
        }
    }

    func mapError<NewFailure>(
        _ transform: (Failure) -> NewFailure
        ) -> ROPResult<Success, NewFailure, Message> {
        switch self {
        case .success(let x, let msgs):
            return .success(x, msgs)
        case .failure(let errors):
            let newErrors = errors.map(transform)
            return .failure(newErrors)
        }
    }

    static func tee(_ f: @escaping (Success) throws -> (), msgs: [Message] = []) -> (Success) -> ROPResult {
        return { a in
            do {
                try f(a)
                return .success(a, msgs)
            } catch {
                return .failure([error] as! [Failure])
            }
        }
    }
}

extension ROPResult where Failure == Swift.Error {
    init(catching body: () throws -> (Success, Message)) {
        do {
            let result = try body()
            self = .success(result.0, [result.1])
        } catch {
            self = .failure([error])
        }
    }
}

struct DB {
    func updateDb(_ request: Request) -> ROPResult<Request, UseCaseError, UseCaseMessage> {
        return ROPResult<Request, UseCaseError, UseCaseMessage>
            .tee(self.updateDbVoid, msgs: [UseCaseMessage.UpdateSuccess])(request)
            .mapError(UseCaseError.update)
    }

    func updateDbVoid(_ request: Request) throws -> Void {
        return
    }
}

struct SmtpServer {
    func sendEmail(to request: Request) -> ROPResult<Request, UseCaseError, UseCaseMessage> {
        do {
            try sendEmail(request.email)
            return .success(request, [UseCaseMessage.SendMailSuccess])
        } catch {
            return .failure([UseCaseError.sendMail(error)])
        }
    }

    func sendEmail(_ email: String) throws -> Void {
        return
    }
}

func validateRequest(_ request: Request) -> ROPResult<Request, UseCaseError, UseCaseMessage> {
    if request.name.isEmpty {
        let error = ValidationError(field: "name", value: request.name, reason: "name should not be empty")
        return .failure([.validation(error)])
    }
    return .success(request, [])
}

func canonicalizeEmail(_ request: Request) -> Request {
    let canonicalized = request.email.trimmingCharacters(in: .whitespaces).lowercased()
    return Request(userId: request.userId, name: request.name, email: canonicalized)
}

func executeUserCase(request: Request, db: DB, stmpServer: SmtpServer) -> String {

    switch validateRequest(request)
        .map(canonicalizeEmail)
        .flatMap(db.updateDb)
        .flatMap(stmpServer.sendEmail(to:)) {
    case .success(let x, let messages):        
        print("\(x)")
        print("Messages are \(messages)")
        return "OK"
    case .failure(let errors):
        return errors.reduce("", { total, error in
            return total + error.localizedDescription
        })
    }
}

非同期処理

講演内では具体的に扱っていませんでしたが

鉄道指向プログラミングは
全ての処理をシーケンシャルに扱うという訳ではなく
インプットとアウトプットをどうやって繋げていくのかということを示しています。

なのでレールの途中は並列に処理を行うこともあります。

F#ではasyncのような
非同期を同期的に扱う仕組みがあるので
それを活用することで変わらず形でコードを書くことができます。

より複雑な処理の場合は
メッセージを送ることで他のワークフローに任せてしまうなどを挙げていました。

SwiftでもCombineフレームワークの中で
Futureが定義されています。
https://developer.apple.com/documentation/combine/publishers/future

またFutureの中で
非同期処理のコールバックでPromiseという型を受け取っていますが
これは(Result<Output, Failure>) -> Voidtypealiasです。
https://developer.apple.com/documentation/combine/publishers/future/promise

※2019/6/22現在のベータ版の情報です。

他にも非同期を同期的に扱うライブラリが活用できます。

ライブラリの参考例
https://github.com/malcommac/Hydra

また将来的には標準として採用される予定のasync/awaitなどが活用できる可能性があります。

これらの他にもログや処理失敗時DBのロールバックなどについても少し言及されていたので
ご興味のある方はスライドや動画をご参照ください。

補足: EitherやMonadとの違い

Scottさんもサイトで言及されていましたが
鉄道指向プログラミングは
下記のような理由でHaskellの用語を用いていないと言っています。

より具体的な形で多くの人に理解して欲しい

これは特定のエラーハンドリングの問題を解決するためのものであり
モナドを知らない人にも
まずはより目に見える具体的な形で見てもらいたかった。

具体的なものから抽象概念を理解する方が理解の進むが早いと強く思っている。

正確にモナド則に従っているわけではない

flatMapはmonadに必ずしも従っているわけではなく
モナドの方がもっと複雑。

Eitherは抽象的すぎる

道具ではなくレシピを提示したかった。

パンを作るためのレシピが欲しいのに
「小麦粉とオーブンを使え」とだけ言うのが役に立たないのと同様に
エラーハンドリングのためのレシピが欲しいのに
Eitherbind(flatMap)を使え」
とだけ言うのは役に立たない。

なので具体的なカスタムオペレーターや
mapteeなどの数多くの状況に使えるけれども
書き方は一つに限定されるようなテンプレートを提供したかった。

こうすることで後々誰がメンテナンスしても全体像が理解しやすくなって楽になる。

最後に

鉄道指向プログラミングの概要と
Resultの動きを見てみました。

鉄道指向プログラミングでは
型を合わせていくことに焦点を当てており
型を通して処理を考えることの大切さや効果といったことを学べました。

またF#というあまり触れる機会がない言語に触れ
普段とは違う考え方やコードの書き方を知り
すごい勉強になりました。

今回は出てきませんが
鉄道指向プログラミングは
ドメイン駆動設計などの話にも繋がっており
Scottさんもドメインモデルについての本や講演もされています。

https://fsharpforfunandprofit.com/books/
https://www.youtube.com/watch?v=Up7LcbGZFuo

まだまだ私が理解できていない部分も多々あると思いますので
引き続き学んでみたいと思います。

間違いなどございましたらご指摘して頂けますとうれしいです:bow_tone1:

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

Cocoapodsのインストールエラー(xcworkspaceが作成されない)

pod installでPodfileを読み取って、ライブラリを導入しようと思っても、workspaceが作成されないことがある。

UsernamenoMBP:GitHubTest2 username$ pod install
Analyzing dependencies
Downloading dependencies
Installing Bolts (1.9.0)
Installing SVProgressHUD (2.2.5)
...(省略)...
[!] Attempt to read non existent folder `/Users/username/iCloud Drive(アーカイブ)/Documents/programing/ios/Githubテスト用/GitHubTest2/Pods/Bolts`.

自分の環境は、"iCloud Drive(アーカイブ)"というフォルダの中にプロジェクトフォルダを作っていたのが問題だった。
問題は、半角スペースが入っていること。

おそらくターミナルは、iCloud\ Drive(アーカイブ)というように、半角スペースを\でエスケープさせないと認知してくれない。

解決法は、半角スペースを含むフォルダの外に、iOS開発のプロジェクトフォルダを格納する事。
Dropboxに移してから、

$ pod install

を行なったら無事にxcworkspaceが作成されました。

半角スペースなど、ターミナルが苦手な記号はフォルダにつけない方が良いし、つけられていないか注意した方が良い。

〜余談〜
ターミナルではcdコマンドで、現在のディレクトリを変えていく事ができるが、
上と同じように、半角スペースを含むフォルダの中に格納されていると、cdコマンドもうまく働かないことがあるので注意。

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

InterfaceBuilder を使用せずにコード内で UIButton のアクションをバインドする

ある UIButton に対して TouchUpInside のイベントにアクションをバインドしたいとき、インターフェースビルダーを使わずに、コードで実現すると下のようになる。

@IBOutlet weak var button: UIButton!

class ViewController: UIViewController {

  override func viewDidLoad() {
    button.addTarget(self,
                     action: #selector(buttonAction(sender:)),
                     for: .touchUpInside)
  }

  @objc func buttonAction(sender: UIButton) {
    // アクションを書く!
  }
}

Mac の性能がよくなく、インターフェースビルダーの動きがもっさり緩慢な場合は、インターフェースビルダーの使用は最低限アウトレットだけのバインドだけに留めて、アクションなどはコードで設定すると、作業がはかどります。

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

[iOS]UIStackView+ContainerView+UIScrollViewで複雑な画面を設計する

今更なネタですが。

アプリのコンテンツ量が多くなってくると、いろんなコンテンツが混在した画面というものが登場します。最たる例はトップ画面です。

こんな雰囲気。

Untitled Diagram (31).png

単純な場合はどのように組んでも問題は生じないのですが、次の場合頭を抱えることになります。

  • 横スクロールが伴う
  • 一部のコンテンツが、状況によって出たり出なかったりする
  • 出たり出なかったりするのがAPIのレスポンス依存(後から決まる)
  • APIが別れている、複数ある
  • 高さがコンテンツの量に応じて変わる
  • 高さがアニメーションにより増減する
  • タッチイベントが競合する
  • Viewが重い
  • 一番下だけ無限スクロールでページングする
  • スイッチが有り、コンテンツが切り替わる

こういった画面に遭遇した時、UITableView、UICollectionView、UIStackViewのどれかを使うことを検討すると思います。
そのUIStackViewでどう組むかという話です。

対象

UIStackViewがある程度分かる人

UITableVIewとUICollectionViewではダメなのか?

ダメではないと思います。
しかしこれらはハマりポイントが多数存在しがちです。

  • 高さの変更でハマる(特にアニメーション)
  • 非同期でAPI取得後、表示・非表示を制御する時にハマる
  • Cellの場合は表示直前に生成するため、Cell内のViewが多いとスクロールする際にカクつくことがある(例えばCellの中にUICollectionViewが含まれている場合など)
  • Cell内にUICollectionViewがあったりすると、設計に悩むしライフサイクルでも悩む

それでせっかく作ってもあーだこーだ調整に時間が掛かってしまうというのが過去数件ありました。
UIStackViewがこの全てを解決するわけではありませんが、仕様によってはUIStackViewを使うことでスッキリ書けることがあります。

UIStackViewが向かないもの

同じようなViewが連続する場合はもちろんCellを使ったほうがよいでしょう。
また、コンテンツ量が非常に多い場合なども向きません(といってもこれは本当に多いケースです)
ヘビーな画面を作る際は、画面表示時に1回だけ重いか、Cell表示時に毎回重いかどちらかを選択することになると思います。

ContainerViewでどのように高さを変更するか問題

これが本題です。

UIStackViewの中にContainerViewを含める場合、高さの制御が問題になります。
なぜか内部のUIViewContollerで自身のViewの高さをAutoLayoutで指定してもUIStackViewの高さに反映されてくれないのです。
できればUITableViewAutomaticDimensionのように、内部側から高さを決めたいですよね。

それを実現するためには、内部のUIViewControllerでこうします。

override func loadView() {
    super.loadView()
    self.view.translatesAutoresizingMaskIntoConstraints = false
}

これで解決します(理由は察せると思うので割愛。何だそんなことかー、と思いました)

サンプル

https://github.com/osanaikoutarou/EasyStackViewControllers

こういう状態で、ContainerViewをUIStackViewに入れます。
スクリーンショット 2019-06-27 1.50.21.png

画面A
UITextViewのScrollableはオフにしています
スクリーンショット 2019-06-27 1.52.13.png

画面B
UIImageViewの高さを固定しています
スクリーンショット 2019-06-27 1.52.25.png

画面C
Addを押すとViewが追加されていきます
スクリーンショット 2019-06-27 1.53.11.png

実行結果
ezgif-2-fa9c8bcdf1c0.gif

このように、内部の高さに応じてUIStackView側の高さも変わっています。

一番上のViewControllerの中身はこの状態です↓
空っぽ!

スクリーンショット 2019-06-27 2.03.07.png

鬼門:UIScrollView

何度やってもハマるUIScrollViewのAutoLayout
参考にどうぞ

スクリーンショット 2019-06-27 2.06.45.png

UIScrollView -> 画面view 上下左右
ContentView -> UIScrollView 上下左右+EqualWidth
StackView -> ContentView 上下左右
このままだとエラーが出るので
UIView -> StackView 高さ固定
高さ固定を使わないなら、Remove at build timeにチェック

スクリーンショット 2019-06-27 2.09.26.png

このViewを使わないなら、viewDidLoadあたりでStackViewの中身を空に
(ここらへんもっとスマートな方法無いんですかね?AutoLayoutのエラーを放置する手もありますが)

UIStackView in UIStackViewは可能か?

上のサンプルの画面Cで既に使っています
大丈夫です

高さアニメーションしたら、見た目が少し変

こちら
[iOS]UIStackViewのアニメーションが変!

Viewにするか、ViewControllerにするか迷う

こちら
https://stackoverflow.com/questions/2632196/what-is-the-difference-between-a-view-and-a-view-controller

別解

ライブラリーを使うてもあるかも知れません(学習コストが高そうで、まだ使ったことありません)

StackViewController
https://github.com/seedco/StackViewController
AloeStackView
https://github.com/airbnb/AloeStackView

積み残した話題

一番下のコンテンツが無限スクロール(ページング)の場合はどうする??
CellとStackViewのコラボレーションは少しハマりどころがあるのでまた別の機会に

追記

このトピック無いなと思って書いたんですが、ありました。
巨大なビューをStoryboardだけで表現する

ContainerView周りの話題は認識しておいたほうが良いですね。

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