cockscomblog?

cockscomb on hatena blog

Swift 2.2

寒さも和らぎ、日によっては春の訪れを感じさせる今日この頃、いかがお過ごしでしょうか。春といえば Swift です。Swift は春と秋に、まるで衣替えのように大きなリリースがあります。2016年の春と予告されていた Swift 2.2 は、おそらく来週には正式にリリースされるものと思われます3月22日にリリースされました

Swift 2.2 は、バグの修正や警告や診断の改善、コンパイル時間や実行速度の向上が主目的であるとされ、それに加えて Swift 2.0 以来のちょっとした機能向上を図ってのリリースとなります。Swift2.2 は OSS となった Swift の初めてのバージョンアップでもあります。すなわちコミュニティからの直接的なフィードバックを経た、最初の Swift と言えるでしょう。そんな Swift 2.2 の変更から主だった(おもしろい)部分を紹介します。

春に備えて準備をはじめましょう!

class の failable initializer でより早く失敗できる

Swift 2.1 では、class の failable initializer (init?()init() throws) に制限がありました。

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Swift 2.1 までの The Swift Programming LanguageFailable Initializers に記載されていたのこの制限により、stored property を全て初期化した後じゃなければ return nilthrow ができませんでした。

Swift 2.2 ではこれが解消され、より早いタイミングで失敗できるようになりました。struct など value type ではできていたことではありますが、class で利用できると便利なことが多いでしょう。

Ref.

#if swift(>=2.2)

#ifSwift のバージョンによって分岐させられるようになりました。当てはまらない方はコンパイラに解釈されないので、エラーや警告も発生しません。この機能により、複数Swift のバージョンに対応したコードを書けるようになります。特にライブラリの開発で役に立つでしょう。

#if swift(>=2.2)
    print("Swift 2.2!")
#else
    // コンパイラによって解釈されない
#endif

Swift 2.2 以降で使えることで、例えば Swift 3.0 と 2.2 のふたつのバージョンでコンパイルできるコードが書けるようになります。しかし Swift 3.0 では大きく API が変更される予定なので、分岐によって分けることがどれくらい現実的かは未知数ではあります。

Ref.

関数を参照する際に引数ラベルまで指定できる

Swift では関数を簡単に参照することができますが、引数ラベルの名前が違う以外はすべて同じシグネチャを持つ関数があった場合でも、引数ラベルまで利用して参照できるようになりました。

class NiceClass {
    func awesomeMethod(x: String, some: String) {}
    func awesomeMethod(x: String, another: String) {}
}
let nice = NiceClass()

let some = nice.awesomeMethod(_:some:)
let another = nice.awesomeMethod(_:another:)

この例のように書くことができます。Swift 2.1 までは nice.awesomeMethod としか書けなかったので、このような状況では曖昧さが解決できませんでした。UIView.insertSubview(_:aboveSubview:)UIView.insertSubview(_:belowSubview:) のように、現実的にもこのようなメソッドが存在していたので、嬉しい変更と言えるでしょう。もしオーバーロードによって型が違うだけのシグネチャがあるときは、型コンテキストを作って型推論に頼らずに書くことで、曖昧さが解決できます。

また関数のほかイニシャライザでも同様です。

Ref.

#selector

Selector というのは Objective-Cメソッドを指すデータで、target-action パターンなどの動的なメッセージパッシングに利用されてきました。Swift においても Objective-C の言語機能を利用して設計されたフレームワークを使用する都合上、この selector を作ることができ、Selector 型のインスタンスとして表現されています。SelectorStringLiteralConvertible として定義されていたため、多くの場合は Selector 型のコンテキストで単なる文字列リテラルとして書かれてきました。

Swift 2.2 からは、Selector の作り方が変更され、#selector 式が導入されます。この式の導入により、Swift の関数を参照するように Selector を生成できます。

let sel = #selector(MyViewController.doSomething(_:))

このとき MyViewController.doSomething(_:) は、@objcObjective-C 用の別名をつけられていても関係なく、Swift 内での表現が利用できます。また SE-0021 と合わせて、引数ラベルまで指定できます。さらにオーバーロードによって引数ラベルまで指定しても曖昧な場合は、as を使って関数の型を記述することができ、例えば #selector(MyViewController.doSomething(_:) as (Int) -> Void) のようにも記述できます。

メソッドのレシーバ (例における MyViewController に当たる部分) はクラスでもインスタンスでもよく、あるいは self でも問題ありません。self の場合は省略もできます。

新しい #selector 式では、独自の構文の導入によってコンパイラによるチェックが可能になりました。存在しないメソッドを指そうとしているとコンパイラがエラーを出します。あるいは Objective-C 側から参照できないメソッドの場合にもエラーとなります。このことでより安全に利用できるようになりました。

Ref.

associatedtype

protocol の associated type を作るとき、Swift 2.1 までは typealias キーワードを使うことになっていましたが、Swift 2.2 からは廃止予定となり、新たに associatedtype キーワードを利用することになります。Swift 3.0 からは associated type を作る目的での typealias は廃止されます。

protocol CollectionType : Indexable, SequenceType {
    associatedtype Generator : GeneratorType = IndexingGenerator<Self>
    func generate() -> Self.Generator
    ...
}

typealias キーワードには他にも用途があり、紛らわしいので新たにキーワードが用意された格好です。protocol に準拠する型で associatedtype に対応する型を与えるときは、これまでと変わらず typealias キーワードを利用します。

Ref.

#file, #line, #column, #function

Swift にはソースコードの現在地を示す識別子 (magic identifier) があり、Swift 2.1 までは __FILE__, __LINE__, __COLUMN__, __FUNCTION__ というように、C のマクロと似たフォーマットが利用されてきました。Swift 2.2 からはこれらは廃止予定とされ、それぞれ #file, #line, #column, #function という # でプリフィックスされた新たな識別子が与えられました。Swift 3.0 では旧来のスタイルは完全に廃止されます。

Swift 2.2 以降 Swift 2.1 以前 説明
#file __FILE__ ファイル名
#line __LINE__ 行番号
#column __COLUMN__ 列番号
#function __FUNCTION__ 関数宣言名

