定食屋おろポン

おろしポン酢と青ネギはかけ放題です

SwiftでNSTimerとかUIButtonを使うときはBlocksKit使うといいよ

Cocoaでは、引数にtargetselectorを取るメソッドがいくつかありますね。

  • NSTimer+ scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
  • UIControl- addTarget:action:forControlEvents:

こういうメソッドを、純粋なSwiftのクラス--つまり、NSObjectを継承していないクラス--で使おうとすると、クラッシュを引き起こします。*1

実際にこのコードは実行時にクラッシュします。

class SwiftClass {
    init() {
        NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("privateFunc"), userInfo: nil, repeats: true)
    }
    
    private func privateFunc() {
        println("hello")
    }
}

これを回避するには、単純にNSObjectから継承すれば済みます。 もう一つの手段としては、BlocksKitを使うことです。

  1. CocoaPodsなどでBlocksKitをプロジェクトに組み込む
  2. Objective-Cで実装されたBlocksKitをSwiftから使うために、ブリッジングヘッダ(<#Project-name#>-Bridging-Header.h)でBlokcsKitのヘッダを読み込む
  3. BlocksKitが提供しているAPIを使う

コードで示します。これはクラッシュせずに動作し、SwiftClassがNSObjectを継承せずにNSTimerを使用できていることがわかります。

class SwiftClass {
    init() {
        NSTimer.bk_scheduledTimerWithTimeInterval(1.0, block: { (t) -> Void in
            println("hello")
        }, repeats: true)
    }
}

BlocksKitを使用することで、コールバック関数を設定するコードとコールバック関数を同じ場所に書けますので、関心を集約することができます。 SwiftプロジェクトではBlocksKitの使用をオススメします。

違う話になるため細かい記述は避けますが、ブロックを使用する際に気をつけないと循環参照を引き起こすので、そこだけは気をつけましょう。

CocoaDocs.org - BlocksKit Reference

*1:レシーバオブジェクトとセレクタからメソッドを呼び出す際に、NSObjectに実装されているメソッドに依存しているためです。また、正確にいうと「NSOjbectを継承していないオブジェクトをターゲットに渡すと」クラッシュします。

Swiftのアクセスコントロールはファイル/モジュール単位だから気ィ付けや

公式ドキュメントはこちら The Swift Programming Language: Access Control

※ドキュメント以上のことは記載されていません。

SwiftにもXcode beta4からアクセスコントロールが可能になりました。 privateとかpublicとかinternalといった修飾子を付けることで、外部に公開する必要のないメソッドやクラスを隠蔽できます。

privatepublicを指定するのは、主に「クラス」と「インスタンス変数」だと思います。 「外部に公開するクラスはパブリックにしよう」とか、「この変数はクラス外から参照される必要がないので、プライベートにしよう」とかですね。

ただし、Swiftのアクセスコントロールはファイル/モジュール単位であって、クラス単位ではありません!!

各修飾子とアクセスレベルの対応は表のようになります。

アクセス修飾子 アクセス可能範囲
public モジュールの外からもアクセスできる
internal モジュールの内部ならどこからでもアクセスできる
private 同じファイル内からのみアクセスできる

なお、Swiftでいうところの「モジュール」とはザックリ言うとライブラリ(フレームワーク)です。

コードで見てみましょう。 Personクラスでは、name変数やgreeting変数はprivate宣言されています。 にも関わらず、SampleClassの中から変数もメソッドもアクセスできてしまっています。エラーもワーニングも起きません。

AccessControlSample.swift

internal class SampleClass {
    internal func sampleMethod() {
        // インスタンス化する
        let taro :Person = Person(name: "Taro")
        let mary :Person = Person(name: "Mary")

        // プライベートメソッドを呼ぶ
        taro.greeting() //=> My name is Taro
        mary.greeting() //=> My name is Mary

        // プライベートインスタンス変数にアクセスする
        println("taro.name: \(taro.name)") //=> taro.name: Taro
        println("mary.name: \(mary.name)") //=> mary.name: Mary
    }
}

private class Person {
    private let name :String

    init(name: String) {
        self.name = name
    }

    private func greeting() {
        println("My name is \(self.name)")
    }
}

これは、SampleClassPersonを同一ファイルに記述したためです。 基本的には「1クラスにつき1ファイル」を記述するので、普段はあまり問題になりませんが。

デフォルトのアクセスレベル

明示的にアクセスレベルを指定しない場合はinternalとなります。

余談

公式ドキュメントに「ターゲットがひとつしかないアプリでは、デフォルトのInternalのママで大体事足りるから、アクセスレベルを明示的に指定しなくていいよー。まぁ、モジュール内の他のコードから実装隠したいときはprivate付けておきなね。」的なことが書いてあります。

これは賛同しかねます。フレームワーク作者ではなくても、実装するクラスで「何がinternalで何がprivateなのか」はとても大事な情報だと思いますよ。

特に、Swiftではヘッダーファイルが無いため、「このクラスのAPIはどれなんだろう」というのが非常に分かりづらいです。 「ファイル外(≒クラス外)からアクセスすることを想定していない変数やメソッド」は全てprivate指定するほうがよいでしょう。

プライベート変数・メソッドはアンダースコアから始めるっていうコーディング規約が広まらないかなあ。

Swiftでも#pragma mark 的なのが使えます

もろもろの対応状況

ジャンプバーの見出しとセパレータ

メニューのView→Show Document Itemsで表示されるアレです。

// Objective-C:
#pragma mark - hogehoge -

// Swift: 
// MARK: - hogehoge -

TODO, FIXME

今までと同様です。

// Objective-C, Swift
// TODO: todo
// FIXME: fixme

!!!, ???

Swiftでは未対応で、ジャンプバーに表示されません。

// Objective-C
// !!!: message
// ???: message

強制的なエラー,ワーニング

Objective-Cでは、プリプロセッサマクロによってユーザーが明示的にエラー,ワーニングを出すことができましたが、Swiftでは今のところ*1同様の機能がありません。

// Objective-C
#error message
#warning message

*1:Xcode 6 GM seed

Swift時代のDelegate通知

Objective-C時代のDelegate

今は昔、Objective-Cの時代ではOwnerへの通知にDelegateを使用していました。 言語仕様としてDelegate機構が用意されているわけでも、Delegateは通知のための機構であるというわけでもなく、CocoaフレームワークDelegateパターンが通知に多用されていた、ということです。

Delegateの実装としては、概ね下記のようになっています。


通知を受け取るReceiverと、通知を送るSenderがいます。

Senderはプロパティとしてdelegateをもっており、delegateプロパティにはReceiverがセットされます。

また、Senderのヘッダ内で、「Delegateがどのような通知に対して処理が出来るのか」 = 「Delegateが受け取ることができるメソッド」 が定義されます。 定義には、NSObjectクラスのカテゴリか、プロトコルを使用します。 プロトコルで定義するメソッドには、「Required: Delegateに実装されていないといけないメソッド」と「Optional: 通知は送るが、実装されていなくてもよいメソッド」の二種類があります。

Receiverでは、定義されたメソッドを実装し、通知を受け取ったときの処理を記述します。

通知を送る処理はSenderで実装します。単純にdelegateプロパティにメッセージを送るだけなので、こんなかんじです。

[self.delegate someMethod];

ただし、delegateプロパティ = Receiver がsomeMethodを実装していない場合、存在しないメソッドを呼び出すこととなり、クラッシュを引き起こします。 当然クラッシュは好ましくないため、respondsToSelector:を使い、メソッドを呼ぶ前にメソッドの存在確認を行なうのが通例です。

if ([self.delegate respondsToSelector:@selector(someMethod)]) {
    [self.delegate someMethod];
}

これが典型的なObjective-CDelegateの使用例となります。


Swift時代のDelegate

まず、SwiftでもObjective-CスタイルのDelegateの実装は可能です。

コードで示します。

// Protocolの定義
@objc protocol SampleDelegate {
    func requiredMethod() -> ()
    optional func optionalMethod() -> ()
}

// Receiver: 通知を受け取る方
class Receiver :SampleDelegate { //SampleDelegateプロトコルに準拠していることを示す
    let sender :Sender = Sender()

    init() {
        // Senderのdelegateに自分を指定
        sender.delegate = self

        // senderに通知を送ってもらう
        sender.notify1()
        sender.notify2()
    }

    // SampleDelegateプロトコルに準拠しているので、`requiredMethod`を実装しないとコンパイルできない
    func requiredMethod() {
        println("required method was called")
    }

    // `optionalMethod`はoptionalなので実装しなくてもよい
}

// Sender: 通知を送る方
class Sender {
    // delegate にはSampleDelegateプロトコルに準拠したオブジェクトかnilが入る
    weak var delegate :SampleDelegate? = nil

    // requiredなメソッドを呼び出す
    func notify1() {
        self.delegate?.requiredMethod()
    }

    // optionalなメソッドを呼び出す
    func notify2() {
        self.delegate?.optionalMethod?() // メソッド呼び出しに?が付いているのがキモ
    }
}

プロトコル定義に@objc属性が付いているのが嫌なかんじです。 これは、SwiftプロトコルではOptionalを指定できないために付けています。*1

Swiftのみで完結させようとすると、プロトコルに定義したメソッドがすべてRequiredになってしまうということですね。

カジュアルにDelegateメソッドをバンバン追加していると、「これ全部実装しないといけないのかよ」となり、使い勝手が非常に悪いです。

UIScrollViewDelegateの定義なんてこんなんですよ。これでもマシなほうですけど。

@protocol UIScrollViewDelegate<NSObject>

@optional

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;                                               // any offset changes
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); // any zoom scale changes

