- 投稿日:2019-07-01T18:47:28+09:00
【Swift】チャットアプリで、iOSアプリ開発入門講座を考えてみた(※随時更新予定)
はじめに
今では、多くのアプリで取り入れられているチャットアプリを用いて、初心者でもiOSアプリ開発ができるように紹介していこうと思います。
※記事全体を通して分からないところ・誤りがあるところあれば、なんでもコメントください。
流れ
- 【Swift】初心者でも絶対にできる"Hello World" iOS アプリを作成 ~iOS アプリ開発超入門~
- 【Swift】Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。
ここで少しだけUIをアレンジしたい方はこちら↓↓↓
【Swift】チャットアプリのUIをちょっとだけ良くしてみた- 【Swift5.0】UserDefaultsでパラメータを保存する~iOSアプリ開発入門~
- 【Swift】UITableViewControllerを使って、チャットアプリにトークルームを追加する
ソースコードはこちらのGitHubからどうぞ
https://github.com/Tetsukick/Firebase-chat-iOS
エラー一覧
実際に実施して発生したエラーを記載しておきます。
Case1. Could not build Objective-C module 'Firebase'
解決方法
「.xcodeproj」ファイルではなく、「.xcworkspace」ファイルから、Xcodeを開き直してください。
説明
- xcodeproj
→ メインプロジェクトとサブプロジェクトを管理することができるファイル- xcworkspace
→ 複数の同階層のプロジェクトを管理することができるファイル
※cocoaPodsを使用して、ライブラリを導入した場合は、同階層にPodsプロジェクトが作成され、そこにライブラリがbuildされているため、「xcworkspace」で開く必要があります。case2. xcode-select: error: tool ‘xcodebuild’ requires Xcode, but active developer directory ‘/Library/Developer/CommandLineTools’ is a command line tools instance
解決方法
Xcodeのpathに誤りがあります。正しいパスを指定し直してください。
$ sudo xcode-select -s <xcode_folder_path>例:
$ sudo xcode-select -s /Applications/Xcode.apptips
複数のXcodeバージョンをインストールしている場合も上記のコマンドで切り替えることができます。
選択されているXcodeをもとにライブラリがインストールされるため、インストール前に以下のコマンドでpathを確認することをおすすめします。$ xcode-select -print-pathcase3. Thread 1: signal SIGABRT
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<AQI_Chat__.ViewController 0x7fd446813f80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key textView2.'解決方法
Main.storyboard
ファイルを開き、UIとコードの接続を確認します。
Warningの箇所を削除して、必要に応じて、もう一度接続し直します。
tips
UIとコードの接続は、1対1である必要があります。
接続して、コードのみ削除すると、Storyboard上に接続関係だけが残ってしまい、エラーの原因になります。
また、接続させたコードの名前を変更すると関係性がなくなってしまい、こちらもエラーの原因になります。Q&A
Question1. 「platform :ios, '10.0'」 は何?
iOS10以上を対象としてライブラリをインストールすることを意味します。
変数の名前の変更方法は?
変更したい変数を選択して、右クリックしてください。
以下の Refactor > Rename を選択します。
今回は試しに、textView
をchatTextView
に変更します。
- 投稿日:2019-07-01T15:38:41+09:00
Swiftでも直和型したい
- 直和型欲しいよね
- TypeScriptでいう
let a : string | number
みたいな奴- Swiftにはありません
- でも
enum
で似たような事ができますenum Foo { case a(Int) case b(String) } // Fooの値にはInt, Stringどちらも持たせられる let fooA = Foo.a(123) let fooB = Foo.b("hello") // パターンマッチで値を取り出す let printFoo = { (foo: Foo) in switch foo { case .a(let a): print(a) case .b(let b): print(b) } } printFoo(fooA) // 123 printFoo(fooB) // hello // if letで値を取り出す if case let .b(b) = fooB { print(b) // hello }使用例
Result
enum Result<T> { case Result(T) case Error(Error) }エラーが出るまで処理を繰り返す、みたいなコードを書いてみる
enum Result<T> { case success(T) case failure(MyError) } enum MyError: Error { case tooSmallError } // 10%の確率で失敗する処理 let doit: () -> Result<String> = { let n = Float.random(in: 0..<1) if n < 0.1 { return .failure(.tooSmallError) } return .success("SUCCESS!") } // 失敗するまで繰り返す loop: while true { switch doit() { case .success(let msg): print(msg) case .failure(let err): print(err) break loop // 失敗したのでループを抜ける } }Option
Swiftはoptional value (
Int?
)があるので要らんけどenum Option<T> { case Some(T) case None } let printIfSome = { (opt: Option<T>) in switch opt { case .Some(let x): print(x) case .None: print("NONE") } } printIfSome(.Some(123)) // 123 printIfSome(.None) // NONE参考URL
- 投稿日:2019-07-01T11:04:31+09:00
【Swift5.0】UserDefaultsでパラメータを保存する~iOSアプリ開発入門~
はじめに
こちらの記事は、下記の記事のUIを変更していく過程を記載しています。
【Swift】Firebase Realtime Database を用いてチャットアプリを爆速コーディングしてみた。この記事で分かること
- UserDefaultsを使用した値の保存方法
スタート時のソースコード
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() readData() addKeyboardShowHideObserver() } private func readData() { 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)" } }) } private func addKeyboardShowHideObserver() { 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. ViewControllerにTextFieldのイベントを検知する権限を移譲する
ViewController.swift
の末尾に以下を追加。ViewController.swiftextension ViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { return true } }
ViewDidLoad()
内に以下を記載ViewController.swiftnameInputView.delegate = selfViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() readData() addKeyboardShowHideObserver() nameInputView.delegate = self }これで、UITextFieldが持つイベントをViewControllerで拾うことができるようになりました。
ちなみに、
textFieldShouldReturn
メソッドはUITextFieldの入力でEnterが押された事を検知するイベントです。2. UserDefaultsのインスタンスを生成
ViewController.swiftlet userDefaults = UserDefaults.standardViewController.swiftclass 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! let userDefaults = UserDefaults.standard override func viewDidLoad() { super.viewDidLoad() readData()3. Enterが押されたタイミングでUserDefaultsに値を保存
ViewController.swiftfunc textFieldShouldReturn(_ textField: UITextField) -> Bool { guard let inputText = textField.text else { return true } userDefaults.set(inputText, forKey: "name") userDefaults.synchronize() return true }4. UserDefaultsから値を読み込む
ViewController.swiftfileprivate func readNameData() -> String { return userDefaults.object(forKey: "name") as? String ?? "" }5. 読み込んだ値をUITextFieldに表示
ViewController.swiftnameInputView.text = readNameData()これで完了!
6. 完成形のソースがこちら
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! let userDefaults = UserDefaults.standard override func viewDidLoad() { super.viewDidLoad() readData() addKeyboardShowHideObserver() nameInputView.delegate = self nameInputView.text = readNameData() } private func readData() { 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)" } }) } private func addKeyboardShowHideObserver() { 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 } fileprivate func readNameData() -> String { return userDefaults.object(forKey: "name") as? String ?? "" } } extension ViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { guard let inputText = textField.text else { return true } userDefaults.set(inputText, forKey: "name") userDefaults.synchronize() return true } }7. リファクタリング
- Keyの定数化
- 保存処理をメソッド化
- 保存処理をボタン押下時にも追加
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! let userDefaults = UserDefaults.standard fileprivate let nameKey = "name" fileprivate let messageKey = "message" override func viewDidLoad() { super.viewDidLoad() readData() addKeyboardShowHideObserver() nameInputView.delegate = self nameInputView.text = readNameData() } private func readData() { databaseRef = Database.database().reference() databaseRef.observe(.childAdded, with: { snapshot in dump(snapshot) if let obj = snapshot.value as? [String : AnyObject], let name = obj[self.nameKey] as? String, let message = obj[self.messageKey] { let currentText = self.textView.text self.textView.text = (currentText ?? "") + "\n\(name) : \(message)" } }) } private func addKeyboardShowHideObserver() { 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 { save(name: name) let messageData = [nameKey: name, messageKey: 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 } fileprivate func readNameData() -> String { return userDefaults.object(forKey: nameKey) as? String ?? "" } fileprivate func save(name: String) { userDefaults.set(name, forKey: nameKey) userDefaults.synchronize() } } extension ViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { guard let inputText = textField.text else { return true } save(name: inputText) return true } }GitHubはこちら
- 投稿日:2019-07-01T10:09:32+09:00
Swiftのコードをすごろくにして、プログラミング初心者がプログラムの動作を体感的に学べるようにした
プログラミング × すごろく
数年前、娘とすごろくで遊んでいて、「すごろくってソースコードに似てるな」と思いました。すごろくのマスには色々な指示が書かれていて、ちょっと複雑なものだと「サイコロを振って 6 が出たら矢印の先のマスに進む。」のような感じです。これはまさに条件分岐です。
すごろくなら幼稚園児でも理解できます。プログラムのコードをすごろくで表現すれば、プログラミング初心者がプログラムの動作を理解する助けになるかもしれないと思いました(ただし、普通のすごろくでは止まったマスに書かれた指示だけを実行しますが、プログラムは通過したすべての行を実行しないといけないので、プログラムをすごろくで表現すると、止まったマスだけでなく通過したマスに書かれた指示もすべて実行するルールですごろくを遊ぶ必要があります)。
たとえば、次のようにすごろくのマスの中にコードと日本語の両方で指示を書けば、コードを読めない人でもコードとその意味を対応付けて学ぶことができます。
また、実際にコマを動かしながらすごろくを遊ぶことで、プログラムの動作をトレースし、プログラムがどのように動くかを体感することができます。プログラミング学習の初歩でつまづいてしまったという話をよく聞きますが、すごろくという目に見える形でプログラムの動作を繰り返し体験することで、感覚的に理解できるようになるかもしれません。
そのようなアイデアに基づいて、プログラミング初心者のためのすごろく 『プゴロク』 を作りました。学習効果を考えると手を動かして遊べる物理的な紙のすごろくの方が良さそうですが、レイアウトやデザインを変えながら実験し、コンセプトを検証するにはソフトウェアの方が手軽です。僕のスキルセットを元に検討し、 iOS アプリとしてプゴロクを開発しました。
ビジュアルプログラミングとの比較
テキストで書かれたコードは初心者や子供にとってわかりづらいので、よりわかりやすい表現をしようという方向性は、 Scratch 等のビジュアルプログラミング言語と似ています。たとえば、↑で挙げたコードを Scratch で表すと次のようになります。
Scratch のコードはプゴロク同様にビジュアルに表現されていますが、その表現手法には大きな違いがあります。
プゴロクの一つのマスはコードの 1 行に対応していて、まるでプログラムをステップ実行するかのようにコマを進めます。条件分岐やループなどで行をジャンプする箇所には矢印が描かれ、プログラムがどのような順序で実行されるかを直接的に表します。たとえば、
while
文は次のように表されます。一方で、 Scratch のコードはより構造的です。 Scratch のブロックは構文木のノードに対応しており、コードの構造を直接的に表します。
プログラミングに慣れた人から見ると、どれも本質的には同じことを表しており、表面的な表現手法の違いにしか見えないでしょう。プゴロクや Scratch だとわかるけど↓のコードだとわからないのがなぜか理解に苦しむかもしれません。
if ? != 6 { ... }しかし、人間は(というと主語が大きいですが、少なくとも僕は)自分が慣れていない表現に接すると、途端に認知能力が落ちてしまいます。たとえば、僕はプログラミングで慣れ親しんだ概念であるにも関わらず、ラムダ計算の記法(↓など)になかなか馴染めませんでした。
(λxy. x − y) 2 3そう考えると、すごろくという慣れ親しんだ表現は、テキストによるコードはもちろんのこと、 Scratch のようなブロックによる表現と比べても、初心者や子供にとって理解しやすい可能性があります。
もしその仮説が正しいなら、プゴロクの表現をベースにしたビジュアルプログラミング言語を考えてみるのもおもしろそうなテーマです。それはつまり、すごろくを作ることを通してコードを書くということです。
コードを書いてプログラムを作るというとハードルが高そうですが、すごろくを作るのなら子供の遊びです。子供たちは、「スタートに戻る」のようなループも含めて、その挙動をイメージしながらすごろくを作ることができます。もしかすると、すごろくを作るという形式であれば、プログラミングのハードルを下げることができるかもしれません。
コードをすごろくで表現する
一口にコードをすごろくで表現すると言っても検討することは色々あります。
たとえば、
if
文の矢印をどのように引くのかという一点をとっても、他との一貫性を考えて決めなければなりません。どのような表現が必要かは言語によっても異なります。たとえば、 Python では
}
がありません。 Python のwhile
文をすごろくで表現しようとすると次のようになるでしょう。プゴロクは、現状では Swift しかサポートしていませんが、後述のように他言語のサポートも検討中です。単純な
if
だけでなく、else
を伴うif
についても考えると矢印の引き方はもっとややこしくなります。 Swift では} else {
のように 1 行で書くことが一般的ですが、すごろくとして表現するには}
とelse {
の二つのマスに分割するのが良さそうです。また、上記の表現では矢印が同じ階層に複数本並行して走っていますが、一つの階層に最大何本の矢印を描画しなければならないでしょうか。色々なケースを検討した結果、最大 3 本描画できれば良さそうでした(下図の赤丸の箇所)。
ここまで検討してきた表現を用いて、色々な制御構造を表すと次のようになります。
break
やcontinue
のように階層を飛び越えるジャンプや、switch
,do/try/catch
のような多くのジャンプを伴う制御構造も表現することができます。関数についても、本流のすごろくとは別のすごろく片として表現し、関数呼び出しはそれらをつなぐ矢印で表現できそうです。
複数箇所から同じ関数が呼ばれている場合、関数を抜けて呼び出し元に戻るときに、適切な呼び出し元に戻らなければなりません。上図のように、矢印を色分けすることで呼び出し元との対応がわかりやすくなります。
その他に検討しなければならないのが変数の表現です。変数については、ボードゲームのスコアボードが参考になります。
ボードゲームのスコアボードでは、 0, 1, 2, 3, ... とスコアが書かれたマスが用意されていて、そこにコマを置くことで現在のスコアを表現することが多いです。スコアの増減はコマを動かすことで表します。これはスコアという変数を扱っているのと同じです。
たとえば、
a
というInt
型の変数をスコアボード形式で表すと次のようになります。このような検討の結果、初歩的なプログラムをすごろくとして表現することは現実的に可能だという結論に至りました。
どのようなコードをすごろくにするか
コードをどのようにすごろくで表すかという表現の問題とは別に、どのようなコードをすごろくにするかも重要です。色々なすごろくを作っても良いですが、まず初めに遊んでほしい定番のすごろくが示された方が、ユーザーにとってはわかりやすいと思います。
検討の結果、次のような要素は必須だろうと考えました。
- 条件分岐
- 繰り返し(ループ)
- 変数
さすがにこの三つの要素を欠いては、プログラミングというものをイメージしづらいと思います(一部の関数型言語などは事情が異なりますが、ここでは一般的な手続き型言語を想定しています)。
配列や関数、制御構造のネストなども検討しましたが、理解のための難易度が高まりますし、すごろくとしての表現も複雑になってしまいます。まず初めに遊んで楽しんでもらうには、あまり欲張って詰め込みすぎずに上記の 3 要素にとどめておく方が良さそうです。
これら 3 要素を含んだコードを作るのは簡単ですが、単にそれらの要素を含んでいるだけでなく、意味のあるコードをすごろくにしたいと考えました。コード自体に意味がないと、プゴロクを通してコードの挙動が理解できるようになっても、それが実際に意味のあるプログラムとしてどのように機能するのかイメージできないと思います。プログラミング学習の第一歩としてプゴロクを遊んでもらうことを期待するなら、意味のあるコードをすごろくにすることは欠かせないと思いました。
また、すごろくとして遊ぶからには、単に遊びとして見てもおもしろい方が望ましいです。普通のすごろくと違ってプゴロクでは通過したすべてのマスに書かれた指示を実行するため、どのマスに止まるかというドキドキがありません。単にサイコロを振って進むだけでは、大きな目をたくさん出した人が勝つことになってしまいます。マスの指示でサイコロを振らせるなどして何らかのランダム性を持たせなければなりません。加えて、すごろくとして遊ぶのに適切な時間で終わるように長さも考慮する必要があります。
これらの条件を元に検討した結果、ユークリッドの互除法(二つの自然数の最大公約数を求める最古のアルゴリズム)を実装したコードをすごろくにすることにしました。↓がそのコードです1。
var ?: Int { return (1...6).randomElement()! } var a = ? print(a) var b = ? + ? print(b) if a < b { let t = b b = a a = t } while b != 0 { let t = b b = a % b a = t } print(a)Swift では ? を含む一部の絵文字を識別子として使えます。また、グローバルな Computed Property を作ることができます。そのため、 ↑のコードは実際に実行可能です。
このコードを実行すると、
a
とb
の二つの自然数がランダムに決定・表示され、ユークリッドの互除法でそれらの最大公約数を求めた上で最後に結果が表示されます。これをすごろくとして遊ぶと、サイコロを振ってa
,b
の値を決めるため、その結果次第で後の挙動が変化し、「大きな目をたくさん出した人が勝つ」だけにはなりません。また、a
とb
がランダムに決定されることで、様々な自然数の組み合わせに対してユークリッドの互除法で最大公約数を求められることも体験してもらえます。
b
の初期値はサイコロを 2 回振った合計なのでa
の初期値より大きくなりやすいですが、運良くa
がb
以上になればif
の{}
をスキップできます。すごろくの序盤ではそれが重要そうに見えますが、しかしもっと重要なのがその後のwhile
ループを何回繰り返さなければならないかです。何回ループするかは実際に計算してみないとわかりづらいので、コマを進めながらドキドキすることになります。お世辞にもこのすごろくがめちゃくちゃおもしろいとは言えませんが、教材としては最低限のゲーム性を確保できたかなと思います。実際に娘と遊んでみましたが、後日「プゴロクやらせてー!」と言ってくる程度には楽しんでくれたようです。
上記のコードをすごろくにしたものの画像は巨大すぎて掲載できないので、興味のある方はアプリをダウンロードして確認してみて下さい。
余談ですが、 Swift で ? を識別子として使えるのは重要です。 Swift と併せて Python と JavaScript を最初からサポートすることを検討していたのですが、 Python や JS で ? が識別子として使えないことをどう扱うか(わかりやすさ優先で実行可能なコードを諦めて ? を使うか、実行可能なことにこだわって
dice()
やsaikoro()
のような関数を作るか)の結論が出ず、ひとまず Swift だけをサポートすることにしました。紙のすごろくの作成
まずは iOS アプリとしてプゴロクを作りましたが、教材としての効果を考えると、アプリに加えて物理的な紙のすごろくも作りたいと考えています。
アプリでは、サイコロを振ると自動的にコマが動かされます。それを見てプログラムの動作を観察することはできますが、自分の手でコードの意味を考えながらコマを動かすのとでは得られる理解の深さが違うでしょう。紙のすごろくで遊んでもらってこそ、本当に「プログラムの動作を体感的に学べる」ものになると思います。
しかし、物理的なすごろくとなると印刷にもコストがかかりますし、配布もアプリと同じようにはいきません。そのあたりの問題は棚上げして、ひとまずは紙のプゴロクのプロトタイプ作成に取り組みたいと考えています。
なお、仮に紙のプゴロクが普及しても、アプリ版は正しい遊び方を確認するのに有用なんじゃないかと思います。僕はときどきボードゲームで遊びますが、紙版で遊んだ後でアプリ版をプレイしたときにルールを間違えていたことに気付くという経験を何度かしています。来年( 2020 年)から小学校でのプログラミング必修化が始まりますが、学校の先生はプログラミングの専門家ではありません。プゴロクをアンプラグドなプログラミング教育の手段として活用してもらうことまで考えれば、プログラミングに精通した人がいなくても正しい遊び方を確かめるアプリという方法があることは重要だと思います2。
レイアウトとレンダリング
アプリにせよ、紙のすごろくにせよ、何らかの方法ですごろくを描画しなければなりません。プゴロクのマスや矢印の描画を考えると、規則性が強く、同じようなパターンが繰り返し出現するので、イラレ等で作るよりもコードで生成した方が労力が小さそうです。
すごろくの描画は次の要件を満たす必要があります。
- マスの中にはテキスト(コードや日本語での指示)が書かれ、その長さに応じてマスの
height
が伸び縮みする- テキストが短い場合にマスが縦に潰れてしまわないように、マスのアスペクト比(
width
:height
またはwidth
/height
)が一定値より大きくならないようにする- 各種要素間に適切な余白を指定できる
- 任意のマス間で矢印を描画できる
- (印刷を見据えると)ベクター形式で書き出し可能か、印刷に耐えうる高解像度で描画できる
マスの柔軟なレイアウトと矢印の複雑なレンダリングを実現するために、僕のスキルセットで最適と思われた Auto Layout + Core Graphics を選択しました。 1, 2, 3 を Auto Layout で対応し、 4 を Core Graphics で対応します。
5 については、
UIView
からベクター形式で書き出すことはできないですが、単純なデザインなので計算された座標を元にベクター形式で書き出すコードを書くことは難しくないだろうと考えました(こちらは現時点で未検証で、紙版のプロトタイプ作成のためにこれから取り組む予定です)。マスのレイアウト
Auto Layout によるマスのレイアウトは↓のように実現できます。
マスのアスペクト比について、 10 : 8 より横長にならないようにする(
<= 10 / 8
の) constraint (制約)を設定するのと同時に、 priority (優先度)を下げて== 10 / 8
の constraint も追加するのがポイントです。このときに、== 10 / 8
の priority はUILabel
の content compression resistance priority よりも小さな値にする必要があります。
UILabel
は保持するテキストのフォントや長さに応じたサイズ intrinsic content size を持っています。たとえば、width
を固定するとテキストの長さに応じてheight
が変化します。UILabel
の intrinsic content size の priority は content compression resistance priority (伸びる場合の priority )と content hugging priority (縮む場合の priority )によって決定されます。今は伸びる場合を考えているので、もし== 10 / 8
の priority を content compression resistance priority 以上に設定してしまうと、テキストの長さに応じてUILabel
のheight
が伸びるよりもアスペクト比が 10 : 8 になることが優先されてしまいます。つまり、テキストが長くてもマスのhieght
が伸びなくなってしまいます。Auto Layout を使えば、親子・隣接 view 間はもちろん、同じ view ツリーに属していれば階層を飛び越えて constraint を設定できたり、任意の対象の間で、
Y == a * X + b
(または、==
の代わりに<=
,>=
、X
,Y
は制約の対象)の形で自由に制約を記述できるなど、柔軟なレイアウトが可能です。柔軟性が高いと、少々考慮漏れがあっても後からリカバリーしやすいです。今回も、スタートのマスと scroll view の左右中心をそろえて表示するというやや複雑な制約の考慮が漏れていましたが、後から簡単に対応することができました。矢印の描画
UIView
のdraw(_: CGRect)
メソッドをオーバーライドすることで、 view をカスタマイズして任意の描画が可能になります。たとえば、 Core Graphics を用いて右上から左下に赤い線を引くだけの view を↓のように作ることができます。
@IBDesignable class CustomView: UIView { override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } context.setLineWidth(6.0) context.setStrokeColor(UIColor.red.cgColor) context.move(to: CGPoint(x: bounds.maxX, y: bounds.minY)) context.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY)) context.strokePath() } }これを使えば矢印の描画も簡単です。各マスの座標は Auto Layout が constraint を元に自動的に決定してくれるので、
draw(_:)
メソッドでその座標を元に矢印を引くだけです。レイアウトとレンダリングの課題
小学生に使ってもらうことを考えるとマスの中の文章にふりがなを付けたかったんですが、
UILabel
の技術的制約で簡単にはできませんでした。今後なんとかして対応したいと考えています。おまけ: 物理演算と 3D レンダリング
プゴロクの iOS アプリを作る上でどうしてもやってみたかったことが、物理演算を使ってサイコロを振ることです。
1 から 6 までの整数をランダムに一つ選べばいいだけなのでやりすぎ感はありますが、すごろくをプレイしてる感じを表現したくてやりました。 SceneKit を使えば物理演算も 3D レンダリングも簡単です。
意外と面倒なのが、出た目が何かを計算することです。人間が見れば一目瞭然ですが、出目を求めるために具体的に何を計算すれば良いかを考えるとそこまで単純ではありません。プゴロクでは、サイコロの座標系における各面の方向を表すベクトルと鉛直上向きのベクトルの成す角を計算して、最小のものを選択するようにしました。
let directions: [SCNVector3] = [ SCNVector3(0, 1, 0), SCNVector3(0, 0, 1), SCNVector3(1, 0, 0), SCNVector3(-1, 0, 0), SCNVector3(0, 0, -1), SCNVector3(0, -1, 0), ] let transform = diceNode.presentation.transform let upDirection = SCNVector3(transform.m12, transform.m22, transform.m32) let (index, _) = directions .map { $0.angle(from: upDirection) } .enumerated() .min { $0.1 < $1.1 }! let number = index + 1また、 SceneKit を使っていたらタイトル画面もかっこよく 3D にしたくなってきて、実際にすごろくを遊ぶときは 2D なのに、タイトル画面のためだけに無駄にすごろくを 3D 化してしました(本投稿冒頭の画像です)。
さらに、 Web サイトも 3D にしたいという誘惑には勝てず、 WebGL / three.js で 3D 化しました。 WebGL / three.js は初めて触りましたが、ブラウザ上に簡単に 3D コンテンツが表示できて楽しかったです。
まとめ
- Swift のコードをすごろくにした( iOS アプリ)
- プログラミング初心者がすごろくで遊びながらプログラムの動作を学べる
- ビジュアルプログラミングの表現形式の一緒として考えてもおもしろい
- ユークリッドの互除法のコードをすごろくにすると楽しい
- 学習効果を考えると紙のすごろくは欠かせないので紙版も作りたい
- 3D は楽しい
ただし、 ? を宣言している 1 行目についてはすごろくから除外しました。スタートのマスにこのコードを入れることも検討しましたが、コードだけあって説明がないのでは混乱を招くだけになりそうなので、どこか別の場所で暗黙的に宣言されていることにしました。ただ、このままではすごろくに書かれたコードをそのままコピペしても実行できません。プゴロクのアプリには、すごろくのすべてのマスのコードを抜き出して並べて表示するページがあるので、そのページ上では注釈をつけた上で ? の宣言も表示するようにしました。 ↩
小学校のプログラミング必修化は、算数などの既存教科の中でプログラミングを取り扱うことになっているので、プゴロクが活用できるかは未知数です。 ↩
- 投稿日:2019-07-01T09:27:24+09:00
NSFetchedResultsControllerの使い方
NSFetchedResultsControllerを使う際注意点のまとめ
1.1つ以上のsort descriptorが必要
let nameSort = NSSortDescriptor(key: "name", ascending: true) fetchRequest.sortDescriptors = [cateSort, nameSort]2.Grouping results into sectionsの際、sectionNameKeyPathの設定も必要し、sectionNameKeyPathのKeyを1番のsort descriptorとして追加することも必要
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: "category",cacheName: nil) let cateSort = NSSortDescriptor(key: "category", ascending: true) let nameSort = NSSortDescriptor(key: "name", ascending: true) fetchRequest.sortDescriptors = [cateSort, nameSort]3.sort descriptorの追加される順でsortする。
例えば
let zoneSort = NSSortDescriptor(key: "qualifyingZone", ascending: true) let scoreSort = NSSortDescriptor(key: "wins", ascending: false) let nameSort = NSSortDescriptor(key: "teamName", ascending: true) fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]
- 投稿日:2019-07-01T06:59:13+09:00
Generics or Protocol associated type? それが問題だ?
はじめに
「Swiftでは、任意の型に対応させられるように、Genericsを使って型を抽象化できるのか。。。?」
「なんと、Protocolでもassosiated typeで任意の型を表現できるらしい!?」「。。。。。。。。。。。。。」
「で、どっち使えばいいんだ???」
※以下 PAT = Protocol associated type
検証 (Swift5, Xcode10.2.1)
例えば下記のようにポケモン(もはや恒例)を表す
Pokemon
というプロトコルとそれに準拠したPikachu
があるとする。
今回はポケモンの名前を出力してくれるPokemonDescriptor
を作りたい。
PokemonDescriptor
は、Pokemon
プロトコルに準拠しているものなら何でも出力できる。protocol Pokemon { var name: String { get } } struct Pikachu: Pokemon { let name: String }PATを使った場合
protocol CharacterDescribable { associatedtype Character func describe(_ character: Character) } struct PokemonDescriptor: CharacterDescribable { typealias Character = Pokemon func describe(_ character: Character) { print("\(character.name)だよ〜") } } let pikachu = Pikachu(name: "ピカチュウ") let pokeDescriptor = PokemonDescriptor() pokeDescriptor.describe(pikachu) // output: ピカチュウだよ〜Genericsを使った場合
struct PokemonDescriptor { func describe<T: Pokemon>(_ pokemon: T) { print("\(pokemon.name)だよ〜") } } let pikachu = Pikachu(name: "ピカチュウ") let pokeDescriptor = PokemonDescriptor() pokeDescriptor.describe(pikachu) // output: ピカチュウだよ〜何が違うのか?
プログラム自体がしていることは同じ。
違いはそれぞれのdescribe()
メソッドが実行される方法(Method dispatch)。Method dispatchとは
メソッドを呼び出すときにどの実装が実行されるかを選択/解決するシステム。
コンパイラがコンパイル時にそれを解決するのがstatic dispatch。実行時に解決するのはdynamic dispatch。Qiita内だと、この記事とかSwiftのMethod dispatchについて詳しく書かれてます?
GenericsとPATにおいて、前者で定義されたメソッドはstatic dispatchで実行されるが後者で定義されたメソッドはdynamic dispatchで実行される。
staticはコンパイル時にどのメソッドを走らせるか事前に決めるので実行速度が速い、ただしメモリーをより多く消費する。
逆にdynamicはプログラム実行中に動的に決定するので、その分実行速度は落ちるが効率的にメモリーを使用できる。結論✍️
- ケースバイケース
- どちらでもよく、簡単なコードならとりあえず実行速度の速いGenericsを検討する
- ただし、PATの方が表現力や可読性が高い(そもそもSwiftはProtocol oriented)
- 自分はPATでやりたい
最後に
自分でもまだしっくり来てないです。誰かアドバイスください!!(説教でも可?)
参考資料
SwiftにおけるMethod Dispatchについて - Qiita
Method Dispatch in Swift – RPLabs – Rightpoint Labs
- 投稿日:2019-07-01T01:28:27+09:00
Swift:NSCollectionViewの使い方
はじめに
NSCollectionView
を使ってアイテムをいい感じに並べて,インタラクションできるようにしたいと思ったのですが,これがなかなか厄介でした.
本記事ではNSCollectionView
の基本的な実装の仕方,NSCollectionViewItem
の設定の仕方,プログラムによるアイテムの追加と削除の仕方,ドラッグ&ドロップによるアイテムの入れ替え方の仕方をまとめます.Storyboardの下準備
NSCollectionViewを配置
ViewController.swiftと紐づける
ここ要注意!普通にアイテムを選んで紐づけるとNSScrollView
になってしまうので,リストの方から確実にCollectionView
を選んで紐づけましょう.NSCollectionViewItemを用意
新規ファイルからカスタムクラスを作りましょう.ここではSampleItemとしています.
このとき,xib
ファイルも一緒に作ります.(xibファイルなしの方法が見つからなかったです.)SampleItem.xibを開いて
Collection View Item
を追加します.
下ようになるはず.
そうしたら,カスタムクラスの指定をします.
view,imageView,textFieldなどをいじるはずなので,それらのUIを配置したあと,紐付けます.
↑こんな感じになっていればOK基本的な実装
ViewController.swiftimport Cocoa class ViewController: NSViewController { @IBOutlet weak var collectionView: NSCollectionView! var data = ["A", "B", "C", "D"] override func viewDidLoad() { super.viewDidLoad() // デリゲートとデータソースの紐付け collectionView.delegate = self collectionView.dataSource = self // nibファイルの登録 let nib = NSNib(nibNamed: "SampleItem", bundle: nil) collectionView.register(nib, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sample")) // データのリロード collectionView.reloadData() } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } } // デリゲートとデータソースの実装 extension ViewController: NSCollectionViewDelegate, NSCollectionViewDataSource { func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return data.count } func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { // アイテムの用意 let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sample"), for: indexPath) as! SampleItem item.textField?.stringValue = data[indexPath.item] item.imageView?.image = NSImage(imageLiteralResourceName: "Sample") return item } }アイテムの追加
ViewController.swift@IBAction func appendData(_ sender: Any) { data.append("E") //例えば collectionView.reloadData() }アイテムの選択(UIの更新例)
SampleItem.swiftimport Cocoa class SampleItem: NSCollectionViewItem { override func viewDidLoad() { super.viewDidLoad() } func updateBG() { if isSelected { self.view.layer?.backgroundColor = NSColor(deviceWhite: 1.0, alpha: 0.3).cgColor } else { self.view.layer?.backgroundColor = CGColor.clear } } }ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // デリゲートとデータソースの紐付け collectionView.delegate = self collectionView.dataSource = self // 選択可能にする collectionView.isSelectable = true // 以下省略 } // 中略 // デリゲートとデータソースの実装 extension ViewController: NSCollectionViewDelegate, NSCollectionViewDataSource { // 中略 // 選択系 func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) { let count = collectionView.numberOfItems(inSection: 0) for n in (0 ..< count) { (collectionView.item(at: n) as! SampleItem).updateBG() } } func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set<IndexPath>) { let count = collectionView.numberOfItems(inSection: 0) for n in (0 ..< count) { (collectionView.item(at: n) as! SampleItem).updateBG() } } }選択中のアイテムの削除
ViewController.swift@IBAction func removeData(_ sender: Any) { guard let n = collectionView.selectionIndexPaths.first?.item else { return } let item = collectionView.item(at: n) as! SampleItem item.isSelected = false item.updateBG() data.remove(at: n) collectionView.reloadData() }ドラッグ&ドロップでアイテムの並び替えをする
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // デリゲートとデータソースの紐付け collectionView.delegate = self collectionView.dataSource = self collectionView.isSelectable = true // ドラッグタイプを設定 collectionView.registerForDraggedTypes([NSPasteboard.PasteboardType.string]) // 以下省略 } // 中略 // デリゲートとデータソースの実装 extension ViewController: NSCollectionViewDelegate, NSCollectionViewDataSource { // 中略 // ドラッグ func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexes: IndexSet, with event: NSEvent) -> Bool { return true } // ↓と間違えないこと func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool { } func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { let pasteboardItem = NSPasteboardItem() pasteboardItem.setString(String(indexPath.item), forType: NSPasteboard.PasteboardType.string) return pasteboardItem } // ↓と間違えないこと func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt index: Int) -> NSPasteboardWriting? { } // ドロップ func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation { return NSDragOperation.move } // ↓と間違えないこと func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndex proposedDropIndex: UnsafeMutablePointer<Int>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation { } func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool { let pasteboard = draggingInfo.draggingPasteboard guard let str = pasteboard.string(forType: NSPasteboard.PasteboardType.string) else { return false } let oldIndex = Int(str)! let newIndex = indexPath.item if oldIndex < newIndex - 1 { collectionView.moveItem(at: IndexPath(item: oldIndex, section: 0), to: IndexPath(item: newIndex - 1, section: 0)) data.swapAt(oldIndex, newIndex - 1) } else { collectionView.moveItem(at: IndexPath(item: oldIndex, section: 0), to: IndexPath(item: newIndex, section: 0)) data.insert(data.remove(at: oldIndex), at: newIndex) // 重要! sawpじゃダメ } return true } // ↓と間違えないこと func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, index: Int, dropOperation: NSCollectionView.DropOperation) -> Bool { } }要注意!
index
は罠です.indexPath
のやつを使いましょう.備考
外部からドラッグ&ドロップをしてアイテムが適合できるものなら追加する,というのもできるらしいので追々実装したい.