95
ゲンバのSwift Feb 19, 2015 クラスメソッド株式会社 安達 勇一

ゲンバのSwift

Embed Size (px)

Citation preview

ゲンバのSwift

Feb 19, 2015

クラスメソッド株式会社安達 勇一

安達 勇一• 2013/12 入社

• TwitterID:   @UsrNameu1

• Github:    https://github.com/UsrNameu1

• Blog:    http://dev.classmethod.jp/author/yad

•        で連載やってます

Topics for today

• UIKitのSelector

• TestでのMock

• iOS7でのSwift OSS

• Moduleの穴

• 後方互換性

UIKitのSelector

• UIKitのイベントハンドリング

• SwiftでのSelector

• Objective-Cとの比較

• BlocksKit

UIKitでのイベントハンドリング

• UIControl, UIBarButtonItem の target, action

navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("cancel", comment: ""), style: .Bordered, target: self, action: Selector(“cancelButtonDidTapped:”));

UIKitでのイベントハンドリング

• UIControl, UIBarButtonItem の target, action

navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("cancel", comment: ""), style: .Bordered, target: self, action: Selector(“cancelButtonDidTapped:”));

UIKitでのイベントハンドリング

• UIControl, UIBarButtonItem の target, action

navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("cancel", comment: ""), style: .Bordered, target: self, action: Selector(“cancelButtonDidTapped:”));

UIKitのAPIのイベントハンドリングでは Selectorが要を握る

SwiftでのSelector

• Selectorの定義

