[Swift 4] 配列からグループ化されたDictionaryを生成する
はじめに
モバイルアプリサービス部の中安です。
swift4になってDictonaryに grouping
という初期化の方法が加わったそうです。
ある配列(Sequence)をルールに基づいて辞書でグルーブ化するなどができるというものです。
たとえば
let strings = """ Improved dictionary functionality One of the most intriguing proposals for Swift 4 was to add some new functionality to dictionaries to make them more powerful, and also to make them behave more like you would expect in certain situations. Let's start with a simple example: filtering dictionaries in Swift 3 does not return a new dictionary. Instead, it returns an array of tuples with key/value labels. """.split(separator: " ") let grouped = Dictionary(grouping: strings) { string in string.first ?? "#" } print(grouped["w", default:[]])
長い英文をスペース区切りで文字列の配列にし、"w"で始まる単語だけを出力する・・・というサンプルです。
// 出力結果 ["was", "would", "with", "with"]
やってることは
Dictionary(grouping: strings) { string in string.first ?? "#" }
だけですので、実装はシンプルですが、これだけで簡単に [String : [String]]
の辞書ができあがるというわけです。
どういうことができそうか
こういう型であれば、Groupedなテーブルビューやインデックス付きのテーブルビューなどで力を発揮しそうです。
今回は試しに某RPGの道具の一覧を出す・・・というようなサンプルでやってみたいと思います。
構造体
struct Item { let name: String let price: Int }
名前と値段だけの構造体です。データとしては以下のようなデータがあるものとします。
static let items = [ Item(name: "ひのきの棒", price: 5), Item(name: "はやぶさの剣", price: 25000), Item(name: "こん棒", price: 30), Item(name: "毒針", price: 10), Item(name: "銅の剣", price: 100), : (略) : Item(name: "スタミナの種", price: 90), Item(name: "賢さの種", price: 120), Item(name: "ラックの種", price: 45), ]
ビューコントローラ
画面はシンプルな UITableViewController です。 やってることは特別なことは何もないので説明は割愛します。
class ViewController: UITableViewController { var items = [Int : [Item]]() override func viewDidLoad() { super.viewDidLoad() prepareData() } private func prepareData() { // (後述) } // (これ以外の実装は後述) }
データを準備するところ
安いアイテムは「レベル1」、そこから4段階評価でアイテムのレベルをグループ分けするものとします。
上記ビューコントローラの実装のprepareData()
で実際にデータを用意してみます。
private func prepareData() { items = Dictionary(grouping: Item.items) { item -> Int in switch item.price { case 0..<100: return 0 case 100..<1000: return 1 case 1000..<5000: return 2 default: return 3 } } }
あまり綺麗ではないですが、値段によって4つのグループに分けると処理はこういう感じで書くことができます。 swift3だともうすこし複雑に書くことになりますね。
この結果を画面で確認してみます。
どうでしょうか。
あくまで先頭から順番に走査していった結果なので、どうにも値段がバラバラに並んでます。
これを安い順に並ぶようにするには reduce
をしてやって再定義する感じになると思います。
reduceの際は要素が [Int : [Item]]
という辞書ではなく、 [(key: Int, value: [Item])]
というタプルの配列で来るので注意が必要です。
(下記ソースの変数tuple
)
private func prepareData() { items = Dictionary(grouping: Item.items) { item -> Int in switch item.price { case 0..<100: return 0 case 100..<1000: return 1 case 1000..<5000: return 2 default: return 3 } } .reduce([Int: [Item]]()) { dic, tuple in var dic = dic dic[tuple.key] = tuple.value.sorted { $0.price < $1.price } return dic } }
うまく値段順に揃いました。
最後に
このサンプルを作る際に Playground で LiveView というのを個人的に初めて使ってみました。
参考: [iOS 10] PlaygroundでUIKitの描画を行う
今回のサンプルを LiveView@Playground でパッと貼り付けて動くようにソースを載せておきますので、swift4 の動 くPlayground 環境で触ってみてください。
import UIKit import PlaygroundSupport struct Item { let name: String let price: Int static let items = [ Item(name: "ひのきの棒", price: 5), Item(name: "はやぶさの剣", price: 25000), Item(name: "こん棒", price: 30), Item(name: "毒針", price: 10), Item(name: "銅の剣", price: 100), Item(name: "聖なるナイフ", price: 200), Item(name: "魔道士の杖", price: 1500), Item(name: "刺の鞭", price: 320), Item(name: "くさり鎌", price: 550), Item(name: "鉄の槍", price: 750), Item(name: "鉄の爪", price: 770), Item(name: "鋼鉄の剣", price: 1500), Item(name: "裁きの杖", price: 2700), Item(name: "鉄の斧", price: 2500), Item(name: "大鋏", price: 3700), Item(name: "理力の杖", price: 2500), Item(name: "大金槌", price: 6500), Item(name: "ゾンビキラー", price: 9800), Item(name: "ドラゴンキラー", price: 15000), Item(name: "破壊の剣", price: 45000), Item(name: "王者の剣", price: 35000), Item(name: "アブない水着", price: 78000), Item(name: "布の服", price: 10), Item(name: "旅人の服", price: 70), Item(name: "稽古着", price: 80), Item(name: "皮の鎧", price: 150), Item(name: "甲羅の鎧", price: 300), Item(name: "身かわしの服", price: 2900), Item(name: "くさりかたびら", price: 480), Item(name: "鉄のまえかけ", price: 700), Item(name: "武闘着", price: 800), Item(name: "鉄の鎧", price: 1100), Item(name: "ハデな服", price: 1300), Item(name: "魔法の法衣", price: 4400), Item(name: "鋼鉄の鎧", price: 2400), Item(name: "天使のローブ", price: 3000), Item(name: "魔法の鎧", price: 5800), Item(name: "水の羽衣", price: 12500), Item(name: "ドラゴンメイル", price: 9800), Item(name: "皮の盾", price: 90), Item(name: "青銅の盾", price: 180), Item(name: "鉄の盾", price: 700), Item(name: "水鏡の盾", price: 8800), Item(name: "力の盾", price: 15000), Item(name: "皮の帽子", price: 80), Item(name: "ターバン", price: 160), Item(name: "鉄兜", price: 2000), Item(name: "鉄仮面", price: 3500), Item(name: "薬草", price: 8), Item(name: "毒消し草", price: 10), Item(name: "キメラの翼", price: 25), Item(name: "聖水", price: 20), Item(name: "満月草", price: 30), Item(name: "まだらクモ糸", price: 35), Item(name: "毒蛾の粉", price: 500), Item(name: "消え去り草", price: 300), Item(name: "祈りの指輪", price: 2500), Item(name: "水鉄砲", price: 15), Item(name: "世界樹の葉", price: 3), Item(name: "幸せの靴", price: 75), Item(name: "銀の竪琴", price: 7), Item(name: "命の木の実", price: 150), Item(name: "力の種", price: 180), Item(name: "すばやさの種", price: 60), Item(name: "スタミナの種", price: 90), Item(name: "賢さの種", price: 120), Item(name: "ラックの種", price: 45), ] } class ViewController: UITableViewController { var items = [Int : [Item]]() override func viewDidLoad() { super.viewDidLoad() prepareData() } private func prepareData() { items = Dictionary(grouping: Item.items) { item -> Int in switch item.price { case 0..<100: return 0 case 100..<1000: return 1 case 1000..<5000: return 2 default: return 3 } } .reduce([Int: [Item]]()) { dic, tuple in var dic = dic dic[tuple.key] = tuple.value.sorted { $0.price < $1.price } return dic } } override func numberOfSections(in tableView: UITableView) -> Int { return items.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items[section]?.count ?? 0 } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return "レベル\(section + 1)" } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let reuseIdentifier = "cell" var cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) if cell == nil { cell = UITableViewCell(style: .value1, reuseIdentifier: reuseIdentifier) } let item = self.items[indexPath.section]![indexPath.row] cell!.textLabel?.text = item.name cell!.detailTextLabel?.text = "\(item.price)G" return cell! } } PlaygroundPage.current.liveView = ViewController(style: .grouped)