この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
モバイルアプリサービス部の中安です。
いよいよ、Swiftが3から4へとメジャーアップデートしました。 それに伴って自分もどんなところが変わったのかというところを調べたり、実際コードに書き起こして試したりしてきました。
これからSwift 4に取り組もうかなという方も多いと思いますので、できるだけサンプル例を示しながら、新しい機能や変更点、それらの使いどころや所感をザッと書いていきます。
数値比較
Swiftは型に厳しい言語ではありますが、Swift 4では整数の数値比較については型の厳しさが緩まりました。
例えば、下記のような比較はSwift 3ではすべてコンパイラがエラーで弾いていましたが、Swift 4では通ります。これは整数型のプロトコル周りの関係性などが見直されたことに起因するそうです。
let intValue: Int = 100
let int32Value: Int32 = 100
let uintValue: UInt = 100
let uint16Value: UInt16 = 100
let uint32Value: UInt32 = 100
if uint16Value == uint32Value { // Swift 3 は型が違うのでエラー
print("一緒だよ!")
}
if intValue == uintValue { // Swift 3 は型が違うのでエラー
print("一緒だよ!")
}
if intValue == int32Value { // Swift 3 は型が違うのでエラー
print("一緒だよ!")
}
if int32Value == uint16Value { // Swift 3 は型が違うのでエラー
print("一緒だよ!")
}
あくまで比較演算がOKであるだけなので、四則演算などは引き続きエラーになります(が、次期Swiftではもしかしたらできるようになるかもという含みは持たされています)。
文字列(String)の強化
コレクション化
Swift 3で打ち切られていた「文字列のコレクション化」が復活しました。つまり、String でも map()
や forEach()
などの機能が使えるということです。
なので、こんなことも
// 1文字ずつ配列にしちゃう
"Hello World".map { String($0) }
こんなこともできちゃいます。
"Hello World".forEach {
// 1文字1文字何かやる
}
もしSwift 3で同じようなことをやるとすればこんな感じでしょうか。
let str = "Hello World"
let arr = (0..<str.characters.count).map { i -> String in
String(str[str.index(str.startIndex, offsetBy: i)])
}
そもそもコレクション化されたので、1文字ずつ配列にするシチュエーションがあるのかどうかはさておいて、随分とシンプルに書くことができます。
文字列長の取得
文字列のコレクション化に伴って、文字列の長さはcount
プロパティで取得できるようになります。
今まではStringを構成するcharacters
の要素数で判定する方法((ちなみに、この方法は不正確なので注意)などが取られていましたが、String自身のカウントをするだけで事足りるように強化されました。
// Swift 3 では
"Hello World".characters.count
// Swift 4 では
"Hello World".count
しかも、取得できる値はUnicodeのいくつかの諸問題も解決した強固なものになっています。これまで絵文字などの特殊文字に苦しめられてきましたが、もうそこに悩む必要はなくなりそうです。
// Swift 3 では
let text = "????"
print(text.characters.count) // 4 (複数の Unicode の合体であるため4となる)
// Swift 4 からは
let text = "????"
print(text.count) // 1 (人の目で見て1文字なので 1 と返る)
文字列を複数行で定義
Swift 4からは、ヒアドキュメントのように複数行にわたって文字列リテラルを定義することができるようになります。
Swift 3ではこのように1行で定義しなくてはいけなかったのですが、
// 横に長くてすいません
let text = "雨ニモマケズn風ニモマケズn雪ニモ夏ノ暑サニモマケヌn丈夫ナカラダヲモチn慾ハナクn決シテ瞋ラズnイツモシヅカニワラッテヰルn一日ニ玄米四合トn味噌ト少シノ野菜ヲタベnアラユルコトヲ"
Swift 4ではこのように複数行にわたって定義できるので、見通しのよいソースになりますね。
let text = """
雨ニモマケズ
風ニモマケズ
雪ニモ夏ノ暑サニモマケヌ
丈夫ナカラダヲモチ
慾ハナク
決シテ瞋ラズ
イツモシヅカニワラッテヰル
一日ニ玄米四合ト
味噌ト少シノ野菜ヲタベ
アラユルコトヲ
"""
上記のように定義した場合、ソース上の改行は文字列の改行として扱われることになります。 そうではなく、文字列上では改行をしたくない場合は``(バックスラッシュ)を書いてやります。
let text = """
雨ニモマケズ
風ニモマケズ
雪ニモ夏ノ暑サニモマケヌ
丈夫ナカラダヲモチ
"""
print(text)
/* 結果
雨ニモマケズ風ニモマケズ
雪ニモ夏ノ暑サニモマケヌ丈夫ナカラダヲモチ
*/
もちろん途中に変数も入れることが可能です。
let season = "夏"
let text = """
雨ニモマケズ
風ニモマケズ
雪ニモ(season)ノ暑サニモマケヌ
丈夫ナカラダヲモチ
"""
Subscript
Swift 4では文字列をsubscriptによって部分取得するという機能まではありません。 しかしながら、前述の通りUnicode問題をクリアしたStringでは、下記のようなextensionを作ってsubscriptを定義してやれば実現できます。
こちらの項目は検証の結果、誤りがありましたので記事を訂正させていただいております。 再検証の内容と訂正につきましては、こちらの記事 に書かせていただきました。
extension String {
subscript (i: Int) -> String {
return String(self[index(startIndex, offsetBy: i)])
}
}
let text = "Hello,World"
print(text[4]) // "o"
インデックスを使ったSubscript
Swift 4では、下記のようにindex(of:)
を使用した便利な文字列の部分取得方法もあります。
let str = "Hello,World Everybody."
let comma = str.index(of: ",")!
let dot = str.index(of: ".")!
let space = str.index(of: " ")!
print(str[..<comma]) // "Hello"
print(str[..<dot]) // "Hello,World Everybody"
print(str[..<space]) // "Hello,World"
ただし、このsubscriptで取得される型はStringではなくSubstringという型で返ってきます。
Stringとは別の型ですが、StringProtocol
というStringと同様のプロトコルに準拠しているため、その違いを意識する必要はあまりないと解説されています。
辞書(Dictionary)の強化
マージ
辞書同士の結合を行うためにmerge
というメソッドが付加されました。
使いどころとしてはjavascriptなどで見られるようなデフォルト設定用の辞書(≒連想配列)に、各設定を詰め合わた辞書をマージして漏れのないオプション用辞書データを作る…などでしょうか(構造体などを使用するほうがベターな気はしますが)。
let defaultOptions: [String : Any] = [
"editEnabled" : false,
"browseEnabled" : false,
"deleteEnabled" : false,
"createEnabled" : false,
"maximumNumberOfArticles" : 10,
]
var options: [String : Any] = [
"browseEnabled" : true,
"maximumNumberOfArticles" : 30,
"textColor" : "#3F3D2B",
]
options.merge(defaultOptions) { optionValue , defaultValue in
return optionValue
}
print(options)
/* 結果
[
"createEnabled": false,
"browseEnabled": true,
"textColor": "#3F3D2B",
"maximumNumberOfArticles": 30,
"editEnabled": false,
"deleteEnabled": false
]
*/
このようにoptions
辞書は、defaultOptions
との差分を上書きした辞書へと変更されます。
クロージャの中で行っていることは何かというと、重複しているキーはoptions
の値を使うのかdefaultOptions
の値を使うのかを選択させています。
(よく考えたら何のキーが重複してるのか取れないと意味がないような気もしますが…)
注意点としては、options自体の値が変わるので、変数はvar
(ミュータブル)で宣言しておく必要があります。
Subscriptのデフォルト値設定
通常、辞書の値を取り出す時は
let dic = ["hoge": 1, "figa": 2, "piyo": 3]
let hoge = dic["hoge"]
としてやれば、OptionalなInt値が返却されてきます。
Swift 4では指定のキーで値が取れなかった時のデフォルト値を設定してやることができます。
書き方は以下の通りです。
let dic = ["hoge": 1, "figa": 2, "piyo": 3]
let hoge = dic["hoge", default: 0]
これで辞書に"hoge"
というキーがなければ0が返却されます。この値は非OptionalなInt型です。
ただし、こういう使い方だけであれば、下記の方法と何が違うのかはよくわかりません。
let dic = ["hoge": 1, "figa": 2, "piyo": 3]
let hoge = dic["hoge"] ?? 0
しかし、下記のような「辞書にキーがなければ作られる…」といった使い方であれば、この新機能の恩恵は受けられそうです。
// "hello world"で使われるアルファベットの数を数える
var dic: [Character: Int] = [:]
for char in "hello world" {
dic[char, default: 0] += 1
}
// 結果: ["w": 1, "r": 1, "e": 1, "o": 2, "l": 3, " ": 1, "d": 1, "h": 1]
配列から連番を振った辞書を作る
辞書のイニシャライザにinit(uniqueKeysWithValues:)
が追加され、配列に対して連番を簡単に振ることができるようになりました。
let students = [
"潮田渚", "赤羽業", "茅野カエデ", "磯貝悠馬", "岡島大河", "岡野ひなた",
"奥田愛美", "片岡メグ", "神崎有希子", "堀部糸成", "寺坂竜馬", "村松拓也",
"吉田大成", "狭間綺羅々", "倉橋陽菜乃", "中村莉桜", "速水凛香"
]
let dic = Dictionary(uniqueKeysWithValues: zip(1..., students))
/* 結果
[
12: "村松拓也", 17: "速水凛香", 10: "堀部糸成", 14: "狭間綺羅々",
5: "岡島大河", 7: "奥田愛美", 15: "倉橋陽菜乃", 3: "茅野カエデ", 11: "寺坂竜馬",
13: "吉田大成", 16: "中村莉桜", 2: "赤羽業", 4: "磯貝悠馬", 9: "神崎有希子",
6: "岡野ひなた", 1: "潮田渚", 8: "片岡メグ"
]
*/
あまり使いどころは浮かばないですが、こういうこともできます…という感じですね。
MapValues
すでに配列のようなコレクションにはmap
という機能が備わっていますが、辞書のようなハッシュコレクションに対しては、新たにmapValues
が加えられました。
平たく言えば、「キーと値のうち、値に対してmapのような挙動をさせる」といったところでしょうか。ループをまわして複雑な処理を書かずとも、値に何かしらの処理を与えた辞書データ生成が可能です。先の例で作った辞書を使って例を示します。
// 先程の例
let students = [
"潮田渚", "赤羽業", "茅野カエデ", "磯貝悠馬", "岡島大河", "岡野ひなた",
"奥田愛美", "片岡メグ", "神崎有希子", "堀部糸成", "寺坂竜馬", "村松拓也",
"吉田大成", "狭間綺羅々", "倉橋陽菜乃", "中村莉桜", "速水凛香"
]
let dic = Dictionary(uniqueKeysWithValues: zip(1..., students))
let dic2 = dic.mapValues { value in
"(value)さん"
}
print(dic2)
/* 結果
[
12: "村松拓也さん", 17: "速水凛香さん", 10: "堀部糸成さん", 14: "狭間綺羅々さん",
5: "岡島大河さん", 7: "奥田愛美さん", 15: "倉橋陽菜乃さん", 3: "茅野カエデさん",
11: "寺坂竜馬さん", 13: "吉田大成さん", 16: "中村莉桜さん", 2: "赤羽業さん",
4: "磯貝悠馬さん", 9: "神崎有希子さん", 6: "岡野ひなたさん", 1: "潮田渚さん", 8: "片岡メグさん"
]
*/
辞書の値すべてに「〜さん」という敬称をつけてみました。このようにキーと値の関係は変わらずに値だけに変化を与えた辞書を生成することができます。
配列からグループ化した辞書を作る
配列などコレクションの要素からグループ化された辞書データをinit(grouping:by:)
というイニシャライザを使って作成できます。この機能は例えばテーブルビューでセクションごとにデータを分けて表示するために役に立つかもしれません。以下に例を挙げます。
enum Era: String {
case ancient = "古代"
case medieval = "中世"
case modern = "近世"
}
struct HistroicalPerson : CustomStringConvertible {
let name: String
let era: Era
var description: String {
return name
}
}
let persons = [
HistroicalPerson(name: "織田信長", era: .medieval),
HistroicalPerson(name: "坂本龍馬", era: .modern),
HistroicalPerson(name: "聖徳太子", era: .ancient),
HistroicalPerson(name: "豊臣秀吉", era: .medieval),
HistroicalPerson(name: "西郷隆盛", era: .modern),
HistroicalPerson(name: "中臣鎌足", era: .ancient),
HistroicalPerson(name: "徳川家康", era: .medieval),
]
let grouped = Dictionary(grouping: persons, by: { $0.era.rawValue })
print(grouped)
// 結果:
// ["中世": [織田信長, 豊臣秀吉, 徳川家康], "近世": [坂本龍馬, 西郷隆盛], "古代": [聖徳太子, 中臣鎌足]]
歴史上の人物を時代区分ごとにグループ化できていることがわかると思います(時代区分の正確性は目をつぶってください)。
返ってくる辞書も [String : [HistroicalPerson]]
という型で返ってくるので非常に扱いやすいですね。
同じようなことを従来の方法で行うためには少なくとも何行かのコードを各必要があったとは思いますが、随分とコーディング量を減らすことができそうです。
filterの挙動の変更
下記のコードは、前項で作成した「時代ごとに歴史上の人物を分けた辞書データ」を元に、「世」という字が含まれる時代区分だけを抽出するというサンプルです。 (サンプルとしては意味がよく分からないですが…)
let dict = ["中世": ["織田信長", "豊臣秀吉", "徳川家康"], "近世": ["坂本龍馬", "西郷隆盛"], "古代": ["聖徳太子", "中臣鎌足"]]
let filtered = dict.filter {
$0.key.contains("世")
}
print(filtered)
こうすると、「古代」は省かれて「中世」と「近世」だけが変数filtered
に抽出されるようになると思いますが、Swift 3では下記のような値が返却されます。
// Swift 3 での戻り値
[(key: "中世", value: ["織田信長", "豊臣秀吉", "徳川家康"]), (key: "近世", value: ["坂本龍馬", "西郷隆盛"])]
辞書をフィルタしたのに戻り値はタプルの配列です。よく考えてみれば気持ち悪い挙動ですね。フィルタしたものをもう一度辞書に作り直す必要すら出てきます。
しかし、Swift 4では、同じソースコードを実行しても結果が変わります。
// Swift4での戻り値
["中世": ["織田信長", "豊臣秀吉", "徳川家康"], "近世": ["坂本龍馬", "西郷隆盛"]]
抽出元の辞書と同じ型の辞書が返ってきました。ごく自然な挙動ですね。
Reduceの強化
コレクションの要素を1つの変数に還元していく配列操作reduce
に新機能が加わりました。
そもそもreduce
は、こんな感じで使用します。
(reduceという言葉って「減らす」って意味が強すぎて、人に「reduceとは」って説明しづらいですよね…)
// 1から9までの数字を全部足した数を求める
let sum = [1, 2, 3, 4, 5, 6, 7, 8, 9].reduce(0) { result, number in
return result + number
}
print(sum) // 45
// もう少しシンプルに書くとこんな感じ
let sum = (1...9).reduce(0) { $0 + $1 }
本来ならforなどで回して書くところを省略形で書けるのがSwiftライクでいいところですが、今までのreduceは上記例でいうresult
に当たるところがインミュータブルであったことから、複雑なことをしようとするとパフォーマンスコストがかかるなど、複雑な記法になりがちでした。
例えば次のような例です。
let increasedIntegers = [3, 1, 2, 3, 4, 5, 4, 6, 7, 3, 8, 9].reduce([Int]()) { result, number in
if (result.last ?? 0) < number {
result.append(number)
}
}
print(increasedIntegers)
これは「不規則に並んだ整数の配列を順番に走査して、次に大きな数があったら配列に入れる」というプログラムです。
3から始まって、次に大きな数字の4が入り、その次の5…という流れで、期待値は[3, 4, 5, 7, 8, 9]
となります。
本来は初期値である[Int]()
はresult
に入ってるので、これで上手くいきそうですが残念ながらエラーになります。
result
はvar
ではなくlet
扱いであり、すなわちインミュータブルな配列だからです。
インミュータブルな配列はappend()
メソッドを有していません。
ただ、直感的にはappend()
が可能であってほしいですよね。
Swift 4では還元される変数をinout扱いするためのreduceの機能が増えました。into
ラベルを付けることでそれが実現できます。
let increasedIntegers = [3, 1, 2, 3, 4, 5, 4, 7, 3, 8, 9].reduce(into: [Int]()) { result, number in
if (result.last ?? 0) < number {
result.append(number)
}
}
print(increasedIntegers)
この書き方で前述のエラーは消えます。変更したのはreduce([Int]())
をreduce(into: [Int]())
にしただけです。これでresult
はミュータブルな配列となり、append()
をすることができるようになりました。
キーパス
KVOなどで使用するプロパティのキーを、Swift 4では(クラス名).(プロパティ)...
というパス形式で指定することができるようになりました。
今までは文字列ベースで指定していましたが、これでXcodeの補完も効きますし、スマートな感じになります。
例を挙げてみます。
struct Weapon {
let name: String
let strength: Int
}
struct Hero {
let name: String
let hitPoint: Int
let magicPoint: Int
let weapons: [Weapon]
}
struct Enemy {
let name: String
let hitPoint: Int
}
let steelSword = Weapon(name: "鋼の剣", strength: 20)
let yoshihiko = Hero(name: "勇者ヨシヒコ", hitPoint: 90, magicPoint: 12, weapons: [steelSword])
print(yoshihiko[keyPath: Hero.name]) // "勇者ヨシヒコ"
print(yoshihiko[keyPath: Hero.weapons.first!.name]) // "鋼の剣"
こういう使い方であれば、普通にyoshihiko.name
でアクセスすればよいのですが、最初に書いたようにKVOなどの場面では有効そうです。
class Hero: NSObject {
let name: String
@objc dynamic var hitPoint: Int
init(name: String, hitPoint: Int) {
self.name = name
self.hitPoint = hitPoint
super.init()
}
}
class Enemy: NSObject {
let name: String
@objc dynamic var hitPoint: Int
init(name: String, hitPoint: Int) {
self.name = name
self.hitPoint = hitPoint
super.init()
}
func attack(to hero: Hero) {
hero.hitPoint -= 5
}
}
let yoshihiko = Hero(name: "勇者ヨシヒコ", hitPoint: 90)
let slime = Enemy(name: "スライム", hitPoint: 10)
yoshihiko.observe(Hero.hitPoint, options: [.old, .new]) { me, change in
let diff = (change.newValue ?? 0) - (change.oldValue ?? 0)
let isDamaged = diff < 0
print("(me.name)は、HPを(abs(diff))(isDamaged ? "消費した" : "回復した" )")
}
// スライムの攻撃!
slime.attack(to: yoshihiko) // "勇者ヨシヒコは、HPを5消費した"と出力される
キーパスへの追加
キーパスは appending()
をしてやることによって、動的にパスを加えてやることもできます。
struct Weapon {
let name: String
let strength: Int
}
struct Hero {
let name: String
let hitPoint: Int
let magicPoint: Int
let weapon: Weapon
}
let steelSword = Weapon(name: "鋼の剣", strength: 20)
let yoshihiko = Hero(name: "勇者ヨシヒコ", hitPoint: 90, magicPoint: 12, weapon: steelSword)
let keyPath1 = Hero.weapon
let keyPath2 = keyPath1.appending(path: .name) // 名前(name)を追加
print(yoshihiko[keyPath: keyPath1]) // "Weapon(name: "鋼の剣", strength: 20)"
print(yoshihiko[keyPath: keyPath2]) // "鋼の剣"
Codable
WebAPIなどでJSONから構造体を作ったり、また逆にJSONを作ったりという場面がアプリ開発ではよくあります。しかしながら、その実装はサードパーティのライブラリに頼ったりすることが多かったかと思います。今回追加されたCodable
という優秀なプロトコルの登場によって、純製の(非ライブラリ依存な)方法でJSON解析が楽に行うことができそうです。
Codableの実装例は下記のような感じです。
struct AnimeCharacter: Codable {
let name: String
let job: String
let friends: [String]
}
let jsonString = "{"name":"ルパン","job":"泥棒","friends":["次元","五右衛門","不二子"], "specialty":"変装"}"
let animeCharacter = try! JSONDecoder().decode(AnimeCharacter.self, from: jsonString.data(using: .utf8)!)
/* 結果
AnimeCharacter(name: "ルパン", job: "泥棒", friends: ["次元", "五右衛門", "不二子"]),
*/
例にあるJSON文字列は少し長くて見えづらいかもしれませんが、アニメキャラクターの「名前」と「仕事」「仲間」「特技」が書かれています。
そしてこのJSONからAnimeCharacter
という構造体のオブジェクトを生成しようとしているのですが、AnimeCharacterをCodableに準拠させるだけで、ご覧のようにわずか1行でそれが実現できます。
(当然エラーケースを考慮すればtryあたりはもう少し何かしないといけませんが…)
また、AnimeCharacter構造体には「特技」であるspecialty
がプロパティにありません。
そのあたりも直感的に分かりやすくデコーダは無視してくれます。
ほかにも、JSONがオブジェクト単体ではなく、配列として書かれている場合もあります。 その場合は下記のように定義すればよいです。
struct AnimeCharacter: Codable {
let name: String
let job: String
let friends: [String]
}
let jsonString = "[{"name":"ルパン","job":"泥棒","friends":["次元","五右衛門","不二子"]}, {"name":"のび太","job":"小学生","friends":["ドラえもん","しずか","スネ夫","ジャイアン"]}]"
let animeCharacters = try! JSONDecoder().decode([AnimeCharacter].self, from: jsonString.data(using: .utf8)!)
/* 結果
[
AnimeCharacter(name: "ルパン", job: "泥棒", friends: ["次元", "五右衛門", "不二子"]),
AnimeCharacter(name: "のび太", job: "小学生", friends: ["ドラえもん", "しずか", "スネ夫", "ジャイアン"])
]
*/
逆にJSONに変換する方法や、オブジェクトをネスト化する方法など色々あるのですが、それはまた別記事で書こうと思います。
それにしても随分と見通しよくデータを構造化できるようになったなと思います。
スワッピング(配列要素の入れ替え)
今までは、配列の要素の順番を入れ替えたい時にはswap
という関数を使用していました。
例えばインデックスが1と3の要素を入れ替える場合は、
var vegetables = ["だいこん", "にんじん", "レタス", "きゅうり", "たまねぎ", "キャベツ", "小松菜"]
swap(&vegetables[1], &vegetables[3])
print(vegetables)
/* 結果
["だいこん", "きゅうり", "レタス", "にんじん", "たまねぎ", "キャベツ", "小松菜"]
*/
Swift 4からは、この書き方は非推奨となるようです(むしろエラーになります)。
それに代わるものとしてミュータブルな配列にはswapAt()
というメソッドが付加されています。
/// 定義はこんな感じ
public struct Array<Element> : RandomAccessCollection, MutableCollection {
public mutating func swapAt(_ i: Int, _ j: Int)
}
これを使うことによって
var vegetables = ["だいこん", "にんじん", "レタス", "きゅうり", "たまねぎ", "キャベツ", "小松菜"]
vegetables.swapAt(1, 3)
print(vegetables)
/* 結果
["だいこん", "きゅうり", "レタス", "にんじん", "たまねぎ", "キャベツ", "小松菜"]
*/
こんなにシンプルになりました。
もちろんですが、mutatingなメソッドなのでvegetables
はlet
で宣言してはいけません。
ジェネリクスを使ったSubscript
Swift 4からは、subscriptにジェネリクスを使用することができるようになったそうです。よい使いどころはうまく説明できませんが、かゆいところに手が届くようなデータアクセスができるかもしれません。
例を示します。
struct TVStation {
private let data: [String : Any]
init(_ data: [String : Any]) {
self.data = data
}
}
let dataArray: [[String : Any]] = [
[
"name" : "日本テレビ",
"channel" : 4,
"latitude" : 35.6644039,
"longitude" : 139.7590881,
],
[
"name" : "TBS",
"channel" : 6,
"place": "赤坂",
"latitude" : 35.6721769,
],
[
"name" : "フジテレビ",
"channel" : 8,
"place": "台場",
"latitude" : 35.62527,
"longitude" : 139.7720714,
],
[
"name" : "テレビ朝日",
"place": "六本木",
"latitude" : 35.6577552,
"longitude" : 139.7282176,
],
]
let tvStations = dataArray.map { TVStation(data: $0) }
東京キーの主な民放テレビ局のデータが辞書になっていて、そこからTVStation
という構造体を作成しています。ただし、それぞれの辞書はところどころキーが抜けているような不安定なデータであるものとします。
(スクレイピングしてきたデータや、RSSなどから拾ったデータなどと思ってもらえればよいです)
TVStation構造体は、元の辞書データがどんな構造になっているかが不明なので、具体的なプロパティは定義していません。
そこで、データへのアクセスはsubscriptを使ってキーを文字列で指定するような挙動にしようと思います。ただ、型だけはどうにか担保したいなというときに下記のような改修が考えられます。
// 改修版
struct TVStation {
// (イニシャライザなどは省略)
subscript<T>(key: String, defaultValue: T) -> T {
return (data[key] as? T) ?? defaultValue
}
}
キーを指定して、そのキーがなければdefaultValue
が返るという仕組みです。
defaultValue
でT
の型を固めることができるのがミソです。
このような手法はSwift 3では不可能でした。Swift 4からはジェネリクスを使用することによって戻り値に汎用性が高めることができますね。
下記が使用例になります。
tvStations.forEach { tvStation in
let channel = tvStation["channel", -1]
let name = tvStation["name", "(不明)"]
let place = tvStation["place", "(不明)"]
print("(name)((channel)チャンネル)は、(place)にある")
}
/* 結果
日本テレビ(4チャンネル)は、(不明)にある
TBS(6チャンネル)は、赤坂にある
フジテレビ(8チャンネル)は、台場にある
テレビ朝日(-1チャンネル)は、六本木にある
*/
channel
はデフォルトを-1
に指定しているので、Intが確実に返ってきます。
例でいうところのテレビ朝日にはchannel
キーが存在しないので、デフォルト値が返ってることも確認できると思います。
日本テレビのplace
キーについても同様です。つまりは、下記のように
let channel: String = tvStation["channel", -1] // エラー!
と、することはできないことが簡単に保証できているというわけです。
注意点としては、型推論が危ういものに関しては、やはり型は受け皿側で指定しておくべきだということですね。
例えば下記のように、
let lat = tvStation["latitude", 0]
緯度のデフォルト値をIntにしているがゆえに、変数lat
には常に0が入ります。
let lat: Double = tvStation["latitude", 0]
受け側変数の型を指定しておいてやれば、緯度は意図したとおりに取得できるようになります。
プロトコル指定
Swift 4では「何かのプロトコルに準拠した何かのオブジェクト」という指定が非常にシンプルになりました。
クラス & プロトコル
という記法です。すごくシンプルな書き方ができて、かつわかりやすいという良い変更だと思います。
例えばこういう感じです。
protocol Cloasable {
}
extension Cloasable {
func close() {
// 何か処理
}
}
class HogeViewController: UIViewController, Cloasable {
}
class FugaViewController: UIViewController {
}
var arr = [UIViewController & Cloasable]()
arr.append(HogeViewController())
arr.append(FugaViewController()) // Cloasableに準拠してないのでエラー!
このように、arr
にはCloasable
というプロトコルに準拠したUIViewController
しか入れることはできません。
なのでFugaViewController
はコメントにあるように、この配列の要素に成り得ません。
あとはclose()
というメソッドを持っていることが保証されているので、下記のように安心してメソッドを呼び出せるというわけです。
arr.forEach {
$0.close()
}
もちろん準拠するプロトコルは1つだけとは限りません。下記のように複数指定もできます。
protocol Cloasable {
}
extension Cloasable {
func close() {
// 何か処理
}
}
protocol Openable {
}
extension Openable {
func open() {
// 何か処理
}
}
class HogeViewController: UIViewController, Cloasable, Openable {
}
let viewController: UIViewController & Cloasable & Openable = HogeViewController()
viewController.open()
viewController.close()
片側のみの区間指定
Swiftには元々値の区間を指定するための「区間演算子」なるものが存在します。 名前は知らなくても見たことはあると思います。
- 閉区間 ・・・
1...3
=> 1, 2, 3 - 半開区間 ・・・
1..<3
=> 1, 2
let alphabets = "abcdefghijklnmopqrstuvwxyz".map {$0}
print(alphabets[10...20])
// ["k", "l", "n", "m", "o", "p", "q", "r", "s", "t", "u"]
print(alphabets[10..<20])
// ["k", "l", "n", "m", "o", "p", "q", "r", "s", "t"]
細かくいうと使う値の型によって更に分類できるのですが、大きくはこの2つが存在しています。 Swift 4では、さらに新たな区間演算子が加わることになりました。それが「片側だけの区間指定」です。
この区間指定を使用すると、以下のような感じになります。
let alphabets = "abcdefghijklnmopqrstuvwxyz".map {$0}
print(alphabets[...10])
// ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"]
print(alphabets[..<10])
// ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
print(alphabets[10...])
// ["k", "l", "n", "m", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
このように配列の最初や最後などの指定を明示して書き添えることなく、直感的な書き方で「先頭から」や「末尾まで」を表現できます。
では、これらの正体は一体何なのでしょうか。 実際に型を文字列にして見てみると以下のようになります。
print( String(describing: type(of: (...10))) )
print( String(describing: type(of: (0...10))) )
// PartialRangeThrough<Int>
// CountableClosedRange<Int>
print( String(describing: type(of: (..<10))) )
print( String(describing: type(of: (0..<10))) )
// PartialRangeUpTo<Int>
// CountableRange<Int>
print( String(describing: type(of: (10...))) )
print( String(describing: type(of: (10...Int.max))) )
// CountablePartialRangeFrom<Int>
// CountableClosedRange<Int>
Partial
という名前のついた従来の「閉区間」と「半開区間」とはまったく別の型であることがわかります。
つまりは...10
と 0...10
は、配列の指定範囲で使うには同じ挙動になりますが、別物であることも留意する必要があります。
このあたりは0...9
と0..<10
が同じ挙動でも異なる型であるSwift 3までの仕様と同じかと思います。
それでは、色々と使用例を挙げます。
infix operator ^*
func ^*(lhs: UInt32, rhs: UInt32) -> UInt32 {
return UInt32(pow(Double(lhs), Double(rhs)))
}
func string(ofFileSize fileSize: UInt32) -> String {
switch fileSize {
case ..<1024:
return "(fileSize)bytes"
case 1024..<(1024^*2):
return "(fileSize / 1024)KB"
case (1024^*2)..<(1024^*3):
return "(fileSize / (1024^*2))MB"
case (1024^*3)..<(1024^*4):
return "(fileSize / (1024^*3))GB"
case (1024^*4)...:
return "(fileSize / (1024^*4))TB"
default:
return "--"
}
}
print(string(ofFileSize: 2982922)) // 2MB
上記の例は「整数値を与えると適切なファイルサイズ文字列を返す関数」のサンプルです。 このように、switch文などの判定用に使用するのが一番浮かびやすいかと思います。
続いて例を挙げます。
let str = "Hello,World"
let comma = str.index(of: ",")!
print(str[..<comma]) // "Hello"
print(str[...comma]) // "Hello,"
上記は「文字列の強化」でもすでに出しましたが、文字列のインデックスに対する区間を指定しています。
print(str[str.startIndex..<comma]) // "Hello"
Swift 3までは上記のようにするわけですが、その必要がなくなったということです。
まだ色々とありますが、最後の例を挙げます。
let alphabets = "ABCDEFGHIJK".map{$0}
let dict = Dictionary(uniqueKeysWithValues: zip(1..., alphabets))
print(dict)
/* 結果
[11: "K", 10: "J", 2: "B", 4: "D", 9: "I", 5: "E", 6: "F", 7: "G", 3: "C", 1: "A", 8: "H"]
*/
上記の例は「辞書の強化」でも既に出したものです。zip()
関数との組み合わせで連番をキーとした配列から辞書を作成します。
新しい区間演算子が「指定した整数〜整数の最後まで」ではなく、「指定した整数〜最後の要素まで」であることが分かる好例ではないかと思います。
なお、新しい区間演算子でループを作る場合は注意が必要です。
(...10).forEach() { // エラー!
print($0)
}
(10...).forEach {
print($0) // 無限ループ
}
上記のように、終点だけを指定した場合はforEach
がそもそも備わっていないのでエラーになります。また、直感的に見ればわかるところではありますが、始点だけを指定している場合は無限ループのような動作になります。
スコープの変更
Swift 4ではfileprivate
スコープとprivate
スコープの概念が見直され、「同一ファイル内の同一クラス(または構造体など)はprivateの値にアクセスできる」というルールが追加されました。
今までは
class File {
fileprivate let path: String
init(path: String) {
self.path = path
}
}
extension File: Equatable {
static func ==(lhs: File, rhs: File) -> Bool {
return lhs.path == rhs.path
}
}
このようにextension側からはfileprivate
スコープの値しかアクセスすることができませんでしたが、
class File {
private let path: String
init(path: String) {
self.path = path
}
}
extension File: Equatable {
static func ==(lhs: File, rhs: File) -> Bool {
return lhs.path == rhs.path
}
}
Swift 4ではこのようにprivate
の値にもアクセスすることができます。
もちろん、下記のように別のクラスからは同一ファイル内でもprivate
の値はアクセスできません。
class File {
private let path: String
init(path: String) {
self.path = path
}
}
class FileDeleter {
func delete(file: File) {
try! FileManager.default.removeItem(atPath: file.path) // エラー!!
}
}
同様に、同一クラスでも別ファイル同士であればアクセスできません。
「拡張指向の言語」であるSwiftにおいては、extensionを増やすたびにfileprivate
が量産されてしまい、本来の隠蔽性とはなんだか違和感のある形になっていたので、よい変更だと思います。
Objective-Cランタイムとの関係性
Swift 4になり、Objective-Cランタイムとの関係性が変更されたようです。
まずは例を挙げます。
class EventListener: NSObject {
func didTapButton(_ sender: UIButton) {
// ボタン押されて何かする
}
}
let listner = EventListener()
let button = UIButton()
button.addTarget(listner, action: #selector(EventListener.didTapButton(_:)), for: .touchUpOutside)
こういった実装の場合は、Swift 3までは普通にコンパイラが通してくれます。 しかし、Swift 4からはコンパイラは次のようにエラーを返します。
Argument of '#selector' refers to instance method 'didTapButton' that is not exposed to Objective-C
短く訳すと「セレクタが Objective-C(ランタイム)には公開されていない参照を見てますよー」とのことです。
この解釈はObjective-C時代の知識や、ランタイムとの関係性などを把握する必要があり、説明が長くなるので別途どこかで書き記せればと思いますが、NSObject継承クラスのメソッドであればセレクタは Objective-C の仕組みを使って参照(正しくは推論?)していたのだけれど、 そういう挙動はやめた!という感じでしょうか。 (文章にするとややこしいですね)
この変更によって今まで開発で使っていた書き方から変えざるをえない部分もありますが、 パフォーマンス向上を図られることになるらしいです。(少し難しい話だったので割愛)
さて、では上記例のような場合はどうやって解決させるのか…ですが、
以前からある@objc
アノテーションを使用するパターンと、新たにクラス全体をObjective-Cに公開するための@objcMembers
アノテーションを使用するパターンとがあります。
class EventListener: NSObject {
@objc func didTapButton(_ sender: UIButton) {
// ボタン押されて何かする
}
}
@objcMembers
class EventListener: NSObject {
func didTapButton(_ sender: UIButton) {
// ボタン押されて何かする
}
}
Swift 3から4にマイグレーションする場合に「このクラスは Objective-C ランタイムに解釈されるのが前提なクラスなのだ」というものであれば、@objcMembers
アノテーションでまかなう選択肢もアリかなあとは思います。
(例えばKVOなどでdynamicな変数を使用しているクラスなどがそうです)
まとめ
主なSwift 4の新機能・変更は以上です。まだまだ新機能や変更があるので、今記事ですべてを網羅できているわけではありません。
もう少し詳しくという方は、AppleのWWDCでの発表動画や、こちらなどを見てみるのもいいかもしれませんね。
Swift 4になったことで、例えば文字列の強化などにおいて「人が自然に感じるものが自然に動く」というような変更が多かったように思えます。2から3にあったような命名規則すらも飲み込むような破壊的な変更も少なく、マイグレーションもうまくやればスムーズかと思いました。
駆け足気味に箇条書きしたので至らない部分もあるかもしれませんが、これから試される方は実際に動かしてみて便利になった部分を体験してみてください。
以上です。