20200627のSwiftに関する記事は5件です。

TableViewについて

TableViewについて

表題の通りですが、初心者にとってはTableViewはわかりにくいところが多かったので、メモにしておこうと思います。よろしければ、同じような初心者の方も参考にしてみてください。
間違っていたら、お手数ですがお知らせください。

バージョンは下記の通りです。
Xcode 11.5
Swift 5.2.4

TableViewとは

この機能はゲーム等、色々なところで見かけます。
iPhoneのアプリで言うと、メモとかに使われているもので、垂直方向に値等をストックしていく機能を持っています。なので、配列と併せて使うと効果を発揮します。

TableViewを設置する

Storyboard上にTableViewを画面に設置してその上にTableViewCellを重ねるように設置します。文字入力用としてtextfieldも追加します。

  TableView・・・TableViewの機能の土台となる部分
  TableViewCell・・・値を垂直方向に貯めていくセルを生成する部分

スクリーンショット 2020-06-27 21.58.09.png

また、TableViewCellはStoryboardのAttributes indicatorの中にあるidentifierは空白になっているので、ID名を入れる必要があります。

スクリーンショット 2020-06-27 22.59.17.png

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 = self

class 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) -> UITableViewCell

TableViewプログラム記述(関数)

自動で追加された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-27 13.36.57.png

なんか殺風景すぎるけど、ここにセルの高さ指定、イメージ挿入、背景追加、値の保存、タップした時の動きとか、色々機能を追加することで形にはなると思います。

以上です。

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

UITabBarController において選択されたタブに応じた処理をしたい時

先日、 Google Analytics などのイベントトラッキング系の実装をしている時に、
選択されたタブのログを取りたい、ただし現在開いているタブは取らなくていい。」という場面に出会いました。

画面に依らない共通の処理だったので、それぞれの ViewController ではなく UITabBarController に書くことにしました。
簡単な実装かと思いきや、結構奥深い学びがあったので共有します。

  • バッドプラクティスとそれが悪い実装となる理由
  • 最終的な実装例

を紹介します。

2020 年に Swift 始めたばかりの初心者なので、アドバイス・指摘待っています!!!

ざっくりとした結論

始めに、最終的に至った形を示しておきます。
後の理解を深めるためと、時間がない人のためです。

  1. tabBar(_:didSelect:) を使おう
  2. 引数 itemitems 配列でパターンマッチングしよう
  3. 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
        }
    }
}

理由はまだしっかり理解できていませんが (誰が教えて…)、

  1. 個別に tag を設定する必要がある
  2. そしてその tag を参照することで依存が生まれる

からかなーと思っています。

個別に tag を設定する必要がある

ということは、別に左から 0, 1, 2, 3, ... と付けなくても良いわけで…
めちゃめちゃ屁理屈な人が 4, 8, 12, ... とか付けてたら泣きますよね笑

このように特定の UIView インスタンスへの依存を生むような実装は避けるべきです。

最終的な実装例

じゃあどうするかというと、メソッドの引数である itemtabBar.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 }

何をしているかというと、

  1. 引数である item で選択された UITabBarItem を取得して
  2. tabbaritems (UITabBarItem が順番に入った入った箱) の中の何番目かを firstIndex(of:) で調べて
  3. Tab 型に変換する

ということをしています。
最後の型の変換は今回の実装ならではですが、 2 番目までで、選択されたタブの順番が取得できるので、あとはその順番を使って処理を作っていけばいいです。

この実装では UIKitUIView への依存を生んでおらず、 Tab を使って UITabBar をセットアップしていけば後でバグが生まれるということが少ないです。
(仮にバグが生まれても、発見が早い)

まとめ

UITabBarController 初めて触ったのですが、奥深すぎて仲良くなれる気がしません…
(UITabBarControllerUITabBar の違いも分かってない)


追記
本記事公開後、 @lovee さんより、UITabBarControllerUITabBar の違いについてコメント頂いたのでそのまま記載します!!

ちなみに UITabBarUITabBarController の違いはそれぞれの名前とおり、 UITabBar はバー(UIView 継承)で UITabBarController はコントローラー(UIViewController 継承)です


ですが、今回の実装を経験して、「後から保守性が落ちるコード」、「クラッシュ・バグを生みかねないコード」を書かないように意識して実装したいと思いました。
いずれは人のコード読んで「なんか怪しいな」と怪しいコードを 嗅ぎ分けられる ようになりたいです

この記事は自分の備忘録を建前として、 iOS エンジニアの皆様から「もっとこうしたら良いよ!」 「こんな方法もあるよ!」 「その実装だとこんな時危険だよ!!」 という意見を頂戴することを裏の目的としていますので、
アドバイス・指摘バンバンください!

参考

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

[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 さんに教えていただきました。ありがとうございました。

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

iOSのExposureNotificationのAPIをサンプルアプリとドキュメントから見てみる

ExposureNotification概要

ExposureNotificationはCOVID-19への潜在的な曝露を人々に通知します。
ExposureNotificationの機能を実現するにはExposure Notification Serverの実装が必須です。

おことわり

実機ビルドするには Exposure Notification Entitlement Requestで申請する必要があり、ハードルが高いので、
あくまで、公式ドキュメントとサンプルアプリのシミュレータでの確認になっています。
大いに間違っている可能性があるので公式ドキュメントを正としてください。

公式ドキュメント

https://developer.apple.com/documentation/exposurenotification

サンプルアプリ

https://developer.apple.com/documentation/exposurenotification/building_an_app_to_notify_users_of_covid-19_exposure

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が使えない模様。

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

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が取り除かれている
image.png

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 って予約語だから大丈夫か不安になる(いい名前が浮かばなかった)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む