cockscomblog?

cockscomb on hatena blog

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を買うべきか

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

Swift 1.2

第61回 Cocoa勉強会関西で“Swift 1.2 The long-awaited language updates”と題して発表した、Swift 1.2の主だった(おもしろい)変更点の紹介です。

if let

Swift 1.2で最も改善されたのはif文です。if letでOptionalをunwrapできる機能が大きく向上し、複数のOptionalを同時にunwrapできるほか、unwrapされた値について条件を加えることができるようになりました。

例えばcondition: Boolが真でふたつのOptional<Int>nilではなく、大小関係にも条件がある、という条件を表してみます。

Swift 1.1

let condition = true
let aNumber: Int? = 3
let anotherNumber: Int? = 7
if condition {
    if let a = aNumber {
        if let b = anotherNumber {
            if a < b {
                println(a + b)
            }
        }
    }
}

Swift 1.2

let condition = true
let aNumber: Int? = 3
let anotherNumber: Int? = 7
if condition, let a = aNumber, let b = anotherNumber where a < b {
        println(a + b)
}

極端な例ではありますが、Swift 1.1ではifを4回重ねていたものを、Swift 1.2では1つのifで表現できます。

let constant must be initialized before use

let定数は再代入できません。Swift 1.1ではそのために、宣言と同時に値を決める必要がありました。Swift 1.2ではその制限が少し緩和され、値が使われる前に確実に初期化されるなら、宣言の後で値が決まってもよいことになりました。

Swift 1.1

let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())

var meridiem: String
if hour < 12 {
    meridiem = "AM"
} else {
    meridiem = "PM"
}

println(meridiem)

Swift 1.2

let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())

let meridiem: String
if hour < 12 {
    meridiem = "AM"
} else {
    meridiem = "PM"
}

println(meridiem)

Swift 1.1ではvarにしなければならなかったmeridiemが、Swift 1.2ではletにできます。ifに限らずswitchなども利用できますが、全ての条件で初期化されることが保証されている必要があります。

Set

CollectionTypeArrayDictionaryなどに加えてSetが追加されました。Setは集合を表し、Foundation.frameworkのNSSetに対応します。Setの導入に合わせて、Objective-CでNSSetを利用していたAPISwiftから利用する場合にはSwiftのSetを使うことになります。

Swift 1.1

let abc: NSSet = NSSet(objects: "A", "B", "C")
if NSSet(objects: "A").isSubsetOfSet(abc) {
    println(abc)
}

Swift 1.2

let abc: Set<String> = ["A", "B", "C"]
if abc.isSupersetOf(["A"]) {
    println(abc)
}

NSSetと違って内包する値の型を指定でき、またArrayリテラルで表現できます。

Static methods and properties

Swift 1.2からclassにもstaticな関数やプロパティを持てるようになりました。これまでもclass funcがありましたが、static funcclassかつfinalという意味になります。またstatic letで宣言した定数は最初にアクセスされた時点で評価されるという特徴を持ちます。

class StaticPropertiesAndMethods {
    static func printDate() {
        println(date)
    }
    static let date = NSDate()
}

StaticPropertiesAndMethods.printDate() // => "2015-04-10 07:01:00 +0000"

クラス変数はObjective-Cには存在せず代替的な手法で実現していましたが、Swiftでついに用意された文法と言えます。

Non-Void return types in Void contexts

Swift 1.1でクロージャを使うときに、返り値としてVoidを要求するところに1行だけ、Voidではない値を返す式があると、コンパイルエラーになっていました。これはクロージャが1行だけの式を持つとき、その式の結果がクロージャ全体の返り値として取り扱われ、型がミスマッチだと判定される結果です。このために無意味なreturnを付け加えるなどして回避していました。

Swift 1.2ではこのようなときにVoidではない値が返っても許容されるようになりました。これで意味上は不要なreturnを書く必要がなくなり、すっきりします。

Swift 1.1

let wantsVoid: (() -> Void) = {
    "non-Void here"
    return
}

Swift 1.2

let wantsVoid: (() -> Void) = {
    "non-Void here"
}

zip

Swift 1.2ではzip関数が導入されました。ふたつのSequenceTypeの組み合わせを簡単に作ることができます。要素数が異なる場合には短い方に合わせられます。