// called on start of dragging (may require some time and or distance to move)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
// called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;   // called on finger up as we are moving
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;      // called when scroll view grinds to a halt

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView; // called when setContentOffset/scrollRectVisible:animated: finishes. not called if not animating

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;     // return a view that will be scaled. if delegate returns nil, nothing happens
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view NS_AVAILABLE_IOS(3_2); // called before the scroll view begins zooming its content
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale; // scale between minimum and maximum. called after any 'bounce' animations

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;   // return a yes if you want to scroll to the top. if not defined, assumes YES
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;      // called when scrolling animation finished. may be called immediately if already at top

@end

これが全てRequiredだったら..と考えるとゾッとします。 メソッドの中身は空でもいいけど、とにかく全部実装してあげないとコンパイルが通らないんですからね。

今後

当面は@objcを使っていればよいのですが、今後どうなっていくんでしょうか。

ニュービーなので実際のところはよくわかりませんが、Objective-CDelegateが多用されているのは、Cocoaフレームワークで多用されていて慣れ親しんでいるからだろうと思います。

今後、Objective-Cの領域が徐々に狭くなっていくだろうことが想像されます。その中で、こういった通知の仕組みもSwiftフレンドリーな書き方に置き換わっていくんでしょうか。

SwiftでJSONを扱うライブラリ SwiftyJSON | json-swift

