Swiftのenumが素敵すぎる Part2
前回の投稿で、以下のSwiftの列挙型(enum)がもつ固有の性質のうち、#3と#4を使ってサーチプログラムをリファクタリングした。
今回は、#1と#2を使って、さらにリファクタリングするぜ。
Swiftの列挙型の固有な性質:
1. メンバーに具体的な値をいれる必要はない。メンバーを値として扱える。
2. メンバーに付随する型を設定できる。
3. メンバーに値を入れた場合は、Objective-CのNS_ENUMと同様に扱える。
4. 列挙型自体を値型としてオブジェクトとして扱え、プロパティーやメソッドも定義できる。
問題:
サーチプログラムのコアとなる以下の関数では、サーチ状態を表すのに、大きく3つのパラメタ(isLoading, HasSearched, searchResults)を使っている。なんだけど、本当に興味があるのは、以下の4つの状態のはずで、これらの状態を3つのパラメタ(isLoading, HasSearched, searchResults)の組み合わせで判断する必要がある。コードが美しくないし、わかりにくいので、バグを誘発しそうだ。こういう時に、enumの#1と#2を使ってコードをすっきりできるぜ。
1) サーチ処理実行前
2) サーチ実行中
3) サーチ実行完了かつ該当の項目が一つもない
4) サーチ実行完了かつ該当の項目が一つ以上あり
typealias SearchComplete = (Bool) -> Void func performSearchForText(text: String, category: Category, completion SearchComplete) { if !text.isEmpty { dataTask?.cancel() isLoading = true hasSearched = true searchResults = [SearchResult]() let url = urlWithSearchText(text, category: category) let session = NSURLSession.sharedSession() dataTask = session.dataTaskWithURL(url, completionHandler: { data, response, error in var success = false if let error = error { if error.code == -999 { return } // Search was cancelled } else if let httpResponse = response as? NSHTTPURLResponse { if httpResponse.statusCode == 200 { if let dictionary = self.parseJSON(data) { self.searchResults = self.parseDictionary(dictionary) self.searchResults.sort(<) println("Success! ") self.isLoading = false success = true } } } if (!success) { self.hasSearched = false self.isLoading = false } dispatch_async(dispatch_get_main_queue()) { SearchCompletion(success) } }) dataTask?.resume() } }
素直に、enumを使えば、以下のように定義できる。これらは、先にのべた本当に興味のある状態を表している。
#1の性質にあるように、4つの状態すべてに値を定義しておらず、それぞれのメンバを値として扱える。
また、4つめの.Resultsのケースが特殊で、#2の性質にあるように、Swiftのenumは、メンバーに付随する型を設定できる。ここでは、SearchResultオブジェクトのArrayを設定している。
enum State { case NotSearchedYet // サーチ処理実行前 case Loading //サーチ実行中 case NoResults //サーチ実行完了かつ該当の項目が一つもない case Results([SearchResult]) //サーチ実行完了かつ該当の項目が一つ以上あり }
上記の状態(State)に従って、先のPerformSearchForText関数をリファクタリングをしたのが以下である。
func performSearchForText(text: String, category: Category, completion SearchComplete) { if !text.isEmpty { dataTask?.cancel() state = .Loading //サーチ実行中 let url = urlWithSearchText(text, category: category) let session = NSURLSession.sharedSession() dataTask = session.dataTaskWithURL(url, completionHandler: { data, response, error in self.state = .NotSearchedYet // サーチ処理実行前 var success = false if let error = error { if error.code == -999 { return } // Search was cancelled } else if let httpResponse = response as? NSHTTPURLResponse { if httpResponse.statusCode == 200 { if let dictionary = self.parseJSON(data) { var searchResults = self.parseDictionary(dictionary) if searchResults.isEmpty {  self.state = .NoResults //サーチ実行完了かつ該当の項目が一つもない } else { searchResults.sort(<) self.state = .Results(searchResults) //サーチ実行完了かつ該当の項目が一つ以上あり success = true } } } dispatch_async(dispatch_get_main_queue()) { SearchCompletion(success) } }) dataTask?.resume() } }
このようにenumを使えば、処理状態の管理が容易になり、かつ、ある状態にのみ付随する情報をまとめて設定できるので便利だ。
参考に、上記で、.Resultsに付随する情報として設定したsearchResultsを読み出す処理例として、状態(state)に応じて、tableViewの構成する各Cellを返す関数の例を以下に記載しておく。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { switch search.state { case .NotSearchedYet: fatalError("Should never get here") case .Loading: let cell = tableView.dequeueReusableCellWithIdentifier(TableViewCellIdentifiers.loadingCell, forIndexPath: indexPath) as UITableViewCell let spinner = cell.viewWithTag(100) as UIActivityIndicatorView spinner.startAnimating() return cell case .NoResults: return tableView.dequeueReusableCellWithIdentifier( TableViewCellIdentifiers.nothingFoundCell, forIndexPath: indexPath) as UITableViewCell case .Results(let list): let cell = tableView.dequeueReusableCellWithIdentifier( TableViewCellIdentifiers.searchResultCell, forIndexPath: indexPath) as SearchResultCell let searchResult = list[indexPath.row] cell.configureForSearchResult(searchResult) return cell } }
U
Swiftのenumが素敵すぎる
Swiftの列挙型(enum)が便利すぎて泣けてきた。
今回は、Swiftの列挙型がもつ、固有な性質を使って、コードのリファクタリングを実施してみた。
Swiftの列挙型には、以下の固有な性質があるんです!素敵だ。
1. メンバーに具体的な値をいれる必要はない。メンバーを値として扱える。
2. メンバーに付随する型を設定できる。
3. メンバーに値を入れた場合は、Objective-CのNS_ENUMと同様に扱える。
4. 列挙型自体を値型としてオブジェクトとして扱え、プロパティーやメソッドも定義できる。
特に、今回は#3と#4の性質を使って、コードをリファクタリングしてみる。
問題: 以下の関数を実際に使用する際、category値として、segmentedContrrol.selectedSegmentIndexを入力する。そのため、Int型で宣言してあるが、現在のコード上、実際には0-3の値しかとらない。例えば、4以上の値が入ってくることはない。でも、この関数のシグネチャだけでは、categoryとしてどんな値を取りうるのか分からず、将来、バグを発生させてしまう可能性がある。このような時にもちろん列挙型が使えるわけだ。
func urlWithSearchText(searchText: String, category: Int) -> NSURL { var entityName: String switch category { case 1: entityName = "musicTrack" case 2: entityName = "software" case 3: entityName = "ebook" default: entityName = "" } let escapedSearchText = searchText.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)! let urlString = String(format: "http://itunes.apple.com/jp/search?term=%@&limit=200&entity=%@", escapedSearchText, entityName) let url = NSURL(string: urlString) return url! }
上記をリファクタリングしたのが以下だ。ここで、#3の性質を使って、メンバーに値を設定している。これにより、urlWithSearchTextの引数は、Int型ではなく、Category型となり、値としては、0-3までしか扱えなくなった。
enum Category: Int { case All = 0 case Music = 1 case Software = 2 case EBook = 3 } private func urlWithSearchText(searchText: String, category: Category) -> NSURL { var entityName: String switch category { case .All: entityName = "" case .Music: entityName = "musicTrack" case .Software: entityName = "software" case .EBook: entityName = "ebook" } let escapedSearchText = . . . ・・・・・・・・・・・
上記では、urlWithSearchText関数内で、Category値に従って、さらにentityName値を決定しており、なんかすっきりしない。そこでで、さらに、#4の性質を使って、すっきりさせるぜ!
Swiftの列挙型は本当に素敵だ。なんと、Computed propertyを定義できるんだ。つまり、Categoryに、entityNameというcomputed propertyを追加できちゃうんだな。
追加したCateogyが以下だ。
enum Category: Int { case All = 0 case Music = 1 case Software = 2 case EBook = 3 var entityName: String { switch self { case .All: return "" case .Music: return "musicTrack" case .Software: return "software" case .EBook: return "ebook" } } }
これにより、urlWithSearchText関数は、以下のようにすっきりできる。
private func urlWithSearchText(searchText: String,category: Category) -> NSURL { let entityName = category.entityName // cateogory値に基づいて決定されるentityName値を取り出せる。 let escapedSearchText = . . .
Swiftの列挙型がもつ、
「列挙型自体を値型としてオブジェクトとして扱え、プロパティーやメソッドも定義できる」
という性質は本当に便利だ。
これを使って、コードをすっきりさせよう。
Swift版 AFNetworkingのAlamofireを使ってみたぜ!
毎週末のジョギングを終えて、ビール片手に、前回以下で投稿した生NSURLSessionを使って実装したサーチプログラムを、Alamofireフレームワークを使って、書き換えてみた。
iTuneStoreのWeb serviceであるSearch APIを使ってみた - Passion+Action+Sincerely=Success!
まず、Alamofireフレームワークを以下からダウンロードする。
プロジェクトへの組み込み方は、以下のエントリーを参考にしたよ。
Beginning Alamofire Tutorial - Ray Wenderlich
このサーチプログラムでは、大きく2箇所で、NSURLSessionを使っているので、これらをAlamofireを使って書き換えるよ。
(1) 検索バーに入力されたテキストをもとに実際のサーチ処理を実行するperformSearch()関数を以下のように書き換えた。
ここで何気に重要なのが、dataRequestこれは、Alamofire.Requestクラスのオブジェクトだ。サーチ中に、別のサーチが実行された場合は、dataRequest?.cancel()で、実行中の処理をキャンセルすることができる。
func performSearch() { if !searchBar.text.isEmpty { searchBar.resignFirstResponder() dataRequest?.cancel() //ダウンロード中は、ActivityIndicatorを表示する isLoading = true tableView.reloadData() hasSearched = true searchResults = [SearchResult]() //検索バーに入力されたテキストから、SearchAPIコール用のurlを作成する let url = self.urlWithSearchText(searchBar.text, category: segmentedControl.selectedSegmentIndex) dataRequest = Alamofire.request(.GET, url, parameters: nil).responseJSON() { _, response, data, error in //SearchAPIのレスポンスが返ってきたらここが呼ばれる。 if error == nil { if let response = response { if response.statusCode == 200 { if let dictionary = data as? [String: AnyObject] { self.searchResults = self.parseDictionary(dictionary) self.searchResults.sort(<) //メインスレッドで、サーチ結果を表示する。 // ActivityIndicatorを非表示にすることも忘れずにね! dispatch_async(dispatch_get_main_queue()) { self.isLoading = false self.tableView.reloadData() } return } } } } dispatch_async(dispatch_get_main_queue()) { self.hasSearched = false self.isLoading = false self.tableView.reloadData() self.showNetworkError() } } } }
(2) もう一箇所は、Search結果で入手したImageデータが置いてあるURLからImageデータをダウンロードする部分だ。これはUIImageViewのextensionとして実装してあり、以下のように書き換えた。
import Foundation import UIKit import Alamofire extension UIImageView { func loadImageWithURL(url: NSURL) -> Request { var fileName: String? var finalPath: NSURL? let request = Alamofire.download(.GET, url, { (temporaryURL, response) in /*この部分で、ダウンロードされたファイルが格納されたローカルファイルのフルパスをreturnするコードを書く。*/ if let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] as? NSURL { fileName = response.suggestedFilename! finalPath = directoryURL.URLByAppendingPathComponent(fileName!) return finalPath! } return temporaryURL }).response { (request,response, data, error) in /* この部分で、ローカルファイルからImageデータを取り込むコードを書く*/ if let response = response { if response.statusCode == 200 && finalPath != nil { if let data = NSData(contentsOfURL: finalPath!) { if let image = UIImage(data: data) { dispatch_async(dispatch_get_main_queue()) { self.image = image } } } } } } return request } }
上記では、loadImageWithURL関数の中で、Alamofire.Requestオブジェクトを返却している。これも重要だ。例えば、スクロールして、現在のダウンロード処理が不要になった場合は、キャンセルが必要になる。そのために、requestオブジェクトをreturnしている。
Alamofireは、なんか直感的に書けて、とってもモダンで、なんかとってもいい感じだ。俺もビール片手に酔っ払って、なんかとってもいい感じになってきたぜ!
Swiftで検索すると、やっぱり、テイラースイフトだらけになった(前と同じ結果)よ。
今後も積極的にAlamofireを使って通信周りのコードを書いてみようと思う。
iTuneStoreのWeb serviceであるSearch APIを使ってみた
iTuneStoreのWeb serviceであるSearch APIを使ってみた。
しっかりとした仕様書を以下に発見したので、これに従って、swiftベースでNSURLSessionで Search APIをコールし、JSON形式のレスポンスを受信し、tableViewに検索結果として表示する実装をしてみた。
iTunes, App Store, iBooks, and Mac App Store Affiliate Resources - Search API
Step1. 以下のような感じで、検索キーワード(SearchText)と、検索対象(entity)を指定してURLを構築する。なお、SearchTest中にスペースを入れると飛んじゃうので、ちゃんとエスケープしないといけません。
-----コード抜粋------
func urlWithSearchText(searchText: String, category: Int) -> NSURL {
var entityName: String
switch category {
case 1: entityName = "musicTrack"
case 2: entityName = "software"
case 3: entityName = "ebook"
default: entityName = ""
}
let escapedSearchText =
searchText.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
let urlString = String(format:
"http://itunes.apple.com/jp/search?term=%@&limit=200&entity=%@", escapedSearchText, entityName)
let url = NSURL(string: urlString)
return url!
}
-------------
Step2. 以下のような感じで、NSURLSessionを使って、URLをコールする。
Responseは、JSON形式なので、JSONに含まれるwrapperTypeに従い、それぞれのケースのparserを作成した。SearchResult Object Arrayに順番にappendしていき(parseDictionary内で実装)、最後にtableViewに反映する(reloadData内で実装)。
------コード抜粋-----
let session = NSURLSession.sharedSession()
dataTask = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
println("Failure! (error)")
if error.code == -999 { return }
} else if let httpResponse = response as? NSHTTPURLResponse {
if httpResponse.statusCode == 200 {
if let dictionary = self.parseJSON(data) {
self.searchResults = self.parseDictionary(dictionary)
dispatch_async(dispatch_get_main_queue()) {
self.isLoading = false
self.tableView.reloadData()
}
return
}
} else {
println("Failure! (response)")
}
}
// UIのアップデートはメインスレッドでね!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
self.showNetworkError()
}
})
dataTask?.resume()
------------
こんな感じにSwiftで検索すると、テイラースイフトだらけになった。
え!Objective-Cでは、NSURLSessionをそのまま使わず、AFNetworkingを使うのが主流なの?(ってエンジニアが言っていた気が。。。)
Swiftでは、Alamofireというlibraryがあることが分かった。ちなみにAFNetworkingを作った人と同じ人が作ったらしい。"AF"は、AlamofireのObjective-Cの命名規則に従った呼び方なんだな。この週末は、Alamofireを使って、通信周りをリファクタリングしてみようと思う。
以下に、参考になる資料発見。あー、週末が待ち遠しい。
SmartBeat、クラッシュ解析ツールを使ってみた。
SmartBeat、クラッシュ解析ツールを使ってみた。
SDKのインテグレーションが5分で完了し、クラッシュ解析レポーティングツールとして秀逸です。本体の組み込み自体は1行追加で、完了する。
Android/iOSの両方を使ってみたが、スタックトレースだけでなく、NSLog/LogCatログや、クラッシュ時のスマフォ画面をキャプチャしてレポートしてくれる機能もあり、それぞれ1行で機能をON/OFFできる。シンプルだが、パワフルで、一度使い始めたら、やめられないツールになりそう。
CrittercismとかBugSenseとか海外のツールもあるがやっぱり日本純正が安心でしょ。
以下から、無料トライアルができます。
Dashboard上、同一のクラッシュはグルーピングしてくれるので、件数の多いクラッシュから優先的に対応できます。Dashboard上で対応状況のステータストラックをすることもできます。
また、簡単な分析機能(Analytics)もあり、ユニークユーザー数/セッション数/離脱率を視覚的に確認できます。
また、端末情報詳細も自動的にアップされるので、どのバージョンのOSで、どの端末のクラッシュが多いかも簡単にわかっちゃいます。
なお、Dashboard上に設定した回数以上の異常を検出したら、担当者にメールを送信する機能もあります。以下のようなメールを受け取りました。
はい。そのとおり! そこで、存在しないmethodをコールしちゃいました。ごめんなさい。
----------
A new error has occurred for application [xxxxxxxxxxxxxx].
For complete information regarding this error please visit:
https://dash.smrtbeat.com/
Summary
=======
Error Message:
NSInvalidArgumentException: -[MainViewController buttonTapped]: unrecognized selector sent to instance 0x14e4f410
Application Verion:
1.0
Error Location:
unknown: 0
Stacktrace:
0 CoreFoundation 0x000000002929749f 0x29191000 + 1074335
1 libobjc.A.dylib 0x0000000036a76c8b 0x36a70000 + 27787
2 CoreFoundation 0x000000002929c8b9 0x29191000 + 1095865
3 CoreFoundation 0x000000002929a7d7 0x29191000 + 1087447
4 CoreFoundation 0x00000000291cc058 0x29191000 + 241752
5 UIKit 0x000000002c78a9fb 0x2c74b000 + 260603
6 UIKit 0x000000002c78a9a1 0x2c74b000 + 260513
7 UIKit 0x000000002c775613 0x2c74b000 + 173587
8 UIKit 0x000000002c78a40d 0x2c74b000 + 259085
9 UIKit 0x000000002c78a0e7 0x2c74b000 + 258279
10 UIKit 0x000000002c7839b1 0x2c74b000 + 231857
11 UIKit 0x000000002c75a15d 0x2c74b000 + 61789
12 UIKit 0x000000002c9cdab9 0x2c74b000 + 2632377
13 UIKit 0x000000002c758bb9 0x2c74b000 + 56249
14 CoreFoundation 0x000000002925dd57 0x29191000 + 838999
15 CoreFoundation 0x000000002925d167 0x29191000 + 835943
16 CoreFoundation 0x000000002925b7cd 0x29191000 + 829389
17 CoreFoundation 0x00000000291a93c1 0x29191000 + 99265
18 CoreFoundation 0x00000000291a91d3 0x29191000 + 98771
19 GraphicsServices 0x00000000305670a9 0x3055e000 + 37033
20 UIKit 0x000000002c7b8fa1 0x2c74b000 + 450465
21 Problems 0x0000000000068e4b 0x62000 + 28235
22 Problems 0x0000000000068dd8 0x62000 + 28120
OS:
8.1.1
Device:
iPhone6,1
SwiftのExtensionが素敵すぎる
View Controllerを拡張して、delegate methodを作るのは日常茶飯事だが、自分自身が書いていないiOS frameworkのクラスも簡単に拡張できる。
話を超簡単にするため、以下にiOSのStringクラスを拡張して、string objectにランダムな言葉を追加する例を備忘録として残しておく。
Step1: プロジェクトにソースファイルを追加する。例えば、String+RandomWord.swift
Step2: このファイルに、以下のようにString extensionを追加する
extension String {
func addRandomWord() -> String {
let value = arc4random_uniform(3)
var word: String
switch value {
case 0: word = "rabit"
case 1: word = "banana"
case 2: word = "boat"
default: word = ""
}
return self + word
}
}
let someString = "Hello, "
let result = someString.addRandomWord()println("The queen says: \(result)")
SwiftのExtensionって素敵! 例えば、標準ではUIImageは、ネットから画像をダウンロードする機能や、リサイズする機能を持っていないが、UIImageを拡張して自分なりの新しい機能を追加することが簡単にできる。
また、普通は継承できないような structsやenumsにも、Extensionを適用することが可能なようだ(俺は実際にまだつかっことが無い)。
SwiftベースのiOSゲームアプリに、プレイ動画共有サービスのKamcordを組み込んでみた
Swiftで書いたiOS Gameアプリに、Kamcordを組み込んでみた。
Kamcordは、プレイ動画の共有サービスで、Android/iOSの両方をサポートしている。
ゲームセンターで上手い人がプレイしている時は、その周りに多くの人が集まって見てたよね。「なるほど、この難しいレベルは、あーやって攻略するんだー」とか、上手い人からいろいろテクニックを盗んだ記憶があるよね。当然、ゲームを上手にプレイ出来たら、友達に見せびらかしたい。「ねー、見て見て見て、俺ってすごくなーい?」。そんなバイラル効果を狙ったサービスだ。
まず、Swiftで書いたコードからObjective-CのAPIをコールする方法を勉強する必要があったので、以下のページを参考に、WrapperクラスのKamcordBridgeクラスを作って実装したので、備忘録として残しておこうと思う。
How to call Objective C code from Swift - Stack Overflow
Step1. Kamcord.frameworkをプロジェクトに組み込む。以下に従えば簡単。
https://docs.kamcord.com/documents/platform/ios/getting-started/
Step2. Kamcord.frameworkのObjective-C APIをコールするために、KamcordBridgeクラスをプロジェクトに作成する。
ゲーム自体はSwiftベースだが、KamcordBridgeクラスのファイルはObjective-Cで作る。
すると、Bridging-Headerを作成するか,Xcodeに聞かれるので、Yesとする。すると、以下のように勝手に(ProjectName)-Bridging-Header.hが作成される。
ここに、作成したクラスヘッダを定義する。上記では、#import "KamcordBridge.h"
Step3. KamcordBridgeクラス内は、Objective-Cで書けるので、ここからKamcord.frameworkのAPIを呼ぶぞ。
以下のような感じ。
//KamcordBridge.h
#Import <Foundation/Foundation.h>
#import <Kamcord/Kamcord.h>
@interface KamcordBridge : NSObject
- (void)setDeveloperKey:(NSString *)key
developerSecret:(NSString *)secret
appName:(NSString *)appName
parentViewController:(UIViewController *)parentViewController;
- (void)startRecording;
- (void)stopRecording;
- (void)showView;
@end
//KamcordBridge.mm
#import "KamcordBridge.h"
@implementation KamcordBridge
- (void)setDeveloperKey:(NSString *)key developerSecret:(NSString *)secret appName:(NSString *)appName
parentViewController:(UIViewController *)parentViewController {
[Kamcord setDeveloperKey:key developerSecret:secret appName:appName parentViewController:parentViewController];
}
- (void)startRecording {
[Kamcord startRecording];
}
-(void)stopRecording {
[Kamcord stopRecording];
}
-(void)showView {
[Kamcord showView];
}
@end
Step4. あとはSwiftのコードから、KamcordBrigeクラスのAPIを呼べるぞ。
ViewDidLoad()から、初期化のため、以下をコールする。
KamcordBridge().setDeveloperKey("test", developerSecret: "test", appName: "Test", parentViewController: self)
もちろん"Test"ではなく、KamcordのDashbordから入手した、自分のアプリ用のDeveloper Key, Secret, AppNameをセットしてくれ!
録画を開始するためには、以下のようにコールする。
KamcordBridge().startRecording()
録画を終了するためには、以下のようにコールする。
KamcordBridge().stopRecording()
んで、最後に録画したプレイ動画の共有用UIを以下のようにコールする。
KamcordBridge().showView()
プレイ後、以下のように表示されれば成功だぜ!