let ordinals = ["first", "second", "third"]
let values = [1, 2, 3]

var ordinalsDict: [String: Int] = [:]

for (key, value) in zip(ordinals, values) {
    ordinalsDict[key] = value
}

println(ordinalsDict) // => ["first": 1, "second": 2, "third": 3]

Type casting

キャスト関連でも非互換な変更などがあります。

let any: AnyObject = 3
let number: Int = any as! Int

危険な操作であるときの一貫性のため、ダウンキャストするときas!を使うことになりました。

protocol SomeProtocol {
    func something() -> String
}

class SomeClass: SomeProtocol {
    func something() -> String {
        return "Something awesome"
    }
}

let some: AnyObject = SomeClass()
if some is SomeProtocol {
    println((some as! SomeProtocol).something())
}

@objcではない通常のprotocolにもキャスト関連の操作ができるようになっています。

@noescape

関数が引数としてクロージャを受け取るとき、クロージャの内部で利用される変数はキャプチャされ、それぞれ強参照された状態となります。またこのためにクロージャの外のオブジェクトのプロパティなどを利用する場合にはself.で修飾する必要があります。これによってクロージャを実際に評価するタイミングはいつでもよいことになっています。

しかしクロージャを受け取ってすぐに評価することが決まっている関数の場合はどうでしょう。そういうときはキャプチャする必要が無いし、self.も不要です。キャプチャしない方がパフォーマンス上でも有利でしょうし、最適化しやすいかもしれません。このようにすぐに評価されることがわかっているクロージャのために追加された新しいアノテーション@noescapeです。

func map<U>(f: @noescape (T) -> U) -> U?

Optionalmapでは上記のように@noescapeされています。このためクロージャで使う変数がプロパティであってもself.は不要です。

このようにSwiftではこれらの挙動をescapeという言葉で表しています。

func map<U>(transform: (T) -> U) -> [U]

ところでArraymapを見てみると、上記のように@noescapeアノテーションがないことが分かります。一貫性がないように思われるかもしれませんが、これには理由があるように思えます。lazy()関数などを用いて遅延評価する場合にはescapeせざるを得ません。これは簡単な実験で確かめられます。

let start = NSDate()
let array = Array(0..<3)

let result = lazy(array).map { (number: Int) -> Int in sleep(1); return 100 * number }

println(NSDate().timeIntervalSinceDate(start))

このようにlazy()関数を使えばmap中でsleep(1)していても一瞬で実行が終わり、result[2]などresultの要素にアクセスしたとき初めてmapクロージャが評価されます。このlazy()を消すと、このスニペットは少なくとも3秒の実行時間を必要とします。

これがArray.mapがescapeを必要とする理由です。

この@noescapeが追加されたのに合わせてデフォルトでは@autoclosureもescapeされなくなりました。escapeしてほしい場合には@autoclosure(escaping)と書く必要があります。

flatMap

新しいflatMap関数を使うとmapではできないちょっとしたことが簡単にできるようになります。

let mapped = ["A B C", "D E", "F"].map {
    $0.componentsSeparatedByString(" ")
}
println(mapped) // => [[A, B, C], [D, E], [F]]

let flatMapped = ["A B C", "D E", "F"].flatMap {
    $0.componentsSeparatedByString(" ")
}
println(flatMapped) // => [A, B, C, D, E, F]

上記の例のように、Arrayの要素に何らかの変換を行うとき、flatMapなら要素毎に新たなArrayを返しても2次のArrayになりません。

func toInt(value: String) -> Int? {
    return value.toInt()
}

let one: String? = "1"

let mapOne = one.map { toInt($0) }
println(mapOne) // => Optional(Optional(1))

let flatMapOne = one.flatMap { toInt($0) }
println(flatMapOne) // => Optional(1)

また上記の例では、OptionalmapflatMapの違いを示しています。mapの中でさらにOptionalな値を返すとOptional<Optional<Int>>を得ることになります。flatMapではOptional<Int>です。これは非常に有用であることがわかるでしょう。

このようにflatMapmapとよく似ていますが、最初のArrayと要素数が変わったArrayが返ったり、多重のOptionalを避けられたりといった特徴があります。

let intLike = ["12.3", "45", "6-7-8"]
let results = intLike.flatMap {
    $0.toInt().map { [ $0 ] } ?? []
}