ところで、以前からある line control statement としての #line はどうなるのか、気になることかと思います。残念ながら Swift 2.2 の時点では、どちらも #line で表現され、コンテキストによって意味が変わります。

Line control statement では、#line [line-number] [file-name] というフォーマットで #line#file の結果を変えることができ、#line でリセットできます。特にリセットするための #line と、行番号の #line は非常に曖昧です。現在の所、行の最初のトークンとして現れる #line は line control statement として解釈されるようになっています (apple/swift)。

この曖昧な状態は長くは続かず、line control statement を #sourceLocation(file: "foo", line: 42) のようにリネームすることが決定されています。リセットするときは #sourceLocation() となります。余談ですが、当初は #setline となることに決まっていましたが、継続的な議論の末にさらに変更されました (swift-evolution-announce)。

Ref.

Other Improvements

引数ラベルに予約語が使用可能になる

SE-0001

関数の引数ラベルに、これまでは予約語が使用できませんでした。Swift 2.2 ではこの制限が緩和され、inout, var, let を除くほとんどの予約語が使えるようになります。これまでもバッククオート (“`”) を用いることで予約語を使うことができましたが、それが必要なくなりました。

forin など前置詞として使われる単語も予約語として扱われているので、この変更によって書きやすくなる場面があるでしょう。

tuple が比較できるようになる

SE-0015

要素が比較可能なふたつの tuple を比較できるようになります。これは標準ライブラリに tuple の比較演算子が実装されたことによるものです。比較するためには要素数が同じであることと、個々の要素が Equatable であることや Comparable であることが必要です。

ただし tuple の要素数が6以下である必要があります。それ以上の要素数について比較したい場合には、自分で実装を追加しなければなりません。これはそれぞれの要素数の tuple 毎に実装を用意しなければならない都合上、どこかに限度を設ける必要があり、大抵は要素数が6以下であることや標準ライブラリのバイナリサイズの増加分とのバランスにより決定されています。

@objcenum につけられるようになる

Declaration Attributes

If you apply the objc attribute to an enumeration, each enumeration case is exposed to Objective-C code as the concatenation of the enumeration name and the case name. For example, a case named Venus in a Swift Planet enumeration is exposed to Objective-C code as a case named PlanetVenus.

Raw type が Intenum@objc 属性で Objective-C から見えるようになります。Objective-C から見たときは enum の名前と case の名前が結合されます。

Deprecated

Swift 2.2 では、これまで利用できたいくつかの機能が廃止予定になります。Swift という言語をより洗練させるために、非互換であってもきちんと整理していこうという姿勢ですね。これらは Swift 3.0 で実際に削除されます。代替的な書き方が存在するはずなので、順次置き換えていきましょう。

カリー化関数構文

SE-0002

カリー化関数を作るための func curried(x: Int)(y: Int) という構文は、それほど便利に利用されておらず、連鎖的に言語や実装を複雑にしていることから、Swift 3.0 で削除され、Swift 2.2 でも警告されます。

func curried(x: Int)(y: Int) -> Int {
    return x * y
}

func closure(x: Int) -> (y: Int) -> Int {
    return { y in x * y }
}

例のふたつの関数は同等の働きをするので、直接クロージャを用いた下の形式に書き換えることになるでしょう。

関数の引数の var

SE-0003

func increment(var x: Int) -> Int {
    x += 1
    return x
}
increment(1)

関数の引数の var は、便利であることが少なく、inout などとも紛らわしいことから、Swift 3.0 で削除される予定です。Swift 2.2 でも警告されます。

++, -- 演算子

SE-0004

インクリメントやデクリメントのための ++-- 演算子は、前置と後置で返り値が異なるなど複雑であり、また for-in でループすることがふつうである現在となっては利用頻度も低いなどの理由から、Swift 3.0 で削除される予定です。Swift 2.2 でも警告されます。

x += 1x = x.successor() といった代替的な方法に書き換える必要があります。

C スタイルの for ループ

SE-0007

C スタイルの for ループ (for init; comparison; increment {}) は、Swift において使用頻度が非常に低く、簡潔な for-in に較べて複雑であることから、Swift 3.0 で削除され、Swift 2.2 から警告されます。

ほとんどのケースでは for-in で書き換えられ、あるいは while でも同様に記述できるはずです。for-in では収まりの悪いようなケースについては標準ライブラリの改善によって解決される予定であるほか、パフォーマンス上の違いについても最適化によって解決が図られることになっています (swift-evolution-announce)。

?

Optional の Sequence での lazy な flatMap

SE-0008

lazy を使って LazyCollection を作り、それを flatMap した場合について、flatMapOptional を返す場合についても LazyCollection が保たれるべきである (遅延評価されるべきである) という提案です。Optional を返す場合は LazyCollectionTypeflatMap ではなく SequenceTypeflatMap が利用されるためにこの問題が起きているというわけです。

この提案は Swift 2.2 の変更として承認されていますが、実装された様子がありません (SR-361)。

Xcode 7.3

Xcode も合わせてアップデートされます。オートコンプリートが fuzzy な入力にも対応するのは、誰もが嬉しいことでしょう。つまり例えば、UITableViewdequeueReusableCellWithIdentifier(_:forIndexPath:) メソッドを呼び出すのに、deqcellwi などとタイプしてもよい、ということです。これまでは前方一致でしかオートコンプリートされなかったことを考えると、信じられない進歩です。

Interactive Playgrounds によって、Playground で表示されるビューを操作できるようになるのも、とてもおもしろいことです。UI のプロトタイピングにはもちろん、何かを説明するのにも便利そうですね。

また OSS となった Swift の toolchain を利用することもできるようになります。

Xcode のリリースノートを見ると興味深い変更がもう少しあります。

The static analyzer checks for missing localizability. This check is off by default and can be enabled by selecting Yes for “Missing localizability” in the “Static Analyzer – Generic Issues” build settings. (23414217)

デフォルトでは無効になっているものの、NSLocalizedString を使っていないことを静的解析でチェックできるようになっていそうです。

Simulator.app supports delivering touch pressure to iOS and watchOS by using a Force Touch trackpad.

シミュレータ利用時に、Force Touch トラックパッドで 3D touch できるようになりました。

View debugger も全体的に強化されているようです。

Ref.

Xcode Release Notes


Swift 2.2 ではこのように小規模な改善が行われました。それほど大きな変更ではありませんが、しかし現実の問題に対する具体的な解決策と言えるようなものが多いように思われます。これはまさにコミュニティからのフィードバックの効果と言えるでしょう。

これらに加えて、コンパイル時間が改善しパフォーマンスも向上するほか、コンパイラ自体のクラッシュも減り、より正確に警告やエラーを発するようになります。これらが日々の生産性に寄与することに疑いの余地はありません。

そしてもちろん秋には Swift 3.0 が控えています。Swift 3.0 では新しい API ガイドラインによって API のネーミングに大きな変化が起き、まるで別物のように感じられるかもしれません。enum の個々の case が lower case になる (Optional であれば .some.none になる) と知ったら驚くことでしょう (SE-0023)。それ以外にも、ABI の安定に向けて generics を完全にするための議論が始められています (swift-evolution)。Swift 3.0 と共にリリースされる Swift Package Manager は、Xcode のプロジェクトファイルを生成するようになり、これまでより簡単に Swift のライブラリを扱えるようになるでしょう (apple/swift-package-manager, swift-evolution)。そして Swift 3.0 の開発はまだまだ続きます。今はまだわからない、素晴らしい機能が追加されることでしょう。

詳解 Swift 改訂版

詳解 Swift 改訂版

try! Swiftに参加した日記です #tryswiftconf

先日のtry! Swift Conferenceに参加した。try! Swiftは、日本におけるモバイルアプリのカンファレンスとしては間違いなく過去最大規模であり、かつ最も国際色あるものだったように思う。しかし誰もが繰り返し言っているように、その運営は極めてスムーズであり、主催者の苦労が偲ばれる。@NatashaTheRobotさんや id:KishikawaKatsumi さんをはじめとした主催者のみなさんや、ボランティアのみなさん、その他関係者の多くの方々には、ただ感謝するばかりである。

try! Swift


私のtry! Swift

これは個人の感想であるから、僕とtry! Swiftについての関わりから始める。僕はtry! Swiftの参加者であり、スピーカーの一人でもある。またスポンサーとしての はてな とtry! Swift主催者の接点でもある。

try! Swiftの開催が準備されていただろう昨年の秋以来、僕も少しずつ準備を進めることになった。スポンサーについては社内ですぐに同意を得られて、CTOや広報が着々と準備を進めてくださったのでほんとうに助かった。スピーカーとしては、悶々とする日々が続いた。

大規模なカンファレンスでプレゼンテーションするのは難しい。僕の未熟さ故に、どういう話をしたものかと悩み続けることになった。結果としては少し広いトピックを扱うことにして、具体的なライブラリ開発の中からSwiftらしい部分を取り出して紹介する、ということにした。ただトピックが広がっても散漫になるので、少し物語性を加えて、アプリ開発の中からライブラリを抽出する、という体裁にした。

そうしたのだけど、特定のトピックに集中する方がもっとおもしろかったんじゃないか。そういう考えが頭を離れない。面目ない。次回にこのようなカンファレンスがあった際には、きっともっと素敵な発表をしたい。

ともあれ発表の内容自体は、全て僕の正直な考えである。僕たちの多くは、仕事でスマートフォンのアプリを作っている。僕たちが一生懸命作ったアプリで、ユーザーや顧客が喜んでくれたら、それは何にも代えがたい素晴らしいことであり、一番大切なことだ。しかし人によっては、それだけでは満足できないと思う。デベロッパーのコミュニティに貢献したい、そういう人がいるんじゃないか。そういう人たちを応援したいという気持ちを表したつもりでいる。

みんなのtry! Swift

個人の感想であるから、実際にtry! Swiftに参加して感じたことも記しておく。

try! Swiftでは多くの方たちと交流を持てた。最近は関モバなどローカル感のある交流が多かったので、それとは違った刺激を受けた。興味の範囲が大きく違ったりして、とてもおもしろい。

ひとくちにSwiftと言っても、その周辺には多様な人たちが集まっているのだな、という感じがした。もちろん大半はスマートフォンアプリのデベロッパーだろうけど、会社勤めの人もフリーランスの人もいるし、学生もいる。今回は特に海外からの人も多かった。それだけじゃなく、Swiftで自分たちのアプリが開発しやすくなると思う人もいれば、Swiftそのものをもっと良くしたい人、Swiftに関連してコミュニティを盛り上げたい人、のように、人々がSwiftに仮託するものも多様なのだと感じる。

同様にtry! Swiftから受けた感想も多様なのだと思う。Objective-CからSwiftへ移行しないといけないと感じた人もいるだろうし、protocol orientedでSwiftらしいコードに書き直さないとと思った人も、もっとvalue typeを利用したりimmutableにしなければならないと思った人もいるだろうし、英語でのコミュニケーションを課題に感じた人も多かったように思う。もちろんコミュニティに貢献したいと強く思うようになった人も多いだろう。

僕はもう1年半くらい実務としてSwiftを書いていて、その中でもこういった問題意識を感じることが多い。どうしてそういう風に感じるかというと、これらが本質的に難しい問題だからだと思う。Objective-CからSwiftへの移行には大きなコストがかかる。Swiftらしいコードにするには、Swiftという言語やプログラミングそのもに対する深い理解が必要である。そしてUIKitなどのObjective-C時代からの遺産は時としてそれを阻む。もちろん長年日本に暮らしていて英語を学ぶのは簡単じゃない。難しいので、諦めてコツコツやるしかない。

catch

何にせよ、try! Swiftによって少なからず影響を受けて、これからどうしたらよいのだろう。

SwiftでのiOSアプリ開発には、課題が山積している。Immutableなview controllerを作るのは容易ではない。Storyboardを使う使わないだけではなく、例えばstate restorationはどうやって実装したらよいだろうか。あるいはprotocol orientedでvalue typeで、Swiftらしい設計を適用できるのはどういった場面なのか、それは汎用的に使えるのか。

何よりSwiftiOSも、そのエコシステムも、日進月歩で変化を続けていく。OSSとなったSwiftはどこを目指すのか。Swift 3にはどのような機能が、どのような思想で加えられていくのだろう。iOSOS XSwiftに合わせて変化していくのだろうか。サーバーサイドSwiftが現実的な選択肢として十分な説得力を得るまでにどれくらいかかるのだろう。

僕たちを取り巻く環境はどんどん変化していく。僕たちもまた変化しなければならないだろう。ひとまずは、いちばん興味を持てることをやったらいいと思っている。誰かが言う大切なことじゃなく、自分が信じる大切なことに投資すべきである。

一年後にきっと開催されるだろうtry! Swift 2017への期待は大きい。大変なことだろうとは思いますが、どうかよろしくお願いします。僕にできることがあれば何でもお手伝いさせてください。それまでに僕ももっと変わっていたい。


発表の補足

発表の内容につきましては、後日ビデオが公開されるはずですので、そちらをご参照ください。

プレゼンテーション中に登場したライブラリは以下です。ただし HTTPRequestMatcher は準備中です……。

UTIKit

github.com

cockscomb.hatenablog.com

HUDKit

github.com

追って紹介するエントリを書こうと思います。

お返事

また僕が発表中に、「デザイナーは行の高さにこだわる」というような発言をしたことについて、「デザイナーだけじゃなく……」というようなtweetをお見かけしましたが、文脈上「デザイナーがInterface BuilderでUIを作ることにしたとき、デザイナーがこだわりを持って Interface Builder 上で行の高さを調整できるように @IBInspectable を利用できる」といった意図を持ってのものでありましたので、悪しからずご了承ください。

加えていくつかのtweetにもお礼とお返事を述べさせていただきたいと思います。

Totally agreed.

僕らしくない発表がしたいです。

めっちゃ緊張してお腹が気持ち悪かったです。

Thank you so much.

Yeah!!

次は緩くない発表がしたいです。


こちらからは、以上です。

UIControlにOptionSetTypeで状態を追加する

新成人のみなさん、おめでとうございます。大人になるのは難しいことだけど、自由に自分の人生を歩んでほしいと思います。

UIControlState

UIControlにはvar state: UIControlState { get }というプロパティがある。これはすべてのUIControlの状態を示す。UIControlStateは同時に複数の状態を保持できるbit-maskであり、SwiftにおいてはOptionSetTypeとして表現される。つまり例えば、.Disabledでありかつ.Selectedであることが起こり得る。

struct UIControlState : OptionSetType {
    init(rawValue rawValue: UInt)
    static var Normal: UIControlState { get }
    static var Highlighted: UIControlState { get }
    static var Disabled: UIControlState { get }
    static var Selected: UIControlState { get }
    static var Focused: UIControlState { get }
    static var Application: UIControlState { get }
    static var Reserved: UIControlState { get }
}

UIControlStateの様々な状態

このvar state: UIControlState { get }は、読み込みしかできない。ヘッダには以下のようにある。

could be more than one state (e.g. disabled|selected). synthesized from other flags.

すなわち、var enabled: Boolvar selected: Boolvar highlighted: Boolといったプロパティから合成されることになっている。

新しく状態を作る

ここでUIControlState.Applicationというものがあることに気付いただろうか。ドキュメントには以下のように書かれている。

Additional control-state flags available for application use.

つまりこの.Applicationは、私たちのアプリケーションで自由に使ってよいことになっている。Objective-Cのヘッダを見ると以下のように定義されている。

UIControlStateApplication = 0x00FF0000

Objective-Cで見たUIControlStateApplicationの値

Swiftでは隠蔽されてしまっているが、実際の値は0x00FF0000であり、bit-maskにおいて8 bit分の幅が用意されていることがわかる(いうまでもないが、16進数で二桁分であるから、2進数で表現すれば8桁分となる)。この範囲を自由に割り当てて、新しく状態を増やしてみよう。

一度でも押されたことが記録されるボタンを作る

実際の例を示す。

extension UIControlState {
    static var Visited = UIControlState(rawValue: 1 << 16)
}

class VisitableButton: UIButton {

    override var state: UIControlState {
        if visited {
            return super.state.union(.Visited)
        } else {
            return super.state
        }
    }

    var visited: Bool = false {
        didSet {
            setNeedsDisplay()
            titleLabel?.setNeedsDisplay()
            imageView?.setNeedsDisplay()
        }
    }

}

VisitableButton

このUIButtonを継承したVisitableButtonには、新しくvar visited: Bool というプロパティを追加してある。そしてこのvisitedを元に、var state: UIControlState { get }をオーバーライドして新しい状態であるUIControlState.Visitedを追加する。(visitedが設定された後に表示を更新する必要があるだろうから、didSetでなるべく自身を更新するようにしている。)

UIControlState.Visited1 << 16であり、これは0x00FF0000UIControlState.Applicationの範囲に入っている(これを確かめるにはprint(0x00FF0000 as UInt & (1 << 16) as UInt)とするか、print(UIControlState.Application.contains(.Visited))とする)。オーバーライドしているstateプロパティでは、OptionSetTypeunionメソッドを利用して、元の値との論理和を作っている。

class ViewController: UIViewController {

    @IBOutlet weak var button: VisitableButton! {
        didSet {
            button.setTitleColor(UIColor(red: 0x00 / 0xFF, green: 0x00 / 0xFF, blue: 0xEE / 0xFF, alpha: 1),
                forState: .Normal)
            button.setTitleColor(UIColor(red: 0x55 / 0xFF, green: 0x1A / 0xFF, blue: 0x8B / 0xFF, alpha: 1),
                forState: .Visited)
        }
    }

    @IBAction func visit(sender: VisitableButton) {
        sender.visited = true
    }

}

VisitableButtonを利用するコード例

利用する側は、単にvisitedプロパティを操作するだけでよい。この場合は事前に色を設定している。Webのリンクのように、一度でも訪れたページへのリンクが青から紫になる、というのを模している。

UIButtonsetXxx(_:forState:)といったメソッドは、このようにUIControlState.Applicationの範囲に定義された独自の状態に対しても機能する。すなわち、事前に状態毎に設定しておくだけで、自動的にそれが反映されるのである。

まとめ

UIControlvar state: UIControlState { get }プロパティを、新たな状態を作って拡張する方法を示した。この方法によって、UIControlの状態を統一的に表すことができる。

もし既存のコードで、UIControlやそのサブクラスに何らかの状態を増やしたというつもりで、何度setXxx(_:forState:)のようなメソッドを呼び出していたら、それはおおよそ間違いであるとみられる。

同様にUIControlEventsにも.ApplicationReservedという値があり、独自のイベントを作ることができる。

UIControlEventApplicationReserved = 0x0F000000

UIControlEvents.ApplicationReservedは4 bitの範囲を持つ

参考


詳解 Swift 改訂版

詳解 Swift 改訂版

Swiftを使って型付けされた画面遷移がしたい

皆さん今朝はどんな夢をみましたか。今日は私の初夢を紹介します。

prepareForSegue

iOSアプリを開発していて、特にStoryboardを利用して遷移を作るとき、往々にしてそのコードは不完全になる。

ViewControllerの遷移をするときには、次に表示されるViewControllerに必要な情報を受け渡して初期化しなければならない。Storyboardを使った遷移は、すでにインスタンス化されたViewControllerオブジェクトに対して行うことになる。このためViewControllerのイニシャライザを利用できない。従ってUIViewControllerprepareForSegue(_:sender:)メソッドをオーバーライドして、UIStoryboardSegueオブジェクトのdestinationViewController: UIViewController propertyから、次に表示される予定のViewControllerオブジェクトにアクセスして、必要と思われる情報を渡す。これが一般的な実装である。

destinationViewControllerを初期化する

destinationViewController propertyは単にUIViewController型であるから、ここでは目的の型にキャストする必要があるだろう。そしてインスタンス化されたオブジェクトに対して、必ず必要なだけの情報を渡さなければならない。この際に型による保護は得られない。遷移元のViewControllerが遷移先のViewControllerについてよく知っている必要がある。例えば後から必要な情報が増えたときには、コンパイルは通るが実行時にエラーになる、といった状態が発生しやすい。あるいはsegueの接続が変わった場合に、もともと想定していた初期化が行われずに実行時エラーが起きることが頻繁にある。もちろんこれらの問題が気付かれぬままリリースされることはないだろうが、それでも型による保護がないというのは不便である。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let toViewController = segue.destinationViewController as? ToViewController {
        toViewController.entry = Entry(title: "Title")
    } else {
        fatalError()
    }
}

典型的な prepareForSegue の実装では、遷移先のViewControllerの型をキャストし、必要な情報を受け渡す

ViewControllerの初期化

そもそもViewControllerの初期化について考えておく必要がある。インスタンスは、本来であればイニシャライザで初期化されるべきであるが、Storyboardを使う以上それは叶わない(Nibであればイニシャライザを利用することもできる)。仮にイニシャライザが利用できる場合、そのシグネチャそのものが初期化に必要な情報を表すことになる。Storyboardによってインスタンス化されるViewControllerにおいても、どのように初期化されるべきか知っているのはそのViewController自身である方が好ましい。

ところで遷移を表すUIStoryboardSegueは、3つの情報を持っている。var identifier: String? { get }var sourceViewController: UIViewController { get }var destinationViewController: UIViewController { get }である。identifierは一意にsegueを指す。同じsegueは必ず同じ型のsourceViewControllerdestinationViewControllerを持つ。このことを利用して、この情報の組を型として表現することができるのではないか。

SegueInitiator

試しにこの遷移の情報を元にViewControllerを初期化する型を作ってみる。

protocol SegueInitiator: class {
    static var identifier: String? { get }
    func prepare(segue: UIStoryboardSegue)
}

segueのidentifierに対応して、prepare(_:)メソッドを持つというSegueInitiator型を定義した。

extension SegueInitiator {
    static func match(segue: UIStoryboardSegue) -> Bool {
        return identifier == segue.identifier
    }
    func performFrom(viewController: UIViewController) {
        if let identifier = Self.identifier {
            viewController.performSegueWithIdentifier(identifier, sender: self)
        } else {
            fatalError("can't perform nil identifier segue")
        }
    }
}

ふたつのデフォルト実装も用意しておく。ひとつはUIStoryboardSegueが対応するものかどうか。そしてもうひとつは、UIViewControllerperformSegueWithIdentifier(_:sender:)を呼び出すことができるものである。

あとはこのprotocolに準拠するclassを定義すればよく、特にprepare(_:)メソッドを遷移先のViewControllerに合わせて正しく実装すればよいはずである。しかし実際的にはsegueのsourceViewControllerdestinationViewControllerのそれぞれの型を、より具体的なViewControllerの型に束縛したいはずだ。そこで以下のように具体的な型を持てるprotocolを作って元のprotocolを継承させる。

protocol TypedSegueInitiator: SegueInitiator {
    typealias Source: UIViewController
    typealias Destination: UIViewController
    func prepareWithSource(source: Source, destination: Destination)
}

extension TypedSegueInitiator {
    static func match(segue: UIStoryboardSegue) -> Bool {
        return identifier == segue.identifier &&
            segue.sourceViewController is Source &&
            segue.destinationViewController is Destination
    }
    func prepare(segue: UIStoryboardSegue) {
        if let source = segue.sourceViewController as? Source,
            let destination = segue.destinationViewController as? Destination {
                prepareWithSource(source, destination: destination)
        } else {
            fatalError("Given segue '\(segue.dynamicType) - Identifier: \(segue.identifier ?? ""), Source: \(segue.sourceViewController.dynamicType), Destination: \(segue.destinationViewController.dynamicType)' does not match this SegueInitiator '\(self.dynamicType) - Identifier: \(self.dynamicType.identifier), Source: \(Source.self), Destination: \(Destination.self)'")
        }
    }
}

ここではtypealiasSourceDestinationのふたつのassociated typeを定義した。デフォルト実装でprepare(_:)を実装し、型が合っているか検証した後に、新たなprepareWithSource(_:destination:)メソッドを呼び出すことにする。型が合わない場合にはその時点でわかりやすいメッセージと共に実行時エラーを引き起こす。Storyboard上での設定と実装で定義が合わない場合にはもうどうしようもない。

これらを用いることで以下のように書ける。

struct Entry {
    let title: String
}

class FromViewController: UIViewController {

    @IBAction func executeToSegue(sender: AnyObject) {
        ToSegue(entry: Entry(title: "To Segue")).performFrom(self)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let initiator = sender as? SegueInitiator {
            initiator.prepare(segue)
        } else {
            fatalError()
        }
    }

}

class ToViewController: UIViewController {

    @IBOutlet weak var titleLabel: UILabel!

    var entry: Entry!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = entry.title
    }
}

class ToSegue: TypedSegueInitiator {
    let entry: Entry

    init(entry: Entry) {
        self.entry = entry
    }

    static let identifier: String? = "ToSegue"
    typealias Source = UIViewController
    typealias Destination = ToViewController
    func prepareWithSource(source: Source, destination: Destination) {
        destination.entry = entry
    }
}

最後のToSegueが、遷移先のViewControllerを実際に初期化する役割を持ったclassである。prepareWithSource(_:destination:)メソッドの実装はごく単純であり、この段階においては型による安全性が保たれている。この初期化オブジェクトはイニシャライザで初期化に必要なインスタンスを受け取ることにしている。初期化オブジェクトのシグネチャが、初期化に必要な情報を表しているのである。

もちろんこのようにしても、ViewControllerを不適切なやり方で初期化しようとすることはできる。しかし原則的にSegueInitiatorを利用して遷移先の初期化を行うというルールを作り、ViewControllerのコードと同じところにSegueInitiatorのコードを置いておくことで、いくらかの助けにはなるだろう。

まとめ

実際に動作するコードをGitHub上に公開している。この例では、例えばUINavigationControllerが間に挟まっているようなケースにも対応したNavigationSegueInitiatorというclassも用意してある。

本記事の内容は筆者が思いつくままに書いてみたという段階であり、実際に試されたことはない。この方法が現実のアプリケーションでうまく働くかは未知数である。

SwiftからUIKitをはじめとしたObjective-C時代のAPIを利用するときに感じるミスマッチは、現段階においては受け入れるしかないものと思われる。この遷移のインターフェースはiOSのUIKitにおいてもOS XのAppKitにおいてもそう変わらない。*1決して悪いとは思わないが、それでもやはりSwiftに期待される安全性からは程遠く感じられる。

次世代のiOSOS Xではフレームワーク側が進歩して、このような問題が一挙に解決されるかもしれない。そういった期待を持ちつつも、いまのところは様々な工夫を試していくのがよいだろう。


詳解 Swift 改訂版

詳解 Swift 改訂版

*1:watchOSのWatchKitでは、contextオブジェクトを返すメソッドを実装すると、contextオブジェクトは遷移先のWKInterfaceControllerのawakeWithContext(_:)メソッドに渡り、ここで自身を適切に初期化するということになっている。

Swiftにおけるclassとstructの使い分け

新年あけましておめでとうございます。

class, struct

Swiftにはclass (class)の他にstructure (struct)があり、どちらもよく似た機能を提供する。しかしそれぞれ参照型 (reference type)と値型 (value type)という違いがあり、このことはパラダイムの違いをもたらす。そこで多くのSwiftプログラマーは、classとstructのどちらを採用するべきか迷いがちである。本記事ではこの問題について議論を深めたい。

structはカッコいい

classについてはなじみ深いと思うので、structの特徴を整理する。はじめに述べたようにstructは値型である。値型であることがstructを大きく特徴付けている。

structはデフォルトで不変である。var, mutating, inoutのキーワードを用いることで、この不変であるという挙動を変えられる。varで宣言された変数に保持されるstructは、stored propertyをletではなくvarで宣言することで、可変の状態を持つことができる。mutatingとして宣言されたメソッドは自身のstored propertyを変更できる。関数のinoutで宣言された引数は、&で目印された変数を受け取って、そのstored propertyを変更して返すことができる。このようにデフォルトで不変であることが、意図せずに内部状態を変更することを防ぐ。

structは値であるから、何かするたびに毎回コピーされる。別な変数に代入されるときや、関数の引数として渡されるときなどに、いちいち新しくコピーされる。つまり可変のstored propertyがあったとしても、変数間で状態が共有されることは原則としてない。唯一の例外はinoutで、関数の引数がinout宣言されている場合には、関数の内外で共有されているように見える。

これらを大きな特徴とするstructは、この他にもclassと違って継承関係を持つことはなく、またメモリ管理上もclassとは異なる。継承関係はないがprotocolに準拠することはでき、protocol extensionによってmix-inのように何らかの実装を他の型と共有できる。classのメモリ管理が、自動的に参照カウントを増減させるARCであるのに対して、structは値であるからより単純に破棄される。加えてclassには存在するdeinitializerがない。

これらがstructの特徴である。一言でまとめると、カッコいいのである。なるべくならカッコいいstructの方を使いたいのである。

classとstructureのどちらを選ぶか

structが使いたいわけではあるが、ほんとうにいつでもstructを使えるのであろうか。structを使っていいかどうか、経験によって学ぶしかないのだろうか。その答えは用意されている。“Choosing Between Classes and Structures” と題された節が、Appleによるドキュメント “The Swift Programming Language” にある。そこには以下のように条件が載せられている。

As a general guideline, consider creating a structure when one or more of these conditions apply:

  • The structure’s primary purpose is to encapsulate a few relatively simple data values.
  • It is reasonable to expect that the encapsulated values will be copied rather than referenced when you assign or pass around an instance of that structure.
  • Any properties stored by the structure are themselves value types, which would also be expected to be copied rather than referenced.
  • The structure does not need to inherit properties or behavior from another existing type.

(中略、いくつかの具体例)

In all other cases, define a class, and create instances of that class to be managed and passed by reference. In practice, this means that most custom data constructs should be classes, not structures.

ここで挙げられているような条件に当てはまる場合にはstructを用い、そうでないほとんどの場合はclassを用いるべきであると書かれている。ひどく保守的な条件と感じた読者も多いだろう。

実際にこの条件を意識して例を書いてみよう。

struct ScreenSize {
    let width: Int
    let height: Int
}

let fullHD = ScreenSize(width: 1920, height: 1080)

画面の解像度を表現したstruct

この例は「比較的単純ないくつかのデータをまとめた」ものであり、明確に先の条件に当てはまる。何の文句もなくstructでよいだろう。

struct Counter {
    var count = 0

    mutating func countUp() {
        count += 1
    }
}

var counter = Counter()
print(counter.count)
counter.countUp()
print(counter.count)

カウンターをstructにしたもの

これは「インスタンスが代入や関数に渡した際に参照されるのではなくコピーされることが期待されて適当」と言えるだろうか。むしろカウンターの状態が共有されないことが不自然に感じられるかもしれない。classの方がよいのではないだろうか。

struct Observing {
    var observer: AnyObject? = nil
    var noticed = false

    init() {
        observer = NSNotificationCenter.defaultCenter().addObserverForName("MyNotification", object: nil, queue: nil) { (notification) -> Void in
            print("Noticed!")
            self.noticed = true
        }
    }
}

var observing = Observing()
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name: "MyNotification", object: nil))
print(observing.noticed)

structがNSNotificationCenterに登録する例

この例は信じられないほど全くあり得ない例である。structのイニシャライザで作られたクロージャが自身を参照し、破壊的な変更をしている。このコードは期待される動作にはならない。クロージャは参照型であり、クロージャによって参照されたstructはコピーされ、このようにクロージャでstructの状態を変更することには何の意味もない。

struct ScreenSize {
    let width: Int
    let height: Int
}

class Box<Type> {
    let value: Type

    init(_ value: Type) {
        self.value = value
    }
}

let fullHD = ScreenSize(width: 1920, height: 1080)
performSegueWithIdentifier("MySegue", sender: Box(fullHD))

structをAnyObjectに一致させるためのBox classを作る

UIViewControllerperformSegueWithIdentifier(_:sender:)は、ドキュメントによると第二引数senderに対して

The object that you want to use to initiate the segue. This object is made available for informational purposes during the actual segue.

とあり、遷移を初期化するために何らかのオブジェクトを渡してよいことになっている。これはAnyObject?型にstructはそのまま渡せない (structはAny型ではあるが、AnyObject型ではない) ので、Box classを作ってラップしている例である。ScreenSizeはstructにするのに適していたが、Objective-Cで書かれたフレームワークとの兼ね合いでは余計な手間がかかるようになってしまった。この箇所だけであればこういったワークアラウンドも悪くないが、もしプログラムのあちこちにこういったワークアラウンドが出現することになったら、そもそも不変なstored propertyを持つclassで定義しておけばよかっただけかもしれない。

2016/09/12追記

Swift 3 での改善

SE-0116 Import Objective-C id as Swift Any type により、Swift 3 では Objective-CidAny 型としてインポートされるようになった。したがってこの例のような API では sender に struct を利用できるようになり、ワークアラウンドしなくてもよい。Swift 3 以降においてはこのようなケースで struct が使いやすくなっている。

Swiftにおけるclassとstructの使い分け

こうして見ると、カッコいいstructはただカッコいいというだけでは使いにくいことがわかる。ドキュメントの通り、多くの場合はclassを使うことが適切かもしれない。これを読んでいる諸氏は当然読んでいるはずのAppleの“The Swift Programming Language”は、Swiftを学ぶ者のために書かれたものであって、Swiftグルの読者諸氏には関係ないとも言える。

実際問題としては、現実的ないくつかの指針を挙げることができるだろう。いまclassかstructかで作ろうとしているものが、共有されるべき状態を持つならclassを選ぶべきだし、そうでなければstructが好ましいかもしれない。Objective-Cのコードとやりとりすることが想定されるなら最初からclassにしてしまってもよいだろう。classやクロージャのような参照型の要素をstored propertyなどに保持し、これと密にやりとりするならclassが好ましい。何らかの副作用を持つようなものを抽象化する場合には、コピーされるより参照される方が適切であることが多い。

classとstructの使い分けは難しい問題である。カッコいいstructを使うためには、それに合わせたカッコいい設計も必要だろう。そもそも可変なインスタンスを減らす方がよいだろうし、状態が共有されずに済むように作れるならそのほうがよい。


本記事は、関西モバイルアプリ研究会 #9での発表を元に書かれている。

2016年もこういったことをチマチマと考えながら生きていきたい。本年もどうぞよろしくお願い申し上げます。

詳解 Swift 改訂版

詳解 Swift 改訂版

Swift 2 Error Handling in Practice #swift2symposium

Swift 2 から新たに導入されたエラーハンドリングに関する機能を実際のアプリで利用しようとすると、いくつか悩みどころがあることに気付く。これらの問題について議論を深め、実践的な解を求めていきたいと思う。

Which is better? — Optional type or throws

func parseInt(string: String) -> Int?

func parseInt(string: String) throws -> Int

func parseInt(string: String) throws -> Int?

文字列を整数型としてパースする関数のシグネチャ

文字列を整数型にパースする関数があったとして、そのシグネチャはどのようにあるべきだろうか。明らかに、nil を返すかまたはエラーを throw するような、つまり一番下のシグネチャは不適切である。残るふたつ、文字列が整数としてパースできないとき、nil を返すべきか、あるいは何らかのエラーを throw するべきか。整数に nil という状態はあり得ないので nil が返るのは好ましいとは言えない、文字列を整数値にパースしようとしてできないのは明らかな異常系である、といった理由で throw するべきという意見。あるいは、文字列を整数値にするような取るに足りない処理でいちいちエラーハンドリングを強制するのは過剰である、こういったことでエラーが投げられることは相対的にエラーの重みを軽くしてしまう、などの意見。

func parseJSON(data: NSData) -> JSONObject?

func parseJSON(data: NSData) throws -> JSONObject

func parseJSON(data: NSData) throws -> JSONObject?

バイナリを JSON のオブジェクトとしてパースする関数のシグネチャ

一方で、バイナリを JSON としてパースする関数はどうだろう。これについても一番下のシグネチャは明らかに適切でない。JSON がパースできなかった場合はその原因がわかった方がよいだろうから、throws する真ん中のシグネチャがよいのではないか。

このように、正しい値を返せない場合に nil を返すようにするのか、あるいはエラーを throw するのか、どちらのインターフェースが好ましいのだろう。現実的にはコンテキストに依存することもあるだろうし、現在のところ一定した結論はない。

try? 構文の追加によって、インターフェースのユーザーから見れば throws 関数であってもエラーを無視するのが容易になった。このためインターフェースを作る側は気軽に throws にできるようになったこともまたこの議論に影響する。

ErrorType を丁寧に作る

enum JSONDecodeError: ErrorType {
    case MissingRequiredKey(String)
    case UnexpectedType(key: String, expected: Any.Type, actual: Any.Type)
}

struct JSONObject {
    let raw: [String : AnyObject]
    func getValue<T>(key: String) throws -> T {
        guard let value = raw[key] else {
            throw JSONDecodeError.MissingRequiredKey(key)
        }
        guard let typedValue = value as? T else {
            throw JSONDecodeError.UnexpectedType(key: key, expected: T.self, actual: value.dynamicType)
        }
        return typedValue
    }
}

JSON のオブジェクトから何らかの型の値にマッピングしようというときに投げられるエラー

JSON のオブジェクトから何らかの型の値にマッピングするとき、本来存在するべきキーが存在しなかったり、予期しているものと違う型の値が入っている場合に、それをエラーとしてレポートしたい。どういった ErrorType を定義するのが良いのだろうか。

エラーを見たときすぐに問題に気付けるように、ErrorType は丁寧に作られるべきである。加えて CustomDebugStringConvertible などを実装し、エラーを読みやすくする工夫もあると良いだろう。

enum JSONDecodeError: ErrorType, CustomDebugStringConvertible {
    case MissingRequiredKey(String)
    case UnexpectedType(key: String, expected: Any.Type, actual: Any.Type)

    var debugDescription: String {
        switch self {
        case .MissingRequiredKey(let key):
            return "Required key '\(key)' missing"
        case let .UnexpectedType(key: key, expected: expected, actual: actual):
            return "Unexpected type '\(actual)' was supplied for '\(key): \(expected)'"
        }
    }
}

CustomDebugStringConvertible に準拠することで debugPrint した際の可読性を改善できる

できることは他にもある。エラーを投げる関数の側に、ドキュメンテーションコメントを利用してどのようなエラーが投げられ得るか記述しておくことができる。これはその関数の利用者にとって有益な情報になるはずだ。

struct JSONObject {
    let raw: [NSString : AnyObject]

    /**
    Get typed value for the key

    - parameters:
      - key: JSON key
    - returns: Typed value
    - throws: JSONDecodeError
    */
    func getValue<T>(key: String) throws -> T {
        guard let value = raw[key] else {
            throw JSONDecodeError.MissingRequiredKey(key)
        }
    ...

ドキュメンテーションコメントの throws セクションで投げられるエラーの種類を明示できる

ErrorType と NSError

enum MyError: ErrorType {
    case SuperError
}

let error = MyError.SuperError as NSError

print(error.localizedDescription)
// The operation couldn’t be completed.
       (Module.MyError error 0.)

ErrorTypeNSError 型にキャストすることができる。NSErrorErrorType に準拠することになっているので、この逆のキャストは自然であるが、ErrorType から NSError へのキャストができるのは不自然である。おそらく暗黙的な変換が内部で行われている。

NSErrorErrorType よりも役割の広い型であった。localizedDescriptionlocalizedRecoverySuggestion といった、ユーザーインターフェイスに表示されることを意図した機能が NSError には存在している。ErrorType はそのような機能を規定しておらず、つまり ErrorTypeNSError にキャストした場合には、localizedDescription などに NSError のデフォルトの実装が使われる。しかしデフォルトの実装はユーザーインターフェイスとの連携にあまり適さず、これまでのようにエラーをユーザーインターフェイスに反映させるのが難しくなった。

NSErroriOS 9 や OS X 10.11 から新たに、public class func setUserInfoValueProviderForDomain(errorDomain: String, provider: ((NSError, String) -> AnyObject?)?) というクラスメソッドを持つ。これは localizedDescription などに対応するキーが userInfo 辞書に存在しない場合に利用される。つまり、事前に NSError にユーザーインターフェイス向けの情報が設定されていなくても、オンデマンドにそれらを設定できるのである。しかし、ErrorTypeNSError になったとき(内部的に _SwiftNativeNSError になっていて)、本来 ErrorType であったときに持っていた情報は全て失われ、domaincode だけが残っている。この状態から元のエラーの原因を識別し、意味のある情報を得るのは難しい場合も多い。

これらのことから、ErrorTypeNSError として取り扱うのはあまり筋が良くない。元々持っていた情報や機能が失われてしまう。

議論の結果、例えば ErrorDescriptable のような protocol を定義し、var localizedDescription: String { get } のようなプロパティから情報を得られるようにしておき、ErrorDescriptable の protocol extension に var toNSError: NSError { get } といった機能をつけるのが良いのではないか。これを利用して NSError にキャストせずに直接 NSError を作ることで、有用な情報を保ったまま NSError オブジェクトを得られるのではないか。


Swift の言語機能を実際のソフトウェアの中でどのように活かすかというのは、Swift でソフトウェアを書こうとする私たちが直面する大きなテーマである。エラーハンドリングだけを取り上げたとしても、ここまでに述べたような多様な問題や選択肢が存在している(それに加えて非同期のエラーハンドリングというさらに大きな問題がある)。こういったテーマについて考え、それを実践し、そしてさらなる議論を深めていくことで、Swift 時代における iOS アプリ開発の定石を発見していきたい。

この記事は Swift 2シンポジウム #2 - 2015/08/30(日) - dots. [ドッツ] で発表し、議論した内容を簡単にまとめたものです。

結婚しました

学生の頃から4年半付き合ってきた彼女と結婚しました。

区役所に婚姻届を提出し、両家の両親らをお招きして食事会を催しました。

もう2年ほど同棲しておりましたので、これといって変わることもありませんが、よりいっそうがんばって参りたいと思います。

今後ともよろしくおねがいいたします。