能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

CaseIterableサンプルコードをSwiftUIに置き換える - SwiftUI100行チャレンジ⑨

端的にいうと

Swiftの列挙定義を拡張するにはCaseIterableプロトコルを用いる CaseIterableプロトコルを用するとallCasesプロパティが利用できる UIKitのサンプルコードをSwiftUIに置き換えてみる

1) enum〜case概要

Swiftの列挙機能は他言語と比べて強力な機能(値付き列挙、列挙のネスト, etc)が用意されている。列挙は強力な機能を用意している反面便利な機能については言語の基本仕様の外、protocol 経由での機能拡張、Swift Attributes経由での未知定義対応などでサポートしている。

列挙(enum〜case)

  • 基本機能(値付き列挙、ネスト、静的変数、etc)
  • protocolによる機能拡張(CaseIterable、etc)
  • Swift Attributes(未知定義対応@unknown)

話題がそれるがSwift Attributesは動作環境によってビルドできないかもしれない。例えばApple以外の環境(Linux, Windows10)ではサポートされない定義があるかもしれない。

2) CaseIterable protocol

CaseIterableは列挙に使用できるprotocolで名前が示すとおり列挙定義を数え上げる。enumにCaseIterableを指定するとallCasesプロパティへアクセス可能となる。

enum [定義名]: CaseIterable {
case .定義1
case .定義2
case .定義n
}

CaseIterable protocolの使用方法については以下の投稿を参考にして欲しい。

Listing enum cases using CaseIterable in Swift | by Steven Curtis | The Startup | Medium

3) UIKitのサンプルコードをSwiftUIで記述する

2で紹介したサンプルコードをSwiftUIで書き直してみる。

import SwiftUI
//
// ContentView.swift
// ListSampleUsingCaseIterable
//
// Created by 能登 要 on 2021/04/14.
//
import SwiftUI
enum AnimalStatus: String {
case stable
case angry
case hungry
}
struct Animal {
var id: String
var name: String
var status: AnimalStatus
init(name: String, status: AnimalStatus) {
id = name
self.name = name
self.status = status
}
}
enum Animals: String, CaseIterable, CustomStringConvertible {
case dogs
case elephants
var description: String {
return self.rawValue
}
var items: [Animal] {
switch self {
case .dogs:
return [
Animal(name: "Colin", status: .stable),
Animal(name: "Irwin", status: .stable),
]
case .elephants:
return [
Animal(name: "Ahmed", status: .stable),
Animal(name: "Nasser", status: .stable),
]
}
}
}
struct ContentView: View {
var body: some View {
List {
ForEach(Animals.allCases, id: \.self) { animal in
Section(header: Text(String(describing: animal)) ) {
ForEach(animal.items, id: \.name) { item in
Text(String(describing: item.name))
}
}
.textCase(.none)
}
}.listStyle(PlainListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

サンプルコードを試す場合は、Xcodeを立ち上げ新規プロジェクト(Command + Shift + N)でプロジェクトウィザードを開き、iOS - App - Interface にSwiftUIを選択、Life Cycleは任意のものを選択してプロジェクトを保存したのち、ContentViewにソースコードを貼り付けることで試すことができる。

4) ソースコード解説

元のサンプルコードの列挙定義Animalsを一部追加している。

enum Animals: String, CaseIterable, CustomStringConvertible {
    case dogs
    case elephants
    var description: String {
        return self.rawValue
    }
    var items: [Animal] {
        switch self {
        case .dogs:
            return [
                Animal(name: "Colin", status: .stable),
                Animal(name: "Irwin", status: .stable),
            ]
        case .elephants:
            return [
                Animal(name: "Ahmed", status: .stable),
                Animal(name: "Nasser", status: .stable),
            ]
        }
    }
}

CustomStringConvertible プロトコルを使用してdescriptionプロパティを利用できる形とし、itemsプロパティのアイテム数を増やしている。

UIKitで記述されたコードではUITableView、UITableViewDelegate、UITableViewDataSource を使ったコードをList、Section、Textに置き換えている。

ソースコードのList内で要素を追加する際にForEachを使う。ForEachは複数の記述方法が用意されているがサンプルコードでは、引数にcontent、idを渡す記述方法を使用している。contentに渡すのは配列で、idは配列内の要素を識別するためのKeyPathを指定する。ForEachはソースコード上で2度記述しておりそれぞれ、セクション用、アイテム用となっている。

ForEach定義を仕様する際にSwiftの言語使用に含まれるKeyPath(.self、.name)を渡している。リテラル指定で定義依存が発生している点を留意する場合は、KeyPath自体は型を持った値として扱うことができるのでKeyPathを値として渡すこともできる。

Identifiableプロトコルに対応している構造体や列挙ではForEachでidを省略できる記述を使用できる。アイテム用のForEachについて修正案を示すと。

Animal構造体をIdentifiableに対応

struct Animal: Identifiable {
    var id: String
    var name: String
    var status: AnimalStatus
    init(name: String, status: AnimalStatus) {
        id = name
        self.name = name
        self.status = status
    }
}

ContentViewのアイテム用ForEachを以下の様に書き換えるとidの指定を省略できる。

struct ContentView: View {
    var body: some View {
        List {
            ForEach(Animals.allCases, id: \.self) { animal in
                Section(header: Text(String(describing: animal)) ) {
                    ForEach(animal.items) { item in
                        Text(String(describing: item.name))
                    }
                }
                .textCase(.none)
            }
        }.listStyle(PlainListStyle())
    }
}

5) その他

SwiftUIではListのセクション名がデフォルトで大文字に変換されてしまう。セクション名の大文字変換を無効にするさいはtextCaseモデファイヤーでPlainListStyle()を指定する。

SwiftUI 2nd major releaseではListでOutlineGroupが追加されたがこちらは反復性があるデータ階層を表現する際に使用する。具体的にはフォルダ-サブフォルダの関係の様な入れ子構造を持つデータが対象となる。

参考