cockscomblog?

cockscomb on hatena blog

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年ほど同棲しておりましたので、これといって変わることもありませんが、よりいっそうがんばって参りたいと思います。

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

watchOS 2のアーキテクチャが意味すること

以前に書いたように、watchOS 1.0においてWatchKitアプリはiPhoneの側でそのプロセスが動作する。Apple Watch側にあるのはUIリソースのみであり、実際の処理はiPhone上で行われる。これは非常に高度な仕組みでありながらも、パフォーマンス上の問題を抱えていた。watchOS 2.0からはこれが大きく変化して、いわゆるネイティブ化が行われ、実際の処理もApple Watch側で行われるようになる。とはいってもAPI上の変化は少なく、これまでとおおよそ同じようなインターフェースになっている上、ベースはiOSであるからUIKitの一部やFoundationが利用できる。

watchOS 2.0の正式なリリースは秋を予定しており、Apple Watchの発売からおおよそ半年である。しかしここで大きな疑問が残る。watchOS 1.0のアーキテクチャはいったい何のために用意されたのだろうか。ネイティブ化されるまでの半年間はいったい何のために必要だったのだろうか。Apple Watchに搭載されている一部のApple製のアプリは最初からネイティブで動作しているはずだが、はじめからその方法を解放できなかったのはなぜなのか。

Bitcode

iOS 9から加わる新しい仕組みの中に、App Thiningと呼ばれるものがある。アプリがユーザーの端末にダウンロードされる前に、App Storeがアプリに最適化を加えて余分なリソースを減らしたり、必ずしも必要とは限らないリソースを後からダウンロードさせたりといった仕組みである。その中に、Bitcodeと呼ばれる手法が存在する。

Bitcodeでは、デベロッパーはアプリの実行ファイルをLLVM Bitcode状態でApp Storeに(iTunes Connectに)アップロードする。LLVM BitcodeとはコンパイラであるLLVMの中間表現を格納するフォーマットで、Appleの独自の規格というわけではなく、LLVMプロジェクトの中で定められたフォーマットである。LLVMの中間表現は、このあと本来はLLVMによって最適化され、それぞれのプロセッサのアーキテクチャのネイティブコードに変換されるはずである。Bitcodeによる提出ではこの最終工程をApp Storeが行うので、将来さらに高度な最適化ができるようになったり、あるいはプロセッサのアーキテクチャが変わったときにも、デベロッパーによるバイナリの再提出なしに対応できるという寸法である。

Bitcodeでの提出は、iOSアプリではいまのところ任意、watchOSのネイティブアプリでは必須とされている。

Apple Watchのプロセッサ

現在のApple WatchはApple S1というSiPである。Chipworksによれば、内蔵されているプロセッサにはAPL0778と書かれているようだ。Appleはこのプロセッサのアーキテクチャを公表していない。

総合的に見て、AppleApple Watchのアーキテクチャを流動的に保とうとしているように思われる。アプリのネイティブ化はBitcodeの導入を待っていたように感じられるし、Bitcodeがあればアーキテクチャに縛られることがない。

ウェアラブルデバイスにおける共通の問題はバッテリーの保ちである。Appleがこれに対して今後どのようなアプローチを取ってくるのか目が離せない。

Apple Watchを買うべきか

ひとより先に未来に辿り着けるのだとしたら、買わない理由がどこにあるのだろう。