println(results) // => [45]

上記はこれの応用例で、ふつうのmapであればArray<Int?>が返るところを、nilを除去してArray<Int>にしています。

NSEnumerator.generate() -> NSFastGenerator

Swift 1.2ではFoundation.frameworkにも少し拡張が増えています。

let fonts = "/System/Library/Fonts"
let files = NSFileManager().enumeratorAtPath(fonts)!
for path in files {
    println(path)
}

enumeratorAtPathNSDirectoryEnumeratorというNSEnumeratorのサブクラスを返します。Swift 1.2より前では、NSEnumeratorSequenceTypeではなく、for inが使えませんでした。Swift 1.2からはgenerator()メソッドが加わり、protocol SequenceTypeを満たすようになったためfor inできるようになりました。

Swiftはこのように、Foundation.frameworkについてSwiftから利用しやすいように多少の拡張を加えており、今後もこれらが拡充されていく可能性があります。

その他の非互換な変更

Remove implicit conversions from NSString/NSArray/NSDictionary to String/Array/Dictionary

NSStringなどのFoundation.frameworkのオブジェクトから対応するSwiftStringなどへの暗黙的な変換が行われなくなりました。明示的にas Stringなどと書いてキャストしておく必要があります。逆のStringからNSStringといった変換はこれまで通り暗黙的に行われます。

SwiftからObjective-Cのヘッダを見たときにはNSStringStringになっており、Swiftからはほとんど常にStringを利用していることになるので、実際的にこれが問題になるケースは少ないでしょう。ただしCoreFoundation.frameworkに関するオブジェクトを利用するときは別です。

countElements → count

素数を数える関数の名前が変わりました。分かりやすいですね。

パフォーマンスに関する変更

Swift 1.2と共に様々なパフォーマンスが改善されました。Swiftで書かれたプログラムのパフォーマンスが改善されたほかにも興味深い変更があります。

Incremental build

Swiftコンパイルがインクリメンタルになりました。これまでは、ひとつのファイルを変更したときにも全てのファイルのコンパイルをやり直す必要がありましたが、これからは必要最低限のファイルをコンパイルし直すだけでよくなりました。ただし今後も、変更したファイルだけをコンパイルし直すのではなく、変更が影響するであろうファイル全てをコンパイルし直す必要があります。

Swift 1.2でも思ったよりコンパイルが遅いということがあるかもしれませんが、もしかするとXcode 6.3.1の問題かもしれません。

Whole Module Optimization

オブジェクト指向パラダイムにおいて、クラスを継承してメソッドをオーバーライドするというのがあります。すなわち、メソッドを呼び出す際にはオーバーライドされた後の実装を呼び出す必要があるということです。プログラムの実行中、メソッドの呼び出しやプロパティを操作する時に、実際に呼び出すべき対象を動的に解決します。これをdynamic dicpatchと呼んでいます。

ここで、例えばfinal修飾されているメソッドは、ぜったいにオーバーライドできません。そういった場合にはdynamic dispatchする必要がないので、Swiftコンパイラは静的に解決してくれます。dynamic dispatchが行われない分、実行時のパフォーマンスは改善します。

さらにアクセス制御のprivateで修飾されていれば、そのファイルの外からはオーバーライドできません。つまりそのファイル中でオーバーライドされているか検査することで不要なdynamic dispatchをしないようにコンパイルできます。private修飾はパフォーマンスにも役立っていることがわかります。

さらにinternalでもこれを可能にするのがWhole Module Optimizationです。internalで修飾されていればそのモジュールの外からオーバーライドされません。つまり、モジュール全体を検査することで、不要なdynamic dispatchを避けることができます。この最適化は比較的時間がかかるため、現在のところオプションになっています。

これらの話題はSwiftのブログにも書かれています。

Objective-CのNullability

Swift 1.2に合わせてObjective-Cにも変更があります。変数や引数、返り値などがnilを取り得るかをアノテートできるようになりました。nullablenonnullで修飾することでこれが可能です。また全てにこれらのアノテーションを付けて回るのは現実的ではないので、NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_ENDで囲われている部分はnonnullということになっています。基本的にはこれで囲んでおいて、nullableだけ明示的に書くのがよいでしょう。