SwiftJSONを扱う「つらみ」を解消するライブラリがトレンドにあがってたのでながめてみます。

つらみ

SwiftJSONを扱うとなったとき、まず頭に浮かぶのがFoundationフレームワークNSJSONSerializationです。 JSONを渡すと、パースしてNSDictionaryNSArrayのオブジェクトにしてくれます。個々の要素はNSString,NSNumber,あるいはNSNullといったオブジェクトに変換されます。

SwiftではObjective-Cのオブジェクトを扱うことができるので、NSJSONSerializationを使ってパースしたらSwiftでもJSONを扱えるはずです。

さて、ではコレをつかって書いてみましょう。サンプルにはTwitter APIのサンプルレスポンスを使います。

https://dev.twitter.com/docs/api/1.1/get/users/show

ここから、screen_nameと、status/entities/urls/[0]/urlを取ってみます。

import Foundation

let sampleJSONData :NSData = NSData(contentsOfFile: "/path/to/sampleJSON.json")
let sampleJSON :Dictionary = NSJSONSerialization.JSONObjectWithData(sampleJSONData, options: nil, error: nil) as NSDictionary

var screen_name :String
var url :String

// Get screen_name
screen_name = sampleJSON["screen_name"] as NSString

// Get status/entities/urls/[0]/url
if let status :Dictionary = sampleJSON["status"] as? NSDictionary {
    if let entities :Dictionary = status["entities"] as? NSDictionary {
        if let urls :Array = entities["urls"] as? NSArray {
            if let urlSet :Dictionary = urls[0] as? NSDictionary {
                if let anUrl :String = urlSet["url"] as? NSString {
                    url = anUrl
                }
            }
        }
    }
}

