- 投稿日:2020-11-19T23:32:31+09:00
FloatingPanelとmapのピンの連携を実装する方法
FLoatingPanelとピンの連携の目的
- ピンを選択するとそのデータに関する情報をFloatingPanelで表示する
- 選択中のピンのサイズを大きくする(選択中のピンを明確にするため)
手順
- GoogleMapsのAPIでアプリ上にGoogleMapsを表示
- GoogleMapsSDKで地図上にピンを表示
FLoatingPanelの実装
ここまでが準備段階です。
ここまでは僕の使った記事の推薦で進めてきます。ピンが押された時にFLoatingPanelを表示させる
FloatingPanelとの関係でピンのサイズを変化させる
1. GoogleMapsのAPIでアプリ上にGoogleMapsを表示
この公式の記事に従ってAPIを取得し、自分のソースコードの中に書いていきましょう
2. GoogleMapsSDKで地図上にピンを表示
https://developers.google.com/maps/documentation/ios-sdk/marker
こちらを参考にしましょう。これはGoogleMapsSDKのmarkerのところです。3. FLoatingPanelの実装
FloatingPanelは記事がとても少ないです。
こちらを参考にさせてもらいました。
4. ピンが押された時にFLoatingPanelを表示させる
ここでのアクションプラン
- GoogleMapsSDKのプロパティであるdidtap markerを書く
- didtap markerのなかに 3. で書いたFLoatingPanelを実装する
のメソッドを書いていきます。
5. FloatingPanelとの関係でピンのサイズを変化させる
ここでのアクションプランは
- FloatingPanelの表示を一つにする(一つにしなかったらFloatingPanelが重なるから)
- 選択中のピンだけを大きくする
- FloatingPanelが下スワイプで消されて時、選択されていたピンの大きさを元に戻す
FloatingPanelの表示を一つにする
これからソースコード共有して説明していきます。qiita.swiftif fpc != nil { fpc.removePanelFromParent(animated: true) fpc = nil }これをFloatingPanel生成の前に書きます。
FloatingPanel生成の前に書くことによって、一つのFloatingPanelだけが表示されるようになります。
- 選択中のピンだけを大きくする
これについては
- 選択したピンをselected_markerというように変数に代入する
- selected_markerにサイズを指定する 僕が書いたコードをです。
があります。
qiita.swiftif ( selected_marker != nil) { selected_marker.icon = self.imageWithImage(image: UIImage(named: "pin")!, scaledToSize: CGSize(width: 32.0, height: 37.0)) selected_marker = marker } else { selected_marker = marker } marker.icon = self.imageWithImage(image: UIImage(named: "pin")!, scaledToSize: CGSize(width: 42.0, height: 47.0))ここで出てくるimageWithImageでメソッドでサイズ指定をしているのでそのメソッドのコードも載せます。
qiita.swiftfunc imageWithImage(image: UIImage, scaledToSize newSize:CGSize) -> UIImage { UIGraphicsBeginImageContextWithOptions(newSize, false, 0) image.draw(in: CGRect(x: 0, y:0, width: newSize.width, height: newSize.height)) let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return newImage }このメソッドのおかげでピンのサイズ指定ができるようになります。
参考にした記事はこれです。
- FloatingPanelが下スワイプで消されて時、選択されていたピンの大きさを元に戻す これは記事が一つもなくてとても苦労しました。
qiita.swiftfunc floatingPanelWillRemove(_ fpc: FloatingPanelController) { selected_marker.icon = self.imageWithImage(image: UIImage(named: "pin")!, scaledToSize: CGSize(width: 32.0, height: 37.0)) }初めはfloatingpPanelDidRemoveメソッドを書いていたのですがこれでは呼ばれるのがトリガーが起こってすぐではないので挙動がおかしくなりました。
floatingPanelWillRemoveを使うとすぐ呼ばれるようになり、うまくいくようになりました。
最後にどのようになるかを動画を載せます。
FLoatingPanelを下スワイプで消すと選択中のピンも元のサイズに戻ります。以上です。Lostというアプリを作ったのでどうか見てください!
僕のreadmeです!
- 投稿日:2020-11-19T23:26:31+09:00
【第4回】初心者二人で0から麻雀アプリ開発
改版履歴
2020年11月19日投稿
第4回会議(2020年11月13日)
議題
・画面デザイン詰める
・実装してみたい機能
・来週からの活動
・プロジェクト全体の大まかな期間画面デザイン
画面のデザインはgoogledrive内のdraw.ioを使用して作りました。この時、ホーム画面から成績入力画面に移動する際に、画面全体で遷移させるのか、画面を部分的に遷移させるのか悩みました。部分的に遷移するのが今の流行ということなので、部分遷移でいこう!ということになりました。また、ポップアップで入力画面を立ち上げるという案も出ました。
実装してみたい機能
今回の会議で、お互いにやってみたいことが結構出てきました。
・テーマ
最近のアプリはダークテーマとか、ホワイトテーマとか自分の好みで切り替えられるようにしたい。デフォルトであるらしい…❓・githubの芝みたいなもの
打数に応じてカレンダーに色づけするみたいなことができればモチベーションの維持につながるのではないかと思い実装してみたいと思いました。来週からの活動
今回から、各々テーマを決めて、次の会議までに実装の方法を調べてくるという方法をとります。
今週のテーマはそれぞれ
・テーブル表示の実装について
・ヘッダーとフッターの実装について
・スコア表機能の実装
ということになりました。プロジェクトの展望と今後の予定について
要件定義と基本設計 1ヶ月
画面遷移図の作成 2ヶ月くらい
詳細設計 1ヶ月 (勉強会)、クラス図作成、処理の順番、変数名など
実装1ヶ月 (ソースコードを書く)
テスト1ヶ月半
アプリをリリース
という大雑把な計画を立てました。
できるだけ多くの機能を実装してみたいと思っていますが、最低限挫折しないということを目標に頑張っていきます。終わりに
雑記みたいな感じになってしまいました。速くqiitaっぽくcode載せたいです。(切実)
- 投稿日:2020-11-19T19:17:01+09:00
キーボードが表示されたときに UITextField を上にスクロールさせる方法
はじめに
よくある入力フォームで、テキストフィールドをタップしたときには、キーボードに隠れないように、自動的にテキストフィールドが上にスクロールしてほしいですよね。
コーディングしなくても勝手にキーボードを回避してくれる機能はSwiftにはありませんので、自分で実装する必要があります。
世の中いろいろサンプルが出回っていますが、自分なりにようやく書けるようになったので、ここにまとめます。当記事のコードの特徴
- テキストフィールドにタッチすると、
- キーボードに隠れない場合は、何も起きません。
- キーボードに隠れる場合は、テキストフィールドがキーボードの上辺に合わせてスクロールします。
- ユーザーが入力を完了すると、キーボードが下がるのに合わせて、テキストフィールドが元の位置にスクロールします。
- 対象のテキストフィールドがアクティブなときに、キーボードツールバーに上/下ボタンや完了ボタンを設定します。
実装イメージ
コードの説明
ここで説明するコードは、以下のリポジトリで公開しています。
https://github.com/mnaruse/KeyboardUpDownSampleStoryboard での注意点
UITextFieldを、UIScrollViewの中に設置しておく必要があります。
IBOutlet での注意点
大抵の場合、複数のUITextFieldが置いてあると思いますので、[UITextField] というUITextFieldsの配列を作っておきます。
@IBOutlet private var textFields: [UITextField]!ViewControllerのファイルと、Storyboardのファイルを並べて開き、このコード上のIBOutletから、Storyboard内の対象のUITextFieldに向かって、接続していきます。
最終的にこんな感じになります。キーボードのサイズが変化すると実行されるイベントハンドラー のコードのポイント
キーボードのサイズが変化すると実行される関数について、ポイントを説明します。
/// キーボードのサイズが変化すると実行されるイベントハンドラー /// テキストフィールドが隠れたならスクロールする。 /// /// キーボードの退場でも同じイベントが発生するので、編集中のテキストフィールドがnilの時は処理を中断する。 @objc private func keyboardChangeFrame(_ notification: Notification) { // 編集中のテキストフィールドがnilの時は処理を中断する。 guard let textField = editingTextField else { return } // キーボードのframeを調べる。 let userInfo = notification.userInfo let keyboardFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue以下のように、編集中のUITextFieldのframeを、キーボードのframeと同じ座標系にするところがポイントです。
これによって、重なり具合の比較が容易になります。// テキストフィールドのframeをキーボードと同じウィンドウの座標系にする。 guard let textFieldFrame = view.window?.convert(textField.frame, from: textField.superview) else { return }次に、以下のように、編集中のテキストフィールド足す余白分(以下の例では8ptに設定しています)が、キーボードと重なっていないか調べて、それをスクロールするかしないかの判断基準にするところもポイントです。
- 重なっていなかったら、何もせず処理を終了します。
- 重なっていたら、テキストフィールドをキーボードの上辺に合わせて上にスクロールします。
/// テキストフィールドとキーボードの間の余白(自由に変更してください。) let spaceBetweenTextFieldAndKeyboard: CGFloat = 8 // 編集中のテキストフィールドがキーボードと重なっていないか調べる。 // 重なり = (テキストフィールドの下端 + 余白) - キーボードの上端 var overlap = (textFieldFrame.maxY + spaceBetweenTextFieldAndKeyboard) - keyboardFrame.minY if overlap > 0 { // 重なっている場合、キーボードが隠れている分だけスクロールする。 overlap = overlap + scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: 0, y: overlap), animated: true) } }コード全体
コードの全体は以下の通りです。
なお、コード内で実装するかしないか任意な点は以下の通りです。
- 対象のテキストフィールドがアクティブなとき、キーボードのツールバーに、上下ボタンや完了ボタンを設定するかどうかは任意です。
- この部分→
addPreviousNextableDoneButtonOnKeyboard(textFields: textFields, previousNextable: true)
- UIViewControllerのExtensionを作っていますので、よかったら、以下の記事でご覧ください。
- UIToolBarに上下ボタンや完了ボタンをつけるExtension
- ビューがタップされた時に、キーボードを下げるかどうかは任意です。
コード全体
// // ViewController.swift // KeyboardUpDownSample // // Created by Miharu Naruse on 2020/11/15. // import UIKit class ViewController: UIViewController { @IBOutlet private var scrollView: UIScrollView! @IBOutlet private var textFields: [UITextField]! /// 編集中のテキストフィールド private var editingTextField: UITextField? /// キーボードが登場する前のスクロール量 private var lastOffsetY: CGFloat = 0.0 override func viewDidLoad() { super.viewDidLoad() // 全てのテキストフィールドのデリゲートになる。 for textField in textFields { textField.delegate = self } // テキストフィールドとキーボード関連の処理について、通知センターの設定をする。 setNotificationCenter() // 任意: 対象のテキストフィールドがアクティブなとき、キーボードのツールバーに、前後ボタンや完了ボタンを設定する。 addPreviousNextableDoneButtonOnKeyboard(textFields: textFields, previousNextable: true) } /// 任意: ビューがタップされた時に実行される処理 @IBAction func tapView(_ sender: UITapGestureRecognizer) { // キーボードを下げる。 view.endEditing(true) } } // MARK: - Extensions テキストフィールドとキーボード関連の処理 extension ViewController { /// テキストフィールドとキーボード関連の処理について、通知センターの設定をする。 private func setNotificationCenter() { /// デフォルトの通知センターを取得 let notification = NotificationCenter.default // キーボードのframeが変化した時のイベントハンドラーを登録する。 notification.addObserver(self, selector: #selector(keyboardChangeFrame(_:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil) // キーボードが登場する時のイベントハンドラーを登録する。 notification.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) // キーボードが退場する時のイベントハンドラーを登録する。 notification.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) } /// キーボードのサイズが変化すると実行されるイベントハンドラー /// テキストフィールドが隠れたならスクロールする。 /// /// キーボードの退場でも同じイベントが発生するので、編集中のテキストフィールドがnilの時は処理を中断する。 @objc private func keyboardChangeFrame(_ notification: Notification) { // 編集中のテキストフィールドがnilの時は処理を中断する。 guard let textField = editingTextField else { return } // キーボードのframeを調べる。 let userInfo = notification.userInfo let keyboardFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue // テキストフィールドのframeをキーボードと同じウィンドウの座標系にする。 guard let textFieldFrame = view.window?.convert(textField.frame, from: textField.superview) else { return } /// テキストフィールドとキーボードの間の余白(自由に変更してください。) let spaceBetweenTextFieldAndKeyboard: CGFloat = 8 // 編集中のテキストフィールドがキーボードと重なっていないか調べる。 // 重なり = (テキストフィールドの下端 + 余白) - キーボードの上端 var overlap = (textFieldFrame.maxY + spaceBetweenTextFieldAndKeyboard) - keyboardFrame.minY if overlap > 0 { // 重なっている場合、キーボードが隠れている分だけスクロールする。 overlap = overlap + scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: 0, y: overlap), animated: true) } } /// キーボードが登場する通知を受けると実行されるイベントハンドラー @objc private func keyboardWillShow(_ notification: Notification) { // キーボードが登場する前のスクロール量を保存しておく。 lastOffsetY = scrollView.contentOffset.y } /// キーボードが退場する通知を受けると実行されるイベントハンドラー @objc private func keyboardWillHide(_ notification: Notification) { // スクロール量をキーボードが登場する前の位置に戻す。 scrollView.setContentOffset(CGPoint(x: 0, y: lastOffsetY), animated: true) } } // MARK: - Extensions UITextFieldDelegate テキストフィールドとキーボード関連の処理 extension ViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { // テキストフィールドの編集が開始された時に実行される処理。 // どのテキストフィールドが編集中か保存しておく。 editingTextField = textField } func textFieldDidEndEditing(_ textField: UITextField) { // テキストフィールドの編集が終了した時に実行される処理。 // 編集中のテキストフィールドをnilにする。 editingTextField = nil } func textFieldShouldReturn(_ textField: UITextField) -> Bool { // 改行キーが入力された時に実行される処理。 // キーボードを下げる。 view.endEditing(true) // 改行コードは入力しない。 return false } }おわりに
読んでいただきありがとうございました。
何か少しでもお役に立てたら嬉しいです
「こういうやり方もあるよ」とか「ここはこうした方がいいよ」とか、コメントやアドバイス、あるいは質問があれば、よかったらコメントお願いします。関連記事
- 投稿日:2020-11-19T18:53:05+09:00
XGBoostをiOSアプリで動作させる
Kaggleなどのコンペで人気のXGBoostをiOSアプリで動作させたので、その方法を紹介します。
XGBoostとは
勾配ブースティング決定木 (GBDT, Gradient Boosting Decision Tree)を実装したライブラリです。
GBDTとは
勾配ブースティング決定木とは、手短に説明すると『決定木を複数組み合わせたアンサンブル学習の一種で、勾配降下法を用いて学習を行うもの』です。
決定木とは、木構造を用いて回帰や分類を行う手法です。アンサンブル学習は、複数の決定木を用いて予想の精度を上げる手法です。
ブースティングというのは、アンサンブル学習の一種で、それぞれの学習器を直列に学習する手法です。それ以外のアンサンブル学習の手法としては学習器を並列に学習させるバギングなどがあります。例えば、ランダムフォレストはバギングの一種です。
勾配降下法とは、ディープラーニングでもよく用いられる手法で、重みを少しずつ更新して勾配が最小になるように学習する手法です。
図で説明します
駆け足過ぎたので、図で説明します。
例えば、決定木は下の図のようにして、その人がゲーム好きかどうかを予想します。
ブースティングを用いた場合は、複数の決定木を用いて予想値を修正していきます。
【ブースティング決定木の例】
最初の木で出したポイントと、2番目の木で出したポイントを足して、最終的なポイントを出しています。ここではポイントまでしか出していませんが、最終的にはこれを確率に変換します。
なぜこのようにすると予想の精度が上がるのか?についてはこちらに解説がありました。
学習は最初の木を学習させたあと、その予想結果との差に対して新たな木を作って学習させます。このように順番に学習していくので、「直列に学習する」と表現してます。
また、予想結果の差に対して新たに学習していくので、勾配に従って学習していくことになります。これが勾配降下法に相当します。
学習についてはこちらのブログがわかりやすかったです。
XGBoostを使ってみる
Google Colabを使ってXGBoostを試してみます。ほぼこちらのブログの内容の写経になっています。
Python: XGBoost を使ってみる - 乳がんデータセットを分類してみる
乳がんデータセットを使って、特徴量から乳がんであるかを予想する二値分類問題です。
Notebookimport xgboost as xgb from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score from matplotlib import pyplot as plt dataset = datasets.load_breast_cancer() X, y = dataset.data, dataset.target X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, shuffle=True, random_state=42, stratify=y) clf = xgb.XGBClassifier(objective='binary:logistic', n_estimators=1000) evals_result = {} clf.fit(X_train, y_train, eval_metric='logloss', eval_set=[ (X_train, y_train), (X_test, y_test), ], early_stopping_rounds=10, callbacks=[ xgb.callback.record_evaluation(evals_result) ], ) y_pred = clf.predict(X_test) acc = accuracy_score(y_test, y_pred) print('Accuracy:', acc) # 出力結果 # Accuracy: 0.9649122807017544Core MLに変換する
モデルができたので、今度はCore MLに変換します。
Core ML ToolsはXGBoostの変換に対応しているので、簡単に変換することができます。Notebookpip install -U coremltools==4.0 import coremltools as ct coreml_model = ct.converters.xgboost.convert(clf , mode='classifier') coreml_model.save('xgboost_test.mlmodel')iOSアプリで動作させる
iOSアプリで動作させます。Core MLを簡単に試せるリポジトリを作ったのでこれを使います。
このリポジトリをCloneして、変換したモデルをドラッグ & ドロップします。
モデルの情報はこんな感じです。
早速予想してみましょう。
モデルに入力する情報(特徴量)が30もあるのでちょっと大変ですが、Google Colabでダウンロードしたデータセットから取得して、xgboost_testInputの初期化パラメータに与えます。ViewController.swift@IBAction func buttonSimpleMLTapped(_ sender: Any) { let model = xgboost_test() let inputToModel: xgboost_testInput = xgboost_testInput( f0: 1.300e+01, f1: 2.078e+01, f2: 8.351e+01, f3: 5.194e+02, f4: 1.135e-01, f5: 7.589e-02, f6: 3.136e-02, f7: 2.645e-02, f8: 2.540e-01, f9: 6.087e-02, f10: 4.202e-01, f11: 1.322e+00, f12: 2.873e+00, f13: 3.478e+01, f14: 7.017e-03, f15: 1.142e-02, f16: 1.949e-02, f17: 1.153e-02, f18: 2.951e-02, f19: 1.533e-03, f20: 1.416e+01, f21: 2.411e+01, f22: 9.082e+01, f23: 6.167e+02, f24: 1.297e-01, f25: 1.105e-01, f26: 8.112e-02, f27: 6.296e-02, f28: 3.196e-01, f29: 6.435e-02 ) if let prediction = try? model.prediction(input: inputToModel) { print(prediction.target) print(prediction.classProbability) } }アプリを実行するとボタンが複数出てくるので一番上のボタンを押します。
ボタンを押すと、Xcodeのコンソールに予想結果が出力されます。
コンソール出力1 [0: 0.019117622730040473, 1: 0.9808823772699595]うまく分類できているようです。
最後に
2020年11月現在、XGBoostをiOSで動かしたい人はほとんどいないのか、ネット上にはほとんど情報がありません。
また、Core ML Toolsのドキュメントもあまり親切ではないので、もしXGBoostをiOSで動かしたいという方の参考になりましたら幸いです。
NoteではiOS開発、とくにCoreML、ARKit、Metalなどについて定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshidaTwitterでも発信しています。
https://twitter.com/jugemjugemjugem参考資料
勾配ブースティング決定木についてはこちらのブログの説明が詳しかったです。
ブースティングについては上のブログにはあまり説明がなく、こちらの資料がわかりやすかったです。
GBDTと勾配降下法の関係については、こちらのブログが参考になりました。
- 投稿日:2020-11-19T17:44:06+09:00
[Swift][MVVM]関数型ViewModelのススメ
概要
本稿ではアプリアーキテクチャの1形式であるMVVMの、特に
ViewController
とViewModel
の関連を記述するスタイルとして関数型ViewModel
を紹介します。参考リンク: Modeling Your View Models as Functions
前提知識
本稿を読む前提として、以下の知識があると理解しやすいです。
- iOSアプリ開発
- Swift 5
- RxSwiftMVVMを使ったアプリの実装例
いくつかのスタイルで簡単な機能のアプリを実装してみます。
仕様
まず、以下のようなアプリを考えてみましょう
- 画面の中央にカウンターを表示する
- +ボタンを押すとカウンターの数値が1増える
- -ボタンを押すとカウンターの数値が1減る
- 数値が偶数ならカウンターの文字色が黒に、奇数なら赤になる。
画面レイアウト
まずレイアウトを実装します。この時点ではボタンを押しても何も反応しません。
import UIKit final class ViewController: UIViewController { private var countLabel: UILabel! // 合計カウント表示ラベル private var plusButton: UIButton! // +ボタン private var minusButton: UIButton! // -ボタン override func loadView() { super.loadView() view.backgroundColor = .white countLabel = UILabel() view.addSubview(countLabel) countLabel.translatesAutoresizingMaskIntoConstraints = false countLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true countLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true plusButton = UIButton(type: .system) plusButton.setTitle("+", for: .normal) view.addSubview(plusButton) plusButton.translatesAutoresizingMaskIntoConstraints = false plusButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true plusButton.bottomAnchor.constraint(equalTo: countLabel.topAnchor, constant: -10).isActive = true minusButton = UIButton(type: .system) minusButton.setTitle("-", for: .normal) view.addSubview(minusButton) minusButton.translatesAutoresizingMaskIntoConstraints = false minusButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true minusButton.topAnchor.constraint(equalTo: countLabel.bottomAnchor, constant: 10).isActive = true }ViewControllerによる実装例
このアプリを、愚直に
ViewController
だけで実装してみましょう。コードは以下のとおりです。import UIKit final class ViewController: UIViewController { // レイアウトは省略 private var count: Int = 0 override func viewDidLoad() { super.viewDidLoad() plusButton.addTarget(self, action: #selector(plusButtonDidTap), for: .touchUpInside) minusButton.addTarget(self, action: #selector(minusButtonDidTap), for: .touchUpInside) updateCountLabel() } @objc func plusButtonDidTap() { count += 1 updateCountLabel() } @objc func minusButtonDidTap() { count -= 1 updateCountLabel() } private func updateCountLabel() { countLabel.text = "\(count)" countLabel.textColor = count %2 ==0 ? UIColor.black : UIColor.red } }MVVM(Kickstarterスタイル)による実装例
https://github.com/kickstarter/ios-oss (Kickstarter社のリポジトリ)
https://qiita.com/muukii/items/045b12405f7acff1a9fd (KickstarterスタイルのMVVMについての解説記事)こちらのリンクを参考に、同じ機能を実装してみましょう。
ViewModel
は以下のように実装します。import RxSwift import RxCocoa protocol ViewModelInputs { func plusButtonDidTap() func minusButtonDidTap() } protocol ViewModelOutputs { var count: Driver<Int> { get } var isEven: Driver<Bool> { get } } protocol ViewModelType { var inputs: ViewModelInputs { get } var outputs: ViewModelOutputs { get } } struct ViewModel: ViewModelType, ViewModelInputs, ViewModelOutputs { var inputs: ViewModelInputs { return self } var outputs: ViewModelOutputs { return self } let count: Driver<Int> let isEven: Driver<Bool> private let plusButtonDidTapProperty = PublishSubject<Void>() private let minusButtonDidTapProperty = PublishSubject<Void>() init() { count = Observable.merge(plusButtonDidTapProperty.map { _ in 1 }, // +ボタンタップは1に変換する minusButtonDidTapProperty.map { _ in -1 }) // -ボタンタップは-1に変換する .scan(0, accumulator: +) // 0から開始して今までの合計値を出力する .startWith(0) // 初期値は0 .asDriver(onErrorDriveWith: .never()) isEven = count .map { $0 % 2 == 0 } } func plusButtonDidTap() { plusButtonDidTapProperty.onNext(()) } func minusButtonDidTap() { minusButtonDidTapProperty.onNext(()) } }
ViewController
は以下のように実装します。import RxSwift import RxCocoa final class ViewController: UIViewController { // レイアウトは省略 private let viewModel: ViewModelType private let disposeBag = DisposeBag() init(viewModel: ViewModelType = ViewModel()) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() plusButton.rx.tap .subscribe(onNext: { [viewModel] in viewModel.inputs.plusButtonDidTap() }) .disposed(by: disposeBag) minusButton.rx.tap .subscribe(onNext: { [viewModel] in viewModel.inputs.minusButtonDidTap() }) .disposed(by: disposeBag) viewModel.outputs .count .map { "\($0)" } .drive(countLabel.rx.text) .disposed(by: disposeBag) viewModel.outputs .isEven .map { $0 ? UIColor.black : UIColor.red } .drive(onNext: { [countLabel] in countLabel?.textColor = $0 }) .disposed(by: disposeBag) } }国内の資料でiOS・Swift環境でMVVMというと、だいたいはこのようなKickstarterスタイルのMVVMが多い印象を受けます。
RxSwiftを使用するテンプレートとしては優れていますが、以下のような問題点があります。
Inputs
,Outputs
と宣言するプロトコルがやや多い。Inputs
とProperty
との対応関係の記述が冗長。- 使用されない
Outputs
変数があってもコンパイラに警告されない。MVVM(関数型スタイル)による実装例
https://medium.com/grailed-engineering/modeling-your-view-models-as-functions-65b58525717f
こちらの記事を参考に、関数型スタイルのMVVMを実装してみましょう。
ViewModel
は以下のように実装します。リンク先の記事とは違いSwift5.2で追加されたcallAsFunction
を使っています。import RxSwift import RxCocoa protocol ViewModelType { func callAsFunction( plusButtonDidTap: Observable<Void>, minusButtonDidTap: Observable<Void> ) -> ( count: Driver<Int>, isEven: Driver<Bool> ) } struct ViewModel: ViewModelType { func callAsFunction( plusButtonDidTap: Observable<Void>, minusButtonDidTap: Observable<Void> ) -> ( count: Driver<Int>, isEven: Driver<Bool> ) { let count = Observable.merge(plusButtonDidTapProperty.map { _ in 1 }, // +ボタンタップは1に変換する minusButtonDidTapProperty.map { _ in -1 } ) // -ボタンタップは-1に変換する .scan(0, accumulator: +) // 0から開始して今までの合計値を出力する .startWith(0) // 初期値は0 .asDriver(onErrorDriveWith: .never()) let isEven = count .map { $0 % 2 == 0 } return ( count, isEven ) } }
ViewController
は以下のように実装します。import RxSwift import RxCocoa final class ViewController: UIViewController { private let viewModel: ViewModelType private let disposeBag = DisposeBag() init(viewModel: ViewModelType = ViewModel()) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() let ( count, isEven ) = viewModel( plusButtonDidTap: plusButton.rx.tap.asObservable(), minusButtonDidTap: minusButton.rx.tap.asObservable() ) count .map { "\($0)" } .drive(countLabel.rx.text) .disposed(by: disposeBag) isEven .map { $0 ? UIColor.black : UIColor.red } .drive(onNext: { [countLabel] in countLabel?.textColor = $0 }) .disposed(by: disposeBag) } }これまでのスタイルと違いViewModelの記述がシンプルになっていますね。
関数型MVVMを使うメリット・デメリット
主にKickStarterスタイルと比較した場合のメリット・デメリットが以下になります。
メリット
- 宣言するプロトコル、メソッドが一つで済む。その他全体的な記述がシンプルになっている。
- 一つの関数内に
Observable
のイベント遷移が集約されるため、実装の検証が容易。ViewModel
の戻り値に使用されていない変数があった場合、コンパイラが警告してくれる。デメリット
- KickStarterスタイルと比べてより宣言的な記述を強要するため、開発メンバーがRxSwiftに精通していることが必要とされる。
- 一つの関数内の記述量が多いため、適切に処理を切り出さないと却って見通しが悪くなる。
- 投稿日:2020-11-19T12:20:29+09:00
[swift]選択肢をもつダイアログの後に処理に応じて再びダイアログを表示して画面遷移したい
タイトル長くなりがち。
やりたい事
アカウント設定画面でログアウトボタンを押した時に、
「ログアウトしますか?」というダイアログに「OK」/「キャンセル」の選択肢を用意。
「OK」を押すと、APIでログアウト処理が行われる。
→成功すると、「ログアウトしました」というダイアログが表示され、画面が遷移する。
→失敗すると、「ログアウトに失敗しました」というアラートが表示される。「キャンセル」を押すと、最初のダイアログが消える。
っていう処理を行いたい
とりあえず作ってみた
今回は、
- ちゃんと読んでほしいアラートは手動で閉じる。
- お知らせ程度のダイアログは勝手に消える。
みたいな感じでメソッドを使い分けています。
AccountViewController.swiftimport UIKit final class AccountViewController: UIViewController { @IBAction private func tappedLogoutButton(_ sender: UIButton) { let dialog: UIAlertController = UIAlertController(title: "ログアウト", message: "ログアウトしますか?", preferredStyle: .alert) dialog.addAction(UIAlertAction(title: "OK", style: .default) { _ in // 本来はAPI通信の結果をresultに入れています。(今回は割愛) let result:Bool = true switch result { case true: AlertUtil.showAlert(title: "Success", message: "ログアウトに成功しました。",viewController: self) // 以下に画面遷移処理を追加。(今回は割愛) case false: AlertUtil.showAlert(title: "ログアウト失敗", message: "ログアウトに失敗しました。", viewController: self) } dialog.addAction(UIAlertAction(title: "閉じる", style: .cancel)) self.present(dialog, animated: true) }) } }AlertUtil.swiftstruct AlertUtil { static func showDialog(title: String, message: String, viewController: UIViewController) { let dialog = UIAlertController(title: title, message: message, preferredStyle: .alert) viewController.present(dialog, animated: true) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { dialog.dismiss(animated: true, completion: nil) } } } static func showAlert(title: String, message: String, viewController: UIViewController) { DispatchQueue.main.async { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "キャンセル", style: .default)) viewController.present(alert, animated: true) } } }上のコードだと、
アラート関連の処理はAlertUtilにまとめちゃお!
でも、API通信の結果によってaddActionの処理を分けたいから・・・
最初のダイアログだけViewControllerで作っちゃお!って感じ。
でも、他の部分ではアラート表示をAlertUtilで行なっているから、
なるべく統一していきたいな・・・
という事で
alertActionを引数にとる
スッキリ統一感のあるコードにするために、alertActionを引数にとる形に変更してみます。
AccountViewController.swiftimport UIKit final class AccountViewController: UIViewController { @IBAction private func tappedLogoutButton(_ sender: UIButton) { let alertAction: UIAlertAction = UIAlertAction(title: "OK", style: .default) { _ in // 本来はAPI通信の結果をresultに入れています。(今回は割愛) let result:Bool = true switch result { case true: AlertUtil.showAlert(title: "Success", message: "ログアウトに成功しました。",viewController: self) // 以下に画面遷移処理を追加。(今回は割愛) case false: AlertUtil.showAlert(title: "ログアウト失敗", message: "ログアウトに失敗しました。", viewController: self) } } AlertUtil.showChoiceDialog(title: "ログアウト", message: "ログアウトしますか?", viewController: self, alertAction: alertAction) } }AlertUtil.swiftstruct AlertUtil { static func showDialog(title: String, message: String, viewController: UIViewController) { let dialog = UIAlertController(title: title, message: message, preferredStyle: .alert) viewController.present(dialog, animated: true) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { dialog.dismiss(animated: true, completion: nil) } } } static func showAlert(title: String, message: String, viewController: UIViewController) { DispatchQueue.main.async { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "キャンセル", style: .default)) viewController.present(alert, animated: true) } } // このメソッドを追加 static func showChoiceDialog(title: String, message: String, viewController: UIViewController, alertAction: UIAlertAction) { let dialog: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert) dialog.addAction(UIAlertAction(title: "キャンセル", style: .default)) dialog.addAction(alertAction) viewController.present(dialog, animated: true) } }こうする事で、
showAlert系のメソッドは全てAleretUtilで作成する事に統一し、
AccountViewController内の記述もスッキリしました
- 投稿日:2020-11-19T08:29:45+09:00
【SwiftUI】RPGにあるような攻撃エフェクトを再現してみたい!
はじめに
こんにちは!@kanato4です。
仕事ではJavaがメインなのですが、プライベートではSwiftを勉強している若輩者です。ただ、Swiftははじめて間もないので基本的な文法しか分かりません。これからもっと知識をつける予定です。
さて、そんなSwift初心者の私ですが、何を思ったのか「思いきってSwiftUIを使って遊んでみよう!」と考えたわけです。
SwiftUIは2019年に発表されたフレームワークなのですが、従来のSwiftの書き方とかなり違ってくるという話だったので、「やっぱこれからiPhoneアプリ作るなら知っておかないとね☆」と挑戦したわけですが、案の定苦労しました。。
今回はそんな私がはじめてSwiftUIで作れた成果物?を折角なら見てもらいたいなぁと思い記事にしました。アウトプット大事!
完成したもの
まずは完成したものを見てやってください。
内容は簡単なもので、ボタンを押すと爆発するアニメーションが表示されます。同時に、真ん中のイラストもボタンを押す前後で変化するようにしました。もういちどボタンを押すとリセットされます。爆発は繰り返される事なく、一回だけ爆発して終了します。(意外とここで躓いた)
正直、Swiftを学び始めて間もないのでこれだけでも結構苦労しました。。
まだまだ課題は山積みといった感じですが、とりあえず形になって良かったなという感想です。実際のコード
実際に実装したコードがこちらになります。
ContentView.swiftimport SwiftUI struct ContentView: View { //ステートプロパティ @State private var trigger = false var body: some View { VStack { Text("爆破スイッチ") .font(.largeTitle) Spacer() ZStack{ //三項演算子(条件式 ? true時の値 : false時の値) Image(self.trigger ? "computer_note_bad" :"computer_man") .resizable() .scaledToFit() .frame(width: 200, height: 200) //triggerがtrueだったら if trigger{ //構造体を呼び出す LoadingView() } } Spacer() Button(action: { //クロージャ内ではプロパティを参照する際には自分自身を指すselfを指定 //toggleメソッドはブール値(Bool型)の値を反転させる self.trigger.toggle() }) { //三項演算子(条件式 ? true時の値 : false時の値) Text(self.trigger ? "もういちど" : "ばくはつ!") } .font(.largeTitle) .foregroundColor(.red) .padding() } } } struct LoadingView: View { //ステートプロパティ @State private var index = 0 //mapで1~16の数字に処理を適用し、その処理を施した配列imagesを作成する private let images = (1...16).map { UIImage(named: "explotion_\($0)")! } //指定された時間の間隔で現在の日時への接続を繰り返す private var timer = Timer.publish(every: 0.05, on: .main, in: .default).autoconnect() var body: some View { return Image(uiImage: images[index]) .resizable() .scaledToFit() .frame(width: 300, height: 300, alignment: .center) //パブリッシャー(timer)によって発行されたデータを検出したときの処理 .onReceive( timer, perform: { _ in //クロージャ内ではプロパティを参照する際には自分自身を指すselfを指定 self.index = self.index + 1 if self.index >= 16 { //timerの自動接続を停止 self.timer.upstream.connect().cancel() self.index = 0 } } ) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }使用した画像はこのような感じです。
コード中のtoggleメソッドでcomputer_man
とcomputer_note_bad
を切り替えています。爆発エフェクトの画像は
explotion_1
からexplotion_16
に分けています。
explotion_1
には、いわゆる透明な画像が入っている状態ですね。@State private var trigger = false
@State
と書かれている部分はステートプロパティ
と呼ばれるものです。
この属性を宣言する事でSwiftUIがプロパティを自動で監視してくれて、その値が変更されると自動でビューを更新してくれる偉いやつです。ステートプロパティへのアクセスはコンテンツビューの内部からのみに限定される事が望ましいため、private
と言うアクセス修飾子を指定しています。また、privateの装飾子を指定する事で外部からアクセスができなくなるため、必ず初期化をする必要があります。アニメーションの再現方法としては、番号が振ってある爆発の画像を変数
images
に格納し、timer
が接続されるたびにindex
の数字が加算されていき、indexの数字に対応した爆発の画像をViewに返すという処理を繰り返して再現しています。また、アニメーションの処理が記述されている構造体のif文の次の1文が大事。
self.timer.upstream.connect().cancel()この1文で接続をキャンセルしないと永遠と爆発してしまいます。
ノートPCが可哀想ですね。。ここで、爆発の画像の最初の1つ(explotion_1)を透明の画像にした理由なのですが、上記の1文でキャンセルするとアニメーションは停止するのですが、最初のアニメーション画像が呼び出された状態でストップしてしまいます。
explotion_1
に爆発の画像を入れると中途半端に爆発して止まってしまいます。。そのため、最初の画像は透明にしてます。単にコードの出来がよくない可能性もありますが、やむなくこのような形にしました。無念。
おわりに
滅茶苦茶読みづらかったでしょうが、ここまでお付き合いいただきありがとうございました。
Swiftは分からないことだらけですが、きちんとアウトプットをやっていって、ゆくゆくは個人開発したものをストアで公開できるぐらいになりたいと思います。
またちょこちょこSwiftUIで遊んでみたいなぁ。
- 投稿日:2020-11-19T01:52:11+09:00
[iOS]音速でモーダルを実装する
できるだけコードを書かずにモーダルを実装します
バージョン
Swift 5.3
Xcode 12.1モーダルとは
画面の下からふわっと出てくるあれです。
Wikipediaによるとコンピュータアプリケーションソフトウェアのユーザインタフェース設計において、何らかのウィンドウの子ウィンドウとして生成されるサブ要素のうち、ユーザーがそれに対して適切に応答しない限り、制御を親ウィンドウに戻さないもの。モーダルウィンドウはGUIシステムで、ユーザーに注意を促したり、選択肢を提示したり、緊急の状態を知らせたりする目的でよく使われる。
「ユーザーが現在見ている画面の上に表示され、ユーザーにその瞬間に知らせたい情報を表示したり、選択肢を提示するUI」と考えたら良さそう。
実装
モーダルはコードを書かずに実装することが出来ます。Xcodeすごい!!
まず、UIViewControllerを設置して
次にボタンを設置します
そしてボタンをControlを押しながらクリックし、追加したUIViewControllerまでドラッグします。
そうすると、選択肢が表示されるので、Present Modallyを選択!
参考文献