SwiftでNSTimerとかUIButtonを使うときはBlocksKit使うといいよ
Cocoaでは、引数にtarget
とselector
を取るメソッドがいくつかありますね。
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を使うことです。
- CocoaPodsなどでBlocksKitをプロジェクトに組み込む
- Objective-Cで実装されたBlocksKitをSwiftから使うために、ブリッジングヘッダ(
<#Project-name#>-Bridging-Header.h
)でBlokcsKitのヘッダを読み込む - BlocksKitが提供しているAPIを使う
コードで示します。これはクラッシュせずに動作し、SwiftClassがNSObjectを継承せずにNSTimerを使用できていることがわかります。
class SwiftClass { init() { NSTimer.bk_scheduledTimerWithTimeInterval(1.0, block: { (t) -> Void in println("hello") }, repeats: true) } }
BlocksKitを使用することで、コールバック関数を設定するコードとコールバック関数を同じ場所に書けますので、関心を集約することができます。 SwiftプロジェクトではBlocksKitの使用をオススメします。
違う話になるため細かい記述は避けますが、ブロックを使用する際に気をつけないと循環参照を引き起こすので、そこだけは気をつけましょう。
Swiftのアクセスコントロールはファイル/モジュール単位だから気ィ付けや
公式ドキュメントはこちら The Swift Programming Language: Access Control
※ドキュメント以上のことは記載されていません。
SwiftにもXcode beta4からアクセスコントロールが可能になりました。
private
とかpublic
とかinternal
といった修飾子を付けることで、外部に公開する必要のないメソッドやクラスを隠蔽できます。
private
やpublic
を指定するのは、主に「クラス」と「インスタンス変数」だと思います。
「外部に公開するクラスはパブリックにしよう」とか、「この変数はクラス外から参照される必要がないので、プライベートにしよう」とかですね。
ただし、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)") } }
これは、SampleClass
とPerson
を同一ファイルに記述したためです。
基本的には「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
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-CのDelegateの使用例となります。
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-CでDelegateが多用されているのは、Cocoaフレームワークで多用されていて慣れ親しんでいるからだろうと思います。
今後、Objective-Cの領域が徐々に狭くなっていくだろうことが想像されます。その中で、こういった通知の仕組みもSwiftフレンドリーな書き方に置き換わっていくんでしょうか。
SwiftでJSONを扱うライブラリ SwiftyJSON | json-swift
SwiftでJSONを扱う「つらみ」を解消するライブラリがトレンドにあがってたのでながめてみます。
つらみ
SwiftでJSONを扱うとなったとき、まず頭に浮かぶのがFoundationフレームワークのNSJSONSerialization
です。
JSONを渡すと、パースしてNSDictionary
やNSArray
のオブジェクトにしてくれます。個々の要素は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
型ではなくてSwiftのDictionary
やArray
やString
やNumber
でJSONを持てば良いのでは?とも思うのですが、そうは問屋が卸しません。
SwiftではArray
もDictionary
も、宣言した型の要素しか入れることはできません。Array<String>
や、Dictionary<String, Number>
などです。
「なんでもid型だからプリミティブじゃなければ何でも突っ込んでいーよ」っていうObjective-bitchとは違うんです。*3
そんなつらみがあります。
解決方法
Swiftには優秀なEnumがあります。EnumでDictionary
やArray
やString
やNumber
をラップしてあげれば解決です。
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の上のほうにまとまっている。
日本語
はてブのタグ検索
日本語の記事はだいたいココ見てたら流れてくる。と思う。Pressoで読んでもよい。
Vingow
みんな初心者!新言語「Swift」関連情報を最速で収集するたった1つの方法 | Vingow 開発チームブログ で紹介されていた。
要ユーザー登録。今度登録する。
今すぐフォローすべき天才Swiftエンジニアのリストを作るには時期尚早すぎるので、とりあえずFollow me on twitter. と言っておこう。
英語
GithubのExplore
Trending Swift repositories on GitHub today · GitHub
Githubには話題のリポジトリを一覧できるページがあるのはご存知かと思う。 このページでは便利なことに、言語指定が出来る。しかもすでにSwiftを指定できる。
発表されてからまだ数日しか経っていないのに、コードを読む機会が十分にあるというのは素晴らしい。 話題のFlappySwiftやswift-2048もここで読める。
ほかにも、もう関数型Swift?とか、もうUnderscorejsコピー?とか、もうBDDフレームワーク?とか、見てて飽きない。お祭り感がある。
http://www.reddit.com/r/swift/
外人があーだこーだ言ってるのを眺めることができる。
Stack Overflow
Newest 'swift-language' Questions - Stack Overflow
タグ検索でSwiftに絞り込む。さすがStack Overflow, 質問の数も回答の速さも半端ない。 Swiftはググラビリティ低い*1ので、なにか困って検索したいときはググるよりも先にここで検索したほうが幸せになれる。
ちなみにどうしてもググりたいときは、検索ツールで期間を指定すればいい。Swift発表日以降のページのみ表示すればノイズはほぼ除外できる。
So So Swift
手作業で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で置き換えようと思っている人は、盛り上がっているスレッドはひと通り目を通しておくべきだ。