書けました!

つ、つらい。。キャストつらいし、なんていうかつらい。*1 全てid型にぶっ込むObjective-Cのつらみとも言えます。*2

では、いわゆるObject型ではなくてSwiftDictionaryArrayStringNumberJSONを持てば良いのでは?とも思うのですが、そうは問屋が卸しません。

SwiftではArrayDictionaryも、宣言した型の要素しか入れることはできません。Array<String>や、Dictionary<String, Number>などです。 「なんでもid型だからプリミティブじゃなければ何でも突っ込んでいーよ」っていうObjective-bitchとは違うんです。*3

そんなつらみがあります。

解決方法

Swiftには優秀なEnumがあります。EnumDictionaryArrayStringNumberをラップしてあげれば解決です。

SwiftyJSON

こんなかんじ

let sampleJSONData :NSData = NSData(contentsOfFile: "/path/to/sampleJSON.json")
let sampleJSON :JSONValue = JSONValue(sampleJSONData)
let screen_name :String? = sampleJSON["screen_name"].string
let url :String? = sampleJSON["status"]["entities"]["urls"][0]["url"].string

簡潔な記述がよいかんじです。列挙型JSONValueでラップし、.string.integerなどでアンラップします。

json-swift

こんなかんじ

let sampleJSONStringObj :String = NSString(contentsOfFile: "/path/to/sampleJSON.json")
let sampleJSON :JSON? = JSON.parse(sampleJSONStringObj)

var screen_name :String?
var url :String?

if let json :JSON = sampleJSON {
    screen_name =  json["screen_name"]?.string
    url = json["status"]?["entities"]?["urls"]?[0]?["url"]?.string
}

SwiftyJSONと大して変わりません。列挙型JSONでラップし、.string.numberなどでアンラップします。

違い

json-swiftの方はサンプルコードを見ての通り、?を多用するスタイルです。subscript(添字アクセス)がnil返すかもしれないってのがパッと見て分かるって利点はあるかもしれないですが、面倒くさいですね。

SwiftyJSONは、パースや添字アクセスには常にJSONValueを返す(要素が無かったりしたらJSONValue.JInvalidが入ります)ので、いちいち?をつける必要がありません。

まとめ

Enumよい

*1:うおおおお、Objective-Cより簡潔で読みやすい!!!と思わなくもないけど

*2:気づいたら同じことがSwiftyJSONのREADMEに書いてありました。悲しい。

*3:僕はObjective-C好きですよ

Swiftのエラーハンドリング

Objective-Cにおいては、「Exceptionは処理するな」が鉄則です。 つまり、

  • 「例外を投げるのは致命的な問題(復旧不可能な問題)が発生したときのみ」
  • 「例外を受け取ったらアプリを終了させろ」

というポリシーです。

これは、そもそも例外を受け取ってキャッチ句にジャンプすること自体が、ARCによるメモリ管理を壊してしまうことに起因しています。(と理解しています。)

http://clang.llvm.org/docs/AutomaticReferenceCounting.html#exceptions

By default in Objective C, ARC is not exception-safe for normal releases:

ところで、Swiftではどうでしょうか。 Swiftのリファレンスを眺めてもエラーハンドリングに頁が割かれていません。

上記のポリシーはSwiftでも同様であり、「例外を受けとったらアプリを殺せ」はまったく変わらないようです。

https://devforums.apple.com/thread/227375?start=25&tstart=0

Cocoa uses exceptions only for effectively unrecoverable error cases. In most circumstances it doesn't make sense to try to catch an exception and continue execution; ARC will leak references, and framework state will be inconsistent. You should check for exceptional circumstances before invoking methods that may throw exceptions.

