- 投稿日:2020-06-27T23:59:24+09:00
TableViewについて
TableViewについて
表題の通りですが、初心者にとってはTableViewはわかりにくいところが多かったので、メモにしておこうと思います。よろしければ、同じような初心者の方も参考にしてみてください。
間違っていたら、お手数ですがお知らせください。バージョンは下記の通りです。
Xcode 11.5
Swift 5.2.4TableViewとは
この機能はゲーム等、色々なところで見かけます。
iPhoneのアプリで言うと、メモとかに使われているもので、垂直方向に値等をストックしていく機能を持っています。なので、配列と併せて使うと効果を発揮します。TableViewを設置する
Storyboard上にTableViewを画面に設置してその上にTableViewCellを重ねるように設置します。文字入力用としてtextfieldも追加します。
TableView・・・TableViewの機能の土台となる部分
TableViewCell・・・値を垂直方向に貯めていくセルを生成する部分また、TableViewCellはStoryboardのAttributes indicatorの中にあるidentifierは空白になっているので、ID名を入れる必要があります。
UIとプログラムをつなげる
Storyboard上からCtrlキーを押しながらプログラムにつなげて変数名を付けます。
さらにデータを貯める配列も宣言します。@IBOutlet weak var textField: UITextField! @IBOutlet weak var tableView: UITableView! var tableArray :[String] = [] //var tableArray = [String]()TableViewプログラム記述(プロトコル)
プロトコルを宣言します。
①UITableViewDelegate・・・TableView関係のプロトコル
②UITableViewDataSource・・・TableView関係のプロトコル
③UITextFieldDelegate・・・TextField関係のプロトコルこのままだとプロトコルが使えないので、それぞれviewDidLoad()内で宣言して、UIViewControllerで使えるようにします。
①tableView.delegate = self
②tableView.dataSource = self
③textField.delegate = selfclass ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UITextFieldDelegate { @IBOutlet weak var textField: UITextField! @IBOutlet weak var tableView: UITableView! var tableArray :[String] = [] override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self textField.delegate = self }記述が終わると、Xcodeから関数がないと言われるので、fixを選択すると下記の関数が自動で追加されます。この2つの関数がtableViewを動かす最低限の関数だから自動的に追加されるのだと思います。
①のプロトコルからの関数・・・tableViewのセルの数等を決める関数
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int②のプロトコルからの関数・・・生成したセルのプロパティを決める関数
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellTableViewプログラム記述(関数)
自動で追加された2つの関数を作っていきます。
どちらの関数も -> が入っているので、最後にreturn 〇〇で終了し、値を返します。(※但し、①はreturnを省略しても大丈夫です。)上記①のプロトコルからの関数は数の指定なので、tableViewの値を貯める数を指定します。個人的にはtableViewは配列と併せて使うものだと思っていますので、値は配列の数だけ貯めるように指定します。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { tableArray.count }上記②のプロトコルからの関数はそれぞれの引数にクラスのUITableView、構造体のIndexPathを宣言します。
また、①と同様にクラスを引数に入れていますが、こちらではUITableViewクラスのプロパティを設定する必要があります。
その為、UITableViewクラスの関数等の使用は初なので、プロトコルの関数内にて、UITableViewクラスのインスタンス化を行います。
withIdentifierで使われるIDはセルについてなので、当然Storyboard上で設定したTableViewCellのID名とし、UITableViewクラスのプロパティと繋げます。
cellForRowAt indexPath: IndexPathは値が入り生成されたセルの場所を示します。
プロトコル内の引数を同じにして、セルを生成した場所 = セルの保存場所とします。func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let tableCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) tableCell.textLabel?.text = tableArray[indexPath.row] return tableCell }textfieldプログラム記述(関数)
textfieldに入力された値を、tableViewの配列に蓄積させます。
tableArray.append(textField.text!)を記述することで、
tableArrayに値が追加 → tableArrayの数分だけセルの数を追加 → tableArrayの値をセルに追加となる。resignFirstResponder()は実機等で文字のreturnキーを押した時に、キーボードを非表示にする機能です。
-> Boolなのでtrueかfalseで返します。
func textFieldShouldReturn(_ textField: UITextField) -> Bool { tableArray.append(textField.text!) textField.resignFirstResponder() textField.text = "" tableView.reloadData() return true }ビルドしてみる
ここまででビルドするとこんな感じです。
なんか殺風景すぎるけど、ここにセルの高さ指定、イメージ挿入、背景追加、値の保存、タップした時の動きとか、色々機能を追加することで形にはなると思います。
以上です。
- 投稿日:2020-06-27T21:58:57+09:00
UITabBarController において選択されたタブに応じた処理をしたい時
先日、 Google Analytics などのイベントトラッキング系の実装をしている時に、
「選択されたタブのログを取りたい、ただし現在開いているタブは取らなくていい。」という場面に出会いました。画面に依らない共通の処理だったので、それぞれの
ViewController
ではなくUITabBarController
に書くことにしました。
簡単な実装かと思いきや、結構奥深い学びがあったので共有します。
- バッドプラクティスとそれが悪い実装となる理由
- 最終的な実装例
を紹介します。
2020 年に Swift 始めたばかりの初心者なので、アドバイス・指摘待っています!!!
ざっくりとした結論
始めに、最終的に至った形を示しておきます。
後の理解を深めるためと、時間がない人のためです。
tabBar(_:didSelect:)
を使おう- 引数
item
とitems
配列でパターンマッチングしようitem.tag
を使った指定はバッドプラクティスになりがちなので、極力避けよう!これを読んで、「そんなの当たり前じゃん?」ってなった方はもうここから先を読む必要はないです。
逆に「なんでそれがバッドプラクティス?」ってなった方は読んでみて下さい!!バッドプラクティス:
item.tag
を使うググってみると、ちょこちょこ見かける方法ですが、これは 基本的にバッドプラクティスになりがち です。
具体的には以下のような実装ですね。 (僕も最初こうやってました)class TabBarController: UITabBarController { override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { switch item.tag { case 0: // tag = 0 に対する処理 case 1: // tag = 2 に対する処理 ... default: break } } }理由はまだしっかり理解できていませんが (誰が教えて…)、
- 個別に
tag
を設定する必要がある- そしてその
tag
を参照することで依存が生まれるからかなーと思っています。
個別に
tag
を設定する必要があるということは、別に左から 0, 1, 2, 3, ... と付けなくても良いわけで…
めちゃめちゃ屁理屈な人が 4, 8, 12, ... とか付けてたら泣きますよね笑このように特定の
UIView
インスタンスへの依存を生むような実装は避けるべきです。最終的な実装例
じゃあどうするかというと、メソッドの引数である
item
とtabBar.items
の配列を照らし合わせる形で実装しました。まず、実装の前提ですが、以下のような
Tab
というタブの型を定義しています。class TabBarController: UITabBarController { private enum Tab: Int { case home case history case setting } ... }使うメソッドは先と同じく、
tabBar(_:didSelect:)
です。
ただし異なる点として、tabBar.items
の配列とパターンマッチングしていきます。class TabBarController: UITabBarController { ... // 現在選択されているタブをプロパティとして持っておく private var selectedTab: Tab = .home override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let firstIndex = tabBar.items?.firstIndex(of: item), let tab = Tab(rawValue: firstIndex) else { return } // 表示されている画面と同じタブがタップされた場合は何もしない if tab == selectedTab { return } switch tab { case .home: // .home 画面に対する処理 case .history: // .history 画面に対する処理 case .setting: // .setting 画面に対する処理 } // 選択されているタブの更新 selectedTab = tab } }肝になるのはここですね。
guard let firstIndex = tabBar.items?.firstIndex(of: item), let tab = Tab(rawValue: firstIndex) else { return }何をしているかというと、
- 引数である
item
で選択されたUITabBarItem
を取得してtabbar
のitems
(UITabBarItem
が順番に入った入った箱) の中の何番目かをfirstIndex(of:)
で調べてTab
型に変換するということをしています。
最後の型の変換は今回の実装ならではですが、2
番目までで、選択されたタブの順番が取得できるので、あとはその順番を使って処理を作っていけばいいです。この実装では
UIKit
やUIView
への依存を生んでおらず、Tab
を使ってUITabBar
をセットアップしていけば後でバグが生まれるということが少ないです。
(仮にバグが生まれても、発見が早い)まとめ
UITabBarController
初めて触ったのですが、奥深すぎて仲良くなれる気がしません…
(UITabBarController
とUITabBar
の違いも分かってない)
追記
本記事公開後、 @lovee さんより、UITabBarController
とUITabBar
の違いについてコメント頂いたのでそのまま記載します!!ちなみに
UITabBar
とUITabBarController
の違いはそれぞれの名前とおり、UITabBar
はバー(UIView
継承)でUITabBarController
はコントローラー(UIViewController
継承)です
ですが、今回の実装を経験して、「後から保守性が落ちるコード」、「クラッシュ・バグを生みかねないコード」を書かないように意識して実装したいと思いました。
いずれは人のコード読んで「なんか怪しいな」と怪しいコードを 嗅ぎ分けられる ようになりたいですこの記事は自分の備忘録を建前として、 iOS エンジニアの皆様から「もっとこうしたら良いよ!」 「こんな方法もあるよ!」 「その実装だとこんな時危険だよ!!」 という意見を頂戴することを裏の目的としていますので、
アドバイス・指摘バンバンください!参考
- 投稿日:2020-06-27T19:01:03+09:00
[Swift] Date生成時にありえない日付のときはnilを返したい
TL;DR
DateFormatterを使い
isLenient = false
に設定する。func makeDate(year: Int, month: Int, day: Int) -> Date? { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.isLenient = false formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: String(format: "%d-%02d-%02d", year, month, day)) } let date = makeDate(year: 2021, month: 2, day: 29) // -> nilやりたいこと
例えば年と月と日を別々に入力するUIがあってそこからDate型を生成したいとき、ユーザーが2021年2月29日のような存在しない日付を入力したらnilを返したい。
❌ DateComponentsを使う方法
func makeDate(year: Int, month: Int, day: Int) -> Date? { var components = DateComponents() components.calendar = Calendar(identifier: .gregorian) components.timeZone = TimeZone(secondsFromGMT: 0) components.year = year components.month = month components.day = day return components.date } let date = makeDate(year: 2021, month: 2, day: 29) // -> 2021年3月1日自動で日が進んで別の日付を返してしまう。
❌ DateFormatterを使ってisLenientを指定しない方法
func makeDate(year: Int, month: Int, day: Int) -> Date? { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: String(format: "%d-%02d-%02d", year, month, day)) } let date = makeDate(year: 2021, month: 2, day: 29) // -> 2021年3月1日自動で日が進んで別の日付を返してしまう。
ただし、32日を指定するとnilが返ってくる。
この挙動はDateComponentsとは異なる。let date = makeDate(year: 2021, month: 2, day: 32) // -> nil環境
Swift 5.1
謝辞
@kishikawakatsumi さんに教えていただきました。ありがとうございました。
- 投稿日:2020-06-27T12:04:20+09:00
iOSのExposureNotificationのAPIをサンプルアプリとドキュメントから見てみる
ExposureNotification概要
ExposureNotificationはCOVID-19への潜在的な曝露を人々に通知します。
ExposureNotificationの機能を実現するにはExposure Notification Serverの実装が必須です。おことわり
実機ビルドするには Exposure Notification Entitlement Requestで申請する必要があり、ハードルが高いので、
あくまで、公式ドキュメントとサンプルアプリのシミュレータでの確認になっています。
大いに間違っている可能性があるので公式ドキュメントを正としてください。公式ドキュメント
https://developer.apple.com/documentation/exposurenotification
サンプルアプリ
ENManager
activate
func activate(completionHandler: @escaping ENErrorHandler)ENManagerを使う前に最初に呼び出す。
これを行い、完了ハンドラが戻ってきたところでENManagerの機能が使えるようになる。setExposureNotificationEnabled
func setExposureNotificationEnabled(_ enabled: Bool,completionHandler: @escaping ENErrorHandler)曝露通知を有効/無効にするクラス。
自動的に通知許可をもらうダイアログも表示する。
許可が断られた場合、completionHandlerはENError.notAuthorized
を返す?
enabledをfalseにした場合、 Bluetoothのスキャンとアドバタイズを停止するが、取得していた診断データはそのまま残る。
残念ながらシミュレータで動作させてもDomain=NSOSStatusErrorDomain Code=-71148
のErrorになります。detectExposures
func detectExposures(configuration: ENExposureConfiguration, diagnosisKeyURLs: [URL], completionHandler: @escaping ENDetectExposuresHandler) -> Progress曝露を検出する。
diagnosisKeyURLsは、Exposure Notification ServerからダウンロードしたキーのローカルURLとする。
configurationに関しても、Exposure Notification Serverから取得するデータから生成することをサンプルアプリでは想定している。このAPIはBluetooth経由で得ているデータをワンショット返すようで、サンプルアプリではBackgroundTasksフレームワークを用いて定期的に検出を行っている。
completionHandlerで返ってくる
ENDetectExposuresHandler
にはENExposureDetectionSummary
が含まれていて、後述のgetExposureInfo
で使用する。getExposureInfo
func getExposureInfo(summary: ENExposureDetectionSummary, userExplanation: String, completionHandler: @escaping ENGetExposureInfoHandler) -> Progress前述の
detectExposures
から得たENExposureDetectionSummary
をパラメータにして曝露情報を取得する。
userExplanation
はUIの一部としてユーザーに表示するために使われる。
コールバックとしてENExposureInfo
が受け取れる。
サンプルアプリではこの情報を独自の構造にマッピングしてローカルに保存している。getDiagnosisKeys
func getDiagnosisKeys(completionHandler: @escaping ENGetDiagnosisKeysHandler)
ENTemporaryExposureKey
を取得する。
このメソッドを呼び出すたびに、ユーザーは承認を行う必要がある。
このキーはそのままExposure Notification Serverに送信する必要があり、サンプルアプリでは一旦JSONにデコードして送信する想定となっている。getTestDiagnosisKeys
func getTestDiagnosisKeys(completionHandler: @escaping ENGetDiagnosisKeysHandler)
getDiagnosisKeys
のテスト用API。
シミュレータで動作させてもDomain=NSOSStatusErrorDomain Code=-71148
のErrorになります。invalidate
func invalidate()ENManagerを無効にする。
ENStatus
曝露通知システムの有効状態を示すenum。
特筆したいのがbluetoothOff
という種別であり、ENStatusにしかBluetooth使用を示すものがないので、ExposureNotificationのフレームワーク内で完全にBluetoothの処理は閉じていてBluetoothに対するコントロールは出来なさそう。tvOS14(おまけ)
tvOS14にもExposureNotification.frameworkが有効なんですが、
- ENAuthorizationStatus
- ENStatus
- ENError
と、限定的なAPIしか使えず、メインのENManagerが使えない模様。
- 投稿日:2020-06-27T03:14:51+09:00
TypeScriptでオプショナル型
なんかできそうな気がした
ユーザー定義のType Guardというものを知ってこれSwiftのオプショナル型みたいなものが作れるんじゃないかと思った
コード
class Optinal<T> { private _value: T | null public get value() { return this._value } public set value(newValue: T | null) { this._value = newValue } public constructor(value: T | null = null) { this._value = value } private filter(arg: T | null): arg is T { return arg !== null } public unwrap(handler: (nonNullValue: T) => void) { if (this.filter(this._value)) { handler(this._value) } return { else: this.else.bind(this) } } private else(handler: () => void) { if (!this.filter(this._value)) { handler() } } }使い方
定義
定義const hoge = new Optinal<string | number>('初期値と違う型にしたい場合は型を指定する') const fuga = new Optinal('初期値と同じ型のオプショナル型にしたい場合は省略してもいい') const piyo = new Optinal<string>() // nullで初期化する場合は引数省略してもいい生の値を取得
生の値を取得const optinalVar = new Optinal(100) // Getterを使う console.log(optinalVar.value) // 100再代入
再代入const optinalVar = new Optinal(100) // Setterを使う optinalVar.value = null console.log(optinalVar.value) // null // これでもいいけど面倒くさい let pattern2 = new Optinal<number>(3) pattern2 = new Optinal<number>(null)強制アンラップ
強制アンラップconst optinalVar = new Optinal('nullだとエラーになる') // Getter使って取得した生の値に ! をつける console.log(optinalVar.value!)オプショナルチェインニング
オプショナルチェインニングinterface Example { propertyA: string } const ppp = new Optinal<Example>(null) // Getter使って取得した生の値にオプショナルチェインニングする console.log(ppp.value?.propertyA) // undefined※ES2020~/TypeScript3.7~
オプショナルバインディング
Swiftの
if let
のような感じでアンラップできるオプショナルバインディングconst optinalVar = new Optinal('nullだと実行されない') optinalVar.unwrap(optinalVar => { // このブロック(クロージャ)内ではoptinalVarは絶対にnullではない console.log(optinalVar) // nullだと実行されない })ユーザー定義のType Guard を使うことで型情報からnullが取り除かれている
elseも使えるconst optinalVar = new Optinal<number>(null) optinalVar.unwrap(optinalVar => { // nullじゃない場合の処理 }).else(() => { // nullの場合の処理 })できないこと
早期リターン
swiftだと早期リターンできるvar hoge string? = "aaa" if let hoge = hoge { // nilじゃなかったときの処理 return // 早期リターン }早期リターンできないconst optinalVar = new Optinal('コードブロックじゃなくてクロージャだからね') optinalVar.unwrap(optinalVar => { // nullじゃなかったときの処理 return undefined // 早期リターンできない })
try ~ catch
でできなくはないけど型安全じゃないtry { optinalVar.unwrap(optinalVar => { throw 'なんでも投げることはできるけど' }).else(() => { throw 'それってつまりは' }) } catch (e) { // anyってこと return e as string } return 'anyはあかん'nullのオプショナル型を回避できない
こういうこともできちゃうnew Optinal(null) new Optinal<null>(null)こんなしょうもないことする人はいないと思うのでセーフということで
作ってみた感想
- メリットは、ユーザー定義のType Guardを毎回作らなくていいとこ。
- プロパティ名(メソッド名)だから動くけど
else
って予約語だから大丈夫か不安になる(いい名前が浮かばなかった)