これらのアノテーションを書いておくと、Swiftから見たときにImplicitly Unwrapped OptionalではなくOptionalかそうでないかはっきりします。さらにObjective-Cにおいても、nonnullとアノテートされたポインタにnilを入れようとしていれば静的解析で検出されます。ただし実行時には影響しません。


Swiftはまだ変わり続ける生きた言語です。Swift 1.2はSwiftの正式なリリース以来もっとも大きな変化で、開発者にとっても非常に歓迎すべき改善が数多くあります。今後のSwiftの進化に期待しながら、Swift 1.2でバリバリ開発していきたいですね。

詳解 Swift

詳解 Swift


いっしょにSwiftでバリバリ開発しませんか。

iOS/OS X 用ライブラリ UTIKit を OSS でリリースしました

UTI

UTI とは Uniform Type Identifier のことで、iOSOS X の世界で、ファイルなどの種類を表すために用いられる文字列です。

UTI の実例として、画像一般を表す public.image や、そのサブタイプで JPEG 画像を表す public.jpeg などがあります。iOSOS X で利用されるほとんどのファイル形式には対応する UTI があり、例えば Keynote ファイルは com.apple.iwork.keynote.key などとなります。もちろんサードパーティも例外ではなく、Adobe Photoshop のファイルは com.adobe.photoshop-image となり、Microsoft Word の docxorg.openxmlformats.wordprocessingml.document です。

iOSOS Xフレームワークは、ファイルの種類を表すのにこの UTI を利用することが多くあります。例えば Photos.framework から画像データを得たいというとき、PHImageManagerrequestImageDataForAsset(_:options:resultHandler:) メソッドを利用します。この resultHandler((NSData!, String!, UIImageOrientation, [NSObject : AnyObject]!) -> Void)! という型のクロージャで、クロージャの第2引数はドキュメントを読むと dataUTI となっています。すなわち、画像データの種類が UTI として得られます。

ところで、この UTI から拡張子MIME type や拡張子を取得するにはどうすればよいのでしょうか。画像をサーバーにアップロードするときには MIME type が必要かもしれません。例えば public.jpeg だったら image/jpeg という風な対応を丁寧に処理しても良いかもしれませんが、そもそも iOSOS X にはそういった関数が備わっています。func UTTypeCopyPreferredTagWithClass(inUTI: CFString!, inTagClass: CFString!) -> Unmanaged<CFString>! を利用することで UTI から MIME type や拡張子が得られます。

let mime = UTTypeCopyPreferredTagWithClass("public.jpeg", kUTTagClassMIMEType).takeRetainedValue() // => "image/jpeg"

let ext = UTTypeCopyPreferredTagWithClass("public.jpeg", kUTTagClassFilenameExtension).takeRetainedValue() // => "jpeg"

またここでは紹介しませんが、逆に MIME type や拡張子から UTI を得る関数もあります。非常に便利ですが、ごくたまにしか使わないので忘れがちです。筆者は毎回のように忘れて、過去のプロジェクトや IRC のログを検索して思い出します。

UTIKit

ということで、このような UTI に関連した処理を利用しやすくするラッパーライブラリが、UTIKit です。これを利用すると上記の処理が以下のようになります。

UTI("public.jpeg").MIMEType // => "image/jpeg"

UTI("public.jpeg").filenameExtension // => "jpeg"

ごく短くなる上に、インターフェースもわかりやすいですね。

もちろん MIME type や拡張子から UTI を得るのも簡単です。

let jpeg = UTI(MIMEType: "image/jpeg")

let jpeg = UTI(filenameExtension: "jpeg")

Equatable を実装しているためこれらふたつは同値です。

UTI(MIMEType: "image/jpeg") == UTI(filenameExtension: "jpeg") // => true

また、public.jpegpublic.image に従っているので、switch を利用して以下のように書けます。

switch UTI("public.jpeg") {
case UTI("public.image"):
    println("JPEG is a kind of images")
default:
    fatalError("JPEG must be a image")
}

これらのように、UTI が Swift ライクに扱えることがわかります。

ご利用方法

CocoaPods 0.36 以降でインストールできます。Podfile に以下の行を書き足しましょう。

pod "UTIKit"

また Carthage でもインストールできるかもしれません(試してない)。

MIT ライセンスです。どうぞご利用ください。

To Do