SwiftもGCではなくARCによるメモリ管理を採用しているため、仕方がないですね。

Swift情報の収集方法

情報源メモ。初心者向けチュートリアルは他の記事に任せて、Swiftの情報がなんか気になって仕方がない人達のために書いた。

Appleの公式ドキュメント

とりあえずここらへんは基本。

iBooks

iTunes - Books - The Swift Programming Language by Apple Inc.

ドキュメント

[iOS][Mac] Swift を学べる記事のまとめ | Developers.IOの上のほうにまとまっている。

日本語

はてブのタグ検索

Swiftタグで検索

日本語の記事はだいたいココ見てたら流れてくる。と思う。Pressoで読んでもよい。

Vingow

みんな初心者!新言語「Swift」関連情報を最速で収集するたった1つの方法 | Vingow 開発チームブログ で紹介されていた。

要ユーザー登録。今度登録する。

Twitter

今すぐフォローすべき天才Swiftエンジニアのリストを作るには時期尚早すぎるので、とりあえずFollow me on twitter. と言っておこう。

英語

GithubのExplore

Trending Swift repositories on GitHub today · GitHub

Githubには話題のリポジトリを一覧できるページがあるのはご存知かと思う。 このページでは便利なことに、言語指定が出来る。しかもすでにSwiftを指定できる。

発表されてからまだ数日しか経っていないのに、コードを読む機会が十分にあるというのは素晴らしい。 話題のFlappySwiftやswift-2048もここで読める。

ほかにも、もう関数型Swift?とか、もうUnderscorejsコピー?とか、もうBDDフレームワーク?とか、見てて飽きない。お祭り感がある。

Reddit

http://www.reddit.com/r/swift/

外人があーだこーだ言ってるのを眺めることができる。

Stack Overflow

Newest 'swift-language' Questions - Stack Overflow

タグ検索でSwiftに絞り込む。さすがStack Overflow, 質問の数も回答の速さも半端ない。 Swiftはググラビリティ低い*1ので、なにか困って検索したいときはググるよりも先にここで検索したほうが幸せになれる。

ちなみにどうしてもググりたいときは、検索ツールで期間を指定すればいい。Swift発表日以降のページのみ表示すればノイズはほぼ除外できる。

So So Swift

Sososwift

手作業でSwiftの記事をまとめているようだ。チュートリアル記事に力をいれている模様。

Developer Forums

https://devforums.apple.com/community/tools/languages/swift

要ログイン。今Swift触ってる人はみんなiOS Developer Programに参加してお布施してるだろうから問題無いけど。

最後に挙げたけど、今の段階ではこれが一番重要と言っていい。 なぜなら、Appleのエンジニアがせっせと質問に回答してくれているからだ。

Appleのエンジニアによる回答だから情報の信頼性が高いというのも確かにあるけど、もっと重要なのは「まだSwiftは少なからずバグがある」ということと、「SwiftはAppleが開発していて、既知のバグを知るすべはない」という事実だ。 本の虫: Appleが新言語、Swiftを発表するも、すでに閉鎖的すぎて絶望しかないのにもうなずける。

まだSwiftは開発途上なので、不可解な挙動にハマったとしても「Swiftが悪いのか、自分のコードが悪いのか」は区別がつかない。「目玉の数さえ十分あれば、どんなバグも深刻ではない」としても、目玉の数がまだ全然足りていないのだ。*2

いくつかスレッドを挙げる。(当然のことだが)まだ実装もドキュメントも未成熟な言語だということが分かるはずだ。 https://devforums.apple.com/thread/228763?tstart=0 https://devforums.apple.com/thread/227288?tstart=15 https://devforums.apple.com/thread/227468?tstart=15 https://devforums.apple.com/thread/227425?tstart=0

今からSwiftでアプリを書いたり、既存アプリをSwiftで置き換えようと思っている人は、盛り上がっているスレッドはひと通り目を通しておくべきだ。

*1:Googleが作ったあの言語とあの言語よりはマシ

*2:iOS8のリリースまでにはほぼ解消されると思うけど。