struct Selector : StringLiteralConvertible, NilLiteralConvertible { ...

SwiftでのSelector

• Selectorの定義

struct Selector : StringLiteralConvertible, NilLiteralConvertible { ...

"buttonDidTouchUpInside:"リテラルで宣言可能→

SwiftでのSelector

• Selectorの定義

struct Selector : StringLiteralConvertible, NilLiteralConvertible { ...

"buttonDidTouchUpInside:"

Selectorを指定しない時はnilを入れられる→ nil

リテラルで宣言可能→

• リテラルで宣言可能

navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("cancel", comment: ""), style: .Bordered, target: self, action: “cancelButtonDidTapped:");

SwiftでのSelector

• Selectorを指定しない時はnilを入れられる

navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("cancel", comment: ""), style: .Bordered, target: self, action: nil);

SwiftでのSelector

Objective-Cとの比較

• Objective-C

• Swift

[removeButton addTarget:self action:@selector(removeButtonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];

removeButton.addTarget(self, action: Selector("removeButtonDidTouchUpInside:"), forControlEvents: .TouchUpInside)

Objective-Cとの比較

• Objective-C

• Swift

[removeButton addTarget:self action:@selector(removeButtonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];

removeButton.addTarget(self, action: Selector("removeButtonDidTouchUpInside:"), forControlEvents: .TouchUpInside)

⇧Selector名が間違っていたら コンパイラが警告を出してくれる

Objective-Cとの比較

• Objective-C

• Swift

[removeButton addTarget:self action:@selector(removeButtonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];

removeButton.addTarget(self, action: Selector("removeButtonDidTouchUpInside:"), forControlEvents: .TouchUpInside)

⇧Selector名が間違っていたら コンパイラが警告を出してくれる

⇧Selector名が間違っていても コンパイラは警告を出してくれない

Objective-Cとの比較

• Objective-C

• Swift

[removeButton addTarget:self action:@selector(removeButtonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];

removeButton.addTarget(self, action: Selector("removeButtonDidTouchUpInside:"), forControlEvents: .TouchUpInside)

⇧Selector名が間違っていたら コンパイラが警告を出してくれる

⇧Selector名が間違っていても コンパイラは警告を出してくれない

BlocksKit

• Podfile

• Brigdeing Header

pod 'BlocksKit/UIKit'

#import <BlocksKit/BlocksKit+UIKit.h>

BlocksKit

• BeforeremoveButton.addTarget(self, action: Selector("removeButtonDidTouchUpInside:"), forControlEvents: .TouchUpInside)

⇧Selector名が間違っていても コンパイラは警告を出してくれない

BlocksKit

• Before

• After

removeButton.addTarget(self, action: Selector("removeButtonDidTouchUpInside:"), forControlEvents: .TouchUpInside)

⇧Selector名が間違っていても コンパイラは警告を出してくれない

removeButton.bk_addEventHandler({[weak self] sender in self?.removeButtonDidTouchUpInside(sender) return Void() }, forControlEvents: .TouchUpInside)

⇧ハンドラの中でメソッドを直に呼び出すため、 メソッド名が間違っていたらコンパイラはエラーになる

• 循環参照によるリーク

BlocksKit

• weak, unownedで解決

BlocksKit

removeButton.bk_addEventHandler({[weak self] sender in self?.removeButtonDidTouchUpInside(sender) return Void() }, forControlEvents: .TouchUpInside)

• weak, unownedで解決

BlocksKit

removeButton.bk_addEventHandler({[weak self] sender in self?.removeButtonDidTouchUpInside(sender) return Void() }, forControlEvents: .TouchUpInside)

・ weak : selfがなくなった後もアクセスされる場合 ・ unowned : selfがなくなった後にアクセスされない場合

TestでのMock

• iOSでのテスト実施

• 2つのFramework : Quick, Nimble

• SwiftでのMock実現方法

iOSでのテスト実施

• 設計、実装の手助けの為

• Model層に対して実施

• 挙動が明らかなもの(イニシャライザ等)については省略した

2つのFramework : Quick, Nimble

• QuickはテストのためのBDD流DSLを用意

• Nimbleは実際の値と期待する値を比較するマッチャー

• テストはプロダクトコードに含まれない為、    iOS 8 向けにインストール

2つのFramework : Quick, Nimble

• 病院にかかわる一連のモデル

create

confirmuse

public struct Diagnosis { public let name: String public init(name: String) { self.name = name } }

public protocol Examinable { func examine() -> Diagnosis }

public class Doctor: Examinable { public init() {} public func examine() -> Diagnosis { return Diagnosis(name: "cold") } }

public class Hospital { private let examinable: Examinable public init(examinable: Examinable) { self.examinable = examinable } public func serve() -> Diagnosis { return examinable.examine() } }

https://github.com/UsrNameu1/QuickStudy

2つのFramework : Quick, Nimble

• 診断データ型

public struct Diagnosis { public let name: String public init(name: String) { self.name = name } }

2つのFramework : Quick, Nimble

• 診断者プロトコル

public protocol Examinable { func examine() -> Diagnosis }

2つのFramework : Quick, Nimble

• 医者クラス

public class Doctor: Examinable { public init() {} public func examine() -> Diagnosis { return Diagnosis(name: "cold") } }

2つのFramework : Quick, Nimble

• 病院クラス

public class Hospital { private let examinable: Examinable public init(examinable: Examinable) { self.examinable = examinable } public func serve() -> Diagnosis { return examinable.examine() } }

2つのFramework : Quick, Nimble

• BDD Example

class HospitalSpecs: QuickSpec { override func spec() { describe("病院の診断について") { context("診察できる人が診断を行うとき") { let doctor = Doctor() let hospital = Hospital(examinable: doctor) it("診察できる人の診断を返すこと。") { expect(hospital.serve().name) == "cold" } } } } }

SwiftでのMock実装例class HospitalSpecs: QuickSpec { // protocol準拠でMockを使う override func spec() { describe("病院の診断について") { context("診察できる人が診断を行うとき") { class MockExaminable: Examinable { override func examine() -> Diagnosis { return Diagnosis(name: "cold") } } let mock = MockExaminable() let hospital = Hospital(examinable: mock) it("診察できる人の診断を返すこと。") { expect(hospital.serve().name) == "cold" } } } } }

SwiftでのMock実装例class HospitalSpecs: QuickSpec { // protocol準拠でMockを使う override func spec() { describe("病院の診断について") { context("診察できる人が診断を行うとき") { class MockExaminable: Examinable { override func examine() -> Diagnosis { return Diagnosis(name: "cold") } } let mock = MockExaminable() let hospital = Hospital(examinable: mock) it("診察できる人の診断を返すこと。") { expect(hospital.serve().name) == "cold" } } } } }

SwiftでのMock実装例class HospitalSpecs: QuickSpec { // 継承でMockを使う override func spec() { describe("病院の診断について") { context("診察できる人が診断を行うとき") { class MockDoctor: Doctor { override func examine() -> Diagnosis { return Diagnosis(name: "headache") } } // HospitalがDoctorを直接用いている時も使える let mock = MockDoctor() let hospital = Hospital(examinable: mock) it("診察できる人の診断を返すこと。") { expect(hospital.serve().name) == "headache" } } } } }

SwiftでのMock実装例class HospitalSpecs: QuickSpec { // 継承でMockを使う override func spec() { describe("病院の診断について") { context("診察できる人が診断を行うとき") { class MockDoctor: Doctor { override func examine() -> Diagnosis { return Diagnosis(name: "headache") } } // HospitalがDoctorを直接用いている時も使える let mock = MockDoctor() let hospital = Hospital(examinable: mock) it("診察できる人の診断を返すこと。") { expect(hospital.serve().name) == "headache" } } } } }

SwiftでのMock実装例

• protocol, subclassは一長一短

• protocol準拠のMockの場合、@objc とoptionalをつけない限り必須実装をモックにもれなく実装する必要がある

• subclassのMockの場合、ネストの深い箇所ではSegmentation fault 11が起こるケースがあった

iOS7でのSwift OSS

• OSSのインストール

• iOS7でOSSを使う

• 使用中のOSS

OSSのインストール

• git submodule

• Cocoapods (0.36以上)

• Carthage

OSSのインストール

• git submodule

• Cocoapods (0.36以上)

• Carthage

←プロジェクトではこれを利用

←iOS8以降

←iOS8以降

OSSのインストール

• git submodule でのOSS導入例(Alamofire)

$ cd (Project dir)$ git submodule add https://github.com/

Alamofire/Alamofire.git

詳しくはWebで!  http://dev.classmethod.jp/references/swift-oss-

alamofire-1/

iOS7でOSSを使う

https://github.com/CocoaPods/swift/issues/9

iOS7でOSSを使う

iOS7でOSSを使う

https://github.com/CocoaPods/swift/issues/9

iOS7でOSSを使う

• iOS8 では Dynamic Library の形式では使えないので直にソースファイルをプロジェクトに入れる必要がある

• 極力 git submodule の更新のみでファイル追従を行うためにCopyせずにソースファイルへの参照をリンク

iOS7でOSSを使う

iOS7でOSSを使う

チェックを外して参照のみ保持

使用中のOSS

• Alamofire : HTTP通信のためのOSS

• SwiftyJSON : JSONハンドリングのためのOSS

• BrightFutures : 非同期処理のためのOSS

• Quick : テストのDSLを提供するOSS

• Nimble : テストのマッチャ−を提供するOSS

• Alamofire

• SwiftyJson

• BrightFutures

• Quick

• Nimble

テストターゲットのみに含まれる為、 テストをiOS8向けとして Dynamic frameworkでプロジェクトに入れた

使用中のOSS

• Alamofire

• SwiftyJson

• BrightFutures

• Quick

• Nimble

テストターゲットのみに含まれる為、 テストをiOS8向けとして Dynamic frameworkでプロジェクトに入れた

アプリターゲットの対象に iOS7が含まれるため frameworkとしてではなく、 ソースの参照を保持するようにした

使用中のOSS

使用中のOSS : Alamofire

https://github.com/Alamofire/Alamofire

使用中のOSS : Alamofire

https://github.com/Alamofire/Alamofire

使用中のOSS : SwiftyJSON

https://github.com/SwiftyJSON/SwiftyJSON

使用中のOSS : SwiftyJSON

https://github.com/SwiftyJSON/SwiftyJSON

使用中のOSS : BrightFutures

https://github.com/Thomvis/BrightFutures

使用中のOSS : BrightFutures

https://github.com/Thomvis/BrightFutures

• Before

User.logIn(username, password) { user, error in if !error { Posts.fetchPosts(user, success: { posts in // do something with the user's posts }, failure: handleError) } else { // handeError is a custom function to handle errors handleError(error) }}

使用中のOSS : BrightFutures

https://github.com/Thomvis/BrightFutures

• After

User.logIn(username,password).flatMap { user in Posts.fetchPosts(user)}.onSuccess { posts in // do something with the user's posts}.onFailure { error in // either logging in or fetching posts failed}

Moduleの穴• クラス名だけを文字列で取得する

• NSManagedObject in Test

• Storyboard & InterfaceBuiler

クラス名だけを文字列で取得する

• SwiftでNSObjectのクラス名を取得する際にはモジュール名も含まれる

クラス名だけを文字列で取得する

// モジュール名.Sample が得られる let nameWithModule = NSStringFromClass(Sample.self) // Sampleのみを取得したい let nameWithoutModule = Sample.nameForClass

クラス名だけを文字列で取得する

• SwiftでNSObjectのクラス名を取得する際にはモジュール名も含まれる

• 従来のクラス名取得機能についてはエクステンションで実装

クラス名だけを文字列で取得する

extension NSObject { /// クラス名をモジュール名を取り除いて取得します。 public class var nameForClass: String { return NSStringFromClass(self) .componentsSeparatedByString(".").last! } }

NSManagedObject in Test

• NSManagedObjectサブクラスを用いたクラスのテストで実行時に落ちる

NSManagedObject in Test

• NSManagedObjectサブクラスを用いたクラスのテストで実行時に落ちる

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'executeFetchRequest:error: A fetch request must have an entity.' *** First throw call stack: ( 0 CoreFoundation 0x000000010387ef35 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x0000000103517bb7 objc_exception_throw + 45 2 CoreData 0x00000001025e137d -[NSManagedObjectContext executeFetchRequest:error:] + 4541

NSManagedObject in Test

• NSManagedObjectサブクラスを用いたクラスのテストで実行時に落ちる

• Objective-Cから見た時のNSObjectサブクラス名もSwiftのクラスに対してはModule名.クラス名

• @objc()キーワードを使ってObjective-Cから見た時のNSObjectサブクラス名を変更する

NSManagedObject in Test

• Before

class Product: NSManagedObject { @NSManaged var name: String}

NSManagedObject in Test

• Before

• After

class Product: NSManagedObject { @NSManaged var name: String}

@objc(Product)class Product: NSManagedObject { @NSManaged var name: String}

NSManagedObject in Test

• Before

• After

class Product: NSManagedObject { @NSManaged var name: String}

@objc(Product)class Product: NSManagedObject { @NSManaged var name: String}

@objc()キーワードを使ってObjective-Cから見た時のNSObjectサブクラス名を変更

Storyboard & InterfaceBuilder

• Storyboardで定義したInitialViewControllerに接続したUINavigationControllerのrootViewControllerがうまく初期化されない

Storyboard & InterfaceBuilder

• Xcode 6 より追加されたModule入力欄にターゲットモジュールを入力

• またはViewController宣言の前に            @objcでObjective-Cから見た            クラス名を記述

http://stackoverflow.com/questions/24924966/xcode-6-strange-bug-

unknown-class-in-interface-builder-file 

後方互換性• CIとマイナーアップデート

• Swift 1.2

• Chris Lattnerの哲学

CIとマイナーアップデート

• CIサービスはTravisを利用

• Xcodeのマイナーアップデート6.1.0 →6.1.1によるトラブル (Travis側がデフォルトで6.1.0だった)

CIとマイナーアップデート

• Travisがすぐに対応してくれた

• Twitterのアカウントで最新情報にキャッチアップ https://twitter.com/travisci

osx_image: xcode611

CIとマイナーアップデート

• 0.0.1単位のマイナーアップデートでも      XcodeでOptionalの変更がよくある

CIとマイナーアップデート

• 0.0.1単位のマイナーアップデートでも      XcodeでOptionalの変更がよくある

var textLabel: UILabel? { get }

var textLabel: UILabel! { get } Xcode 6.1.0

Xcode 6.1.1

Swift 1.2

• Incremental Buildが実現!

• より便利になった if let !

• 集合データ構造Set<T>! 

Swift 1.2

• Incremental Buildが実現!

• より便利になった if let !

• 集合データ構造Set<T>! 

• Objective-Cの諸クラスからの暗黙的型変換禁止!

Swift 1.2

• Objective-Cの諸クラスからの暗黙的型変換禁止!

func mangleString(input: String) { // do something with input} let someString: NSString = "hello" mangleString(someString) // compile error!

Swift 1.2

• Objective-Cの諸クラスからの暗黙的型変換禁止!

func mangleString(input: String) { // do something with input} let someString: NSString = "hello" mangleString(someString as! String)

Chris Lattnerの哲学

• Chris Lattner:Swiftつくった人

• コンパイラ基盤LLVMの作者でもある

Chris Lattnerの哲学

• The Architecture of Open Source Applications : Chapter 11 LLVM

• 邦語訳へのリンクhttp://m-takagi.github.io/aosa-ja/

Chris Lattnerの哲学

もうひとつ、LLVMを軽量なままに保ち続けている大きな特徴がある (ライブラリを使うクライアント側から見ると賛否両論がある)。 それは、過去の決断も積極的に見直して、過去との互換性を気にせずにAPIを大きく変更していくということだ。 たとえばLLVM IR自体に大幅な変更を加えるには、すべての最適化パスの変更が必要になる。 そしてそれは、C++のAPIにも大きな影響を及ぼす。 LLVMでは過去に何度かそういうことがあった。 クライアント側にとっては辛かっただろうが、今後の開発を順調に進めていくためにはそうすべきだった。

Chris Lattnerの哲学

もうひとつ、LLVMを軽量なままに保ち続けている大きな特徴がある (ライブラリを使うクライアント側から見ると賛否両論がある)。 それは、過去の決断も積極的に見直して、過去との互換性を気にせずにAPIを大きく変更していくということだ。 たとえばLLVM IR自体に大幅な変更を加えるには、すべての最適化パスの変更が必要になる。 そしてそれは、C++のAPIにも大きな影響を及ぼす。 LLVMでは過去に何度かそういうことがあった。 クライアント側にとっては辛かっただろうが、今後の開発を順調に進めていくためにはそうすべきだった。

過去の決断も積極的に見直して過去との互換性を気にせずにAPIを大きく変更していく

Swift発表時

Swift発表時Swiftプロジェクト

開始直後

Swift発表時Swiftプロジェクト

開始直後現在!

Chris Lattnerの哲学

ここまではなんとかうまくやってきたが、まだやり残したことは多い。 さらに、今後LLVMが年を重ねるにつれて、軽快さが失われて硬直化してしまうというリスクもある。 この問題には魔法のような解決策があるわけではない。 でも、新しい問題領域を公開し続けていることや 過去の決断を躊躇せず再考していること、 さらに再設計で過去のコードを捨てられるようにしていることなどが、 すこしでもその対策になって欲しいものだ。 結局のところ、パーフェクトな存在を目指すのではなく、常に向上し続けることが大切なのだ。

Chris Lattnerの哲学

ここまではなんとかうまくやってきたが、まだやり残したことは多い。 さらに、今後LLVMが年を重ねるにつれて、軽快さが失われて硬直化してしまうというリスクもある。 この問題には魔法のような解決策があるわけではない。 でも、新しい問題領域を公開し続けていることや 過去の決断を躊躇せず再考していること、 さらに再設計で過去のコードを捨てられるようにしていることなどが、 すこしでもその対策になって欲しいものだ。 結局のところ、パーフェクトな存在を目指すのではなく、常に向上し続けることが大切なのだ。

過去の決断を躊躇せず再考

パーフェクトな存在を目指すのではなく、常に向上し続けることが大切