- 投稿日:2019-06-27T23:31:58+09:00
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
がリロードしてくれます。
ソースも減って、元データの監視もできて、この書き方覚えたら元には戻れない。
- 投稿日:2019-06-27T22:40:31+09:00
Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。
はじめに
本記事で分かること
- Firebaseで新規プロジェクトの作成〜アプリにFirebaseを組み込むまで
- Firebase Realtime Databaseを用いた超簡易的なチャットアプリ
本記事で分からないこと
- Firebaseのアカウント作成
- いい感じのUIのアプリ
完成形
#picplaypost https://t.co/ZXHerxwHcP
— tetsukick (@TcarpentersR) 2019年6月27日1. Firebaseで新規プロジェクトの作成
まず、Firebase へアクセスして、コンソールへ移動。
下記の赤枠箇所より、新規プロジェクトを追加。
下記のように、プロジェクト名、アナリティクスの地域を選択してプロジェクトを作成。
これで作成完了。2. アプリにFirebaseを導入
CocoapodsでFirebaseを導入します。
詳細は、公式ドキュメントを参照してください。コマンド実行後は、拡張子が
.xcworkspace
となっている方からプロジェクトを開くようにしてください。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
2. Firebaseコンソール画面でiOSアプリの作成
ここからはFirebase公式の手順に従ってください。
先程作成したプロジェクトのページから、以下の赤枠をクリック
後はFirebaseの画面上のステップ通りに進めてください。
こちらについては、
今回導入するのは、Firebase/core と Firebase/Database になります。以下コマンドでpodfileの作成。
shellpod initpodfileの内容は下記になります。プロジェクト名は各々変更してください。
podfilesource 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target 'firebase-chatting' do pod 'Firebase/Core' pod 'Firebase/Database' endそしたら、以下のコマンドを実行しましょう。
shellpod install
はい!これでFirebaseの導入は完了です!!
ここまでだいたい10秒ですかね、、www笑3. RealTimeDatabaseのルールを変更
{ "rules": { ".read": true, ".write": true } }ルールを上記のように設定して、読み込み権限、書き込み権限を与えます。
設定する場所はこちら↓↓↓
4. UI画面の作成
Xcodeの右上の赤枠の箇所をクリックして、UI部品の選択画面を表示する。
UITextViewにConstraints(制約)を付与
今回はSafeAreaに対して、余白15の制約をつけます。
UIViewの制約をSafeAreaに対して、左0,右0,下0で付与する
UIViewの中に入力用のUITextFieldを配置(2つ)
5.配置したUI部品とコードの紐づけ
左側がstoryboard。右側が
ViewControlle.swift
が開かれている状態にします。
左のstoryBoardから、controlを押しながらドラッグ&ドロップします。
名前は適当につけてください。
UITextViewとUITextFieldを紐づけてください。
最後に、画面下部のView.bottomとSafeArea.bottomにつけられている制約も同じくドラッグ&ドロップで紐づけます。
6. データの読み込み処理
初期設定
Database.database().reference()
でインスタンスを取得します。ViewController.swiftvar databaseRef: DatabaseReference! override func viewDidLoad() { super.viewDidLoad() databaseRef = Database.database().reference()データの読み込み
observe
でイベントの監視を行います。
.childAdded
を指定することで、子要素が追加されたときにwithで与えた処理が実行されます。
データに追加があると自動で呼ばれるので、更新処理を実装する必要はありません。ViewController.swiftoverride 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.swiftNotificationCenter.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.swiftimport 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(データ作成、読み込み、更新、削除)をやってみる
- 投稿日:2019-06-27T22:40:31+09:00
【Swift】Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。
はじめに
本記事で分かること
- Firebaseで新規プロジェクトの作成〜アプリにFirebaseを組み込むまで
- Firebase Realtime Databaseを用いた超簡易的なチャットアプリ
本記事で分からないこと
- Firebaseのアカウント作成
- いい感じのUIのアプリ
完成形
#picplaypost https://t.co/ZXHerxwHcP
— tetsukick (@TcarpentersR) 2019年6月27日1. Firebaseで新規プロジェクトの作成
まず、Firebase へアクセスして、コンソールへ移動。
下記の赤枠箇所より、新規プロジェクトを追加。
下記のように、プロジェクト名、アナリティクスの地域を選択してプロジェクトを作成。
これで作成完了。2. アプリにFirebaseを導入
CocoapodsでFirebaseを導入します。
詳細は、公式ドキュメントを参照してください。コマンド実行後は、拡張子が
.xcworkspace
となっている方からプロジェクトを開くようにしてください。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
2. Firebaseコンソール画面でiOSアプリの作成
ここからはFirebase公式の手順に従ってください。
先程作成したプロジェクトのページから、以下の赤枠をクリック
後はFirebaseの画面上のステップ通りに進めてください。
こちらについては、
今回導入するのは、Firebase/core と Firebase/Database になります。以下コマンドでpodfileの作成。
shellpod initpodfileの内容は下記になります。プロジェクト名は各々変更してください。
podfilesource 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target 'firebase-chatting' do pod 'Firebase/Core' pod 'Firebase/Database' endそしたら、以下のコマンドを実行しましょう。
shellpod install
はい!これでFirebaseの導入は完了です!!
ここまでだいたい10秒ですかね、、www笑3. RealTimeDatabaseのルールを変更
{ "rules": { ".read": true, ".write": true } }ルールを上記のように設定して、読み込み権限、書き込み権限を与えます。
設定する場所はこちら↓↓↓
4. UI画面の作成
Xcodeの右上の赤枠の箇所をクリックして、UI部品の選択画面を表示する。
UITextViewにConstraints(制約)を付与
今回はSafeAreaに対して、余白15の制約をつけます。
UIViewの制約をSafeAreaに対して、左0,右0,下0で付与する
UIViewの中に入力用のUITextFieldを配置(2つ)
5.配置したUI部品とコードの紐づけ
左側がstoryboard。右側が
ViewControlle.swift
が開かれている状態にします。
左のstoryBoardから、controlを押しながらドラッグ&ドロップします。
名前は適当につけてください。
UITextViewとUITextFieldを紐づけてください。
最後に、画面下部のView.bottomとSafeArea.bottomにつけられている制約も同じくドラッグ&ドロップで紐づけます。
6. データの読み込み処理
初期設定
Database.database().reference()
でインスタンスを取得します。ViewController.swiftvar databaseRef: DatabaseReference! override func viewDidLoad() { super.viewDidLoad() databaseRef = Database.database().reference()データの読み込み
observe
でイベントの監視を行います。
.childAdded
を指定することで、子要素が追加されたときにwithで与えた処理が実行されます。
データに追加があると自動で呼ばれるので、更新処理を実装する必要はありません。ViewController.swiftoverride 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.swiftNotificationCenter.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.swiftimport 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(データ作成、読み込み、更新、削除)をやってみる
- 投稿日:2019-06-27T20:06:10+09:00
【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)と書けば良い。
- 投稿日:2019-06-27T19:06:55+09:00
[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を表示し、そこで色を選択して前の画面に戻ると
ペンの色が選択した状態にするのが目標です。ご教授いただけると助かります
- 投稿日:2019-06-27T17:13:58+09:00
WKWebViewでwindow.open()をオーバーライドして自画面遷移にした話
経緯
WKWebViewを使用したWebページの表示・遷移をしていた際にアプリが落ちた。
window.open().location.href
で別ウィンドウを表示しているのが悪いらしい。一律で自画面遷移にしてしまえwってことでやってみた。
やったこと
以下のスクリプトを
WKNavigationDelegate
のfunc webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
内で実行。
window.open()
がwindow
を返すようにすることで、window.open().location.href
がwindow.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);") }
- 投稿日:2019-06-27T16:57:16+09:00
こんな書き方もできる三項演算子
- 投稿日:2019-06-27T16:16:24+09:00
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の開発チームと合わせた。
- 投稿日:2019-06-27T13:30:34+09:00
【Swift】鉄道指向プログラミング(Railway Oriented Programming)でResultの使い方を学ぶ
少し前に「これは何だろう?」と思ったことについて調べてみました。
SwiftのResultとは?
Success
とFailure
の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!
今回の例
以下の処理を行います。
- リクエストを受け取る
- バリデーションチェックをする
- リクエストをDBに保存(更新)する
- 認証済みのメールに送信する
- ユーザに結果を返す
命令型プログラミングで書いた例
下記の例を見てみます
命令型プログラミングの実装
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
文が途中で入ってくるため
本来のやりたいことが見えづらくなってしまいます。では
このようなエラーハンドリングを
どうやって関数型プログラミングを使って
綺麗な形で処理できるでしょうか?結果のパターンを考えてみる
上記の例の処理の流れを考えてみます。
Request
を受け取り
処理が成功した場合は次の処理へ
エラーの場合はエラーになった時点で
レスポンスを返しています。次に関数型プログラミングの形で考えてみます。
関数型では処理を上から下へ向かう
データの流れとして捉えます。ここで上記の図のように処理の結果は
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
を使うと処理の流れは下記のようになります。こうすることで各関数が同じレスポンスを返すようになります。
下記に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>ということで型が合いません。
どうやって各処理を連結できるようになるでしょうか?
鉄道の車線をイメージしてみる(鉄道指向プログラミング)
下記の図を見てください。
一つのインプットを与えると一つのアウトプットを出力する関数を
鉄道の車線に例えています。次の図を見てください。
もう一つ車線が出てきました。
左の関数のアウトプットと右の関数のインプットが一致しているため
この二つの車線を繋げることができます。このような場合は
シンプルですぐに理解できると思います。
Result
の場合はアウトプットが2つの可能性があります。
これを表現するためには車線の分岐が必要になります。
Success
車線とFailure
車線ができます。このような分岐を生じる関数を
スイッチ関数
と呼びます。ではスイッチ関数を連結した場合の動きはどうなるでしょうか?
上記の例で説明すると
Validate
関数が
成功した場合 ->Success
車線を通りUpdateDb
を実行する
失敗した場合 ->Failure
車線を通りUpdateDb
は実行せずにFailure
車線を通り続けるとなります。
言い換えると
処理が成功している場合のみ処理を継続し
エラーが発生した場合は以降の処理を行わずに最終的なアウトプットまで進むことになります。
なんとなくイメージはできたでしょうか?
では
問題のResult
の連結方法について車線で考えてみます。上記で示した1つの車線の関数の連結はシンプルでした。
同様にインプットとアウトプットが2車線同士の関数の場合もシンプルです。
インプットとアウトプットが一致すればそのまま繋げることができます。
しかし
Result
を返すような関数は通常の値をインプットとして受け取るため
インプットとアウトプットが一致するため連結できません。ではどうすれば良いのか?
車線の数を合わせれば良い
のです。
1車線インプット、2車線アウトプットの関数から
2車線インプット、2車線アウトプットの関数へ変換する
ことで2車線関数同士の関数を繋ぎ合わせることと同じになります。それを実現するのが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
引数に
Result
のSuccess
型を引数にして
変換して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車線関数です。これも連結して処理できるようにしたいですが
1車線関数のアプトプットと2車線関数のインプットをそのまま繋げることはできません。
同様に2車線関数のインプットと1車線関数のアウトプットをそのまま繋げることはできません。ではどうするか?
これも
flatMap
の時と同じように考えます。つまり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
を使っても連結ができません。※こういう関数はデッドエンド関数と呼ばれているそうです。
ではどうするのか?
インプットで受け取った値を内部で関数を実行した後に
そのままインプットの値を返すようにします。今回はメソッドを一つ追加します。
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/その他の鉄道指向プログラミング
この他にも色々な場合に関しての
鉄道指向プログラミングのアプローチが紹介されています。いくつか挙げたいと思います。
複数のエラーが欲しい場合は?
これまで見てきたケースですと
エラーは一つしか扱うことができません。例えばバリデーションチェックのエラーは
発生した全てのエラーが欲しい場合があるかもしれません。そのような場合
講演の中では詳細には触れていませんが
それぞれの処理をPairとして組み合わせていけば
いくつでも組み合わせることができる
といったことをおっしゃっています。講演で紹介されていたブログの内容はこちら↓
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に伝えるなど)このような場合は
success
の場合にメッセージをリストとして持ち
処理結果とメッセージのリストの組み合わせを伝達するようにします。
Githubを参考にSwiftでも実装してみました(結構無理あり
)
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を分けてみた例(これも結構無理あり
)
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>) -> Void
のtypealias
です。
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は抽象的すぎる
道具ではなくレシピを提示したかった。
パンを作るためのレシピが欲しいのに
「小麦粉とオーブンを使え」とだけ言うのが役に立たないのと同様に
エラーハンドリングのためのレシピが欲しいのに
「Either
とbind(flatMap)
を使え」
とだけ言うのは役に立たない。なので具体的なカスタムオペレーターや
map
、tee
などの数多くの状況に使えるけれども
書き方は一つに限定されるようなテンプレートを提供したかった。こうすることで後々誰がメンテナンスしても全体像が理解しやすくなって楽になる。
最後に
鉄道指向プログラミングの概要と
Result
の動きを見てみました。鉄道指向プログラミングでは
型を合わせていくことに焦点を当てており
型を通して処理を考えることの大切さや効果といったことを学べました。またF#というあまり触れる機会がない言語に触れ
普段とは違う考え方やコードの書き方を知り
すごい勉強になりました。今回は出てきませんが
鉄道指向プログラミングは
ドメイン駆動設計などの話にも繋がっており
Scottさんもドメインモデルについての本や講演もされています。https://fsharpforfunandprofit.com/books/
https://www.youtube.com/watch?v=Up7LcbGZFuoまだまだ私が理解できていない部分も多々あると思いますので
引き続き学んでみたいと思います。間違いなどございましたらご指摘して頂けますとうれしいです
- 投稿日:2019-06-27T06:59:12+09:00
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コマンドもうまく働かないことがあるので注意。
- 投稿日:2019-06-27T05:32:15+09:00
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 の性能がよくなく、インターフェースビルダーの動きがもっさり緩慢な場合は、インターフェースビルダーの使用は最低限アウトレットだけのバインドだけに留めて、アクションなどはコードで設定すると、作業がはかどります。
- 投稿日:2019-06-27T02:17:17+09:00
[iOS]UIStackView+ContainerView+UIScrollViewで複雑な画面を設計する
今更なネタですが。
アプリのコンテンツ量が多くなってくると、いろんなコンテンツが混在した画面というものが登場します。最たる例はトップ画面です。
こんな雰囲気。
単純な場合はどのように組んでも問題は生じないのですが、次の場合頭を抱えることになります。
- 横スクロールが伴う
- 一部のコンテンツが、状況によって出たり出なかったりする
- 出たり出なかったりするのが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に入れます。
画面A
UITextViewのScrollableはオフにしています
このように、内部の高さに応じてUIStackView側の高さも変わっています。
一番上のViewControllerの中身はこの状態です↓
空っぽ!鬼門:UIScrollView
何度やってもハマるUIScrollViewのAutoLayout
参考にどうぞUIScrollView -> 画面view 上下左右
ContentView -> UIScrollView 上下左右+EqualWidth
StackView -> ContentView 上下左右
このままだとエラーが出るので
UIView -> StackView 高さ固定
高さ固定を使わないなら、Remove at build timeにチェックこのViewを使わないなら、viewDidLoadあたりでStackViewの中身を空に
(ここらへんもっとスマートな方法無いんですかね?AutoLayoutのエラーを放置する手もありますが)UIStackView in UIStackViewは可能か?
上のサンプルの画面Cで既に使っています
大丈夫です高さアニメーションしたら、見た目が少し変
こちら
[iOS]UIStackViewのアニメーションが変!Viewにするか、ViewControllerにするか迷う
別解
ライブラリーを使うてもあるかも知れません(学習コストが高そうで、まだ使ったことありません)
StackViewController
https://github.com/seedco/StackViewController
AloeStackView
https://github.com/airbnb/AloeStackView積み残した話題
一番下のコンテンツが無限スクロール(ページング)の場合はどうする??
CellとStackViewのコラボレーションは少しハマりどころがあるのでまた別の機会に追記
このトピック無いなと思って書いたんですが、ありました。
巨大なビューをStoryboardだけで表現するContainerView周りの話題は認識しておいたほうが良いですね。