一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

RxSwiftを使ってGitHubのおすすめユーザーを表示するアプリをつくってみた

ひさしぶりの投稿です。気づけば iOS の開発を始めて半年近く経ち、ようやく半人前になってきたかもしれません。

最近は流行りのリアクティブプログラミングに興味津々なので、今回はその勉強がてらRxSwiftを使って GitHub のおすすめユーザーを表示するアプリを作ってみました。その内容をざっくり書いてみたいと思います。

ちなみにこのアプリはこちらの記事の中で RxJS で作られているものを参考にしており、今回作るのはその iOS 版になります。リアクティブプログラミングについても同記事をかなり参考にしております。

開発環境は Xcode7(Swift2.0)で iOS8 以上をターゲットにしております。

コードはこちら。(本記事に載せているコードはリファクタリングなどで修正されている可能性もあるので、差異がある場合はなるべく GitHub のコードを参考にしてください)

ffmpegで雑に mov→gif 変換した紹介デモも乗せておきます。画質悪っ!しかも 1 件取得失敗してるやつおるやんけ…

WhoToFollowDemo

リアクティブプログラミングのざっくりしたイメージ

  1. Observable(ストリームと同義?)を作る。時間順に並んだ進行中のイベントの列のことらしい。よく見るマーブルダイアグラムのやつ。

  2. map, filterなど Observable に対して使用できる関数をかましてデータを加工する。

  3. Observer を作って、Observable を subscribe して返ってくる値に対して処理する。

この流れに沿って良い感じにコードを書いていきたいです。

API クライアント

GitHubAPI を叩いて User 情報を取得したいので、まずは API クライアントを作成しました。 ちなみにライブラリは CocoaPods ですべて導入済とします。 叩く想定の APIhttps://api.github.com/users?since=◯◯です。

class GithubAPIClient: {

    // MARK: - Properties

    let scheme = "https"
    let host: String = "api.github.com"

    private let manager = Alamofire.Manager.sharedInstance

    static let sharedInstance: GithubAPIClient = GithubAPIClient()


    // MARK: - Initializers

    private init() {}


    // MARK: - Instance methods

    func request(method: Alamofire.Method = .GET, path: String, params: [String : String] = [:]) -> Observable<AnyObject!> {
        let request = self.manager.request(method, self.buildPath(path), parameters: params).request

        if let request = request  {
            return self.manager.session.rx_JSON(request)
        } else {
            fatalError("Invalid request")
        }
    }


    // MARK: - Private methods

    private func buildPath(path: String) -> NSURL {
        let trimmedPath = path.hasPrefix("/") ? path.substringFromIndex(path.startIndex.successor()) : path
        return NSURL(scheme: self.scheme, host: self.host, path: "/" + trimmedPath)!
    }

}

通信は Alamofire を使用して GitHub のユーザー一覧を取得する API を叩きます。そしてここで Observable を返したいので、requestメソッドの返り値はObservable<AnyObject!>です。

RxCocoa のNSURLSession+Rx.swiftの中で、通信で返ってきた JSON データを Observable にして返すrx_JSONというメソッドがあるので、それをありがたく使用しています。

User モデル

API から返ってくるlogin, avatar_url, html_urlを使用して、名前、アバター画像の表示、プロフィールページへの遷移を実現できるようにします。

あとは先ほどの API クライアントを使ってユーザー一覧の Fetch、JSON の Parse 機能を実装します。

class User {

    // MARK: - Properties

    let name: String
    let url: String
    let avatarUrl: String

    static let apiClient = GithubAPIClient.sharedInstance


    // MARK: - Initializers

    init(name: String, url: String, avatarUrl: String) {
        self.name = name
        self.url = url
        self.avatarUrl = avatarUrl
    }


    // MARK: - Static methods

    static func fetch() -> Observable<[User]> {
        let randomOffset = String(arc4random_uniform(500))

        return self.apiClient.request(path: "users", params: ["since": randomOffset])
            .observeOn(Dependencies.sharedInstance.backgroundScheduler)
            .map { json in
                guard let json = json as? [AnyObject] else { fatalError("Cast failed") }
                return self.parseJSON(json)
            }.observeOn(Dependencies.sharedInstance.mainScheduler)
    }

    static func parseJSON(json: [AnyObject]) -> [User] {
        return json.map { result in
            guard let name = result["login"] as? String else { fatalError("Parse error") }
            guard let url = result["html_url"] as? String else { fatalError("Parse error") }
            guard let avatarUrl = result["avatar_url"] as? String else { fatalError("Parse error") }
            return User(name: name, url: url, avatarUrl: avatarUrl)
        }
    }

}

ObserveOnというメソッドにスケジューラーなるものを渡すことでメインスレッド、バックグラウンドスレッドなど処理を実行するスレッドを切り替えられます。重い処理は裏で勝手にやっとけよと。RxSwift の Examples でも同じようにしていたのでおそらく使い方は間違っていないかと。。

ちなみにSubscribeOnというメソッドもあるみたいです。使ったこと無いので違いをあまり理解していないですが、ObserveOnはそれを呼び出した後の処理を行うスレッドを変更するのに対し、SubscribeOnはそれを呼び出す前(Observable の作成そのもの?)の処理も指定のスレッドにできるみたいなイメージでいます。(参考)

というわけでここでの処理の流れは、

  1. ランダムなオフセットをつくって、URL を組み立てて API を叩く

  2. バックグラウンドスレッドでデータ加工(map)スタート

  3. Observable<AnyObject!>が返ってくる想定なのでそいつに対してParseJSON

  4. parseJSON内では引数に渡ってくるのデータはユーザー情報の一覧という想定なので、ほしい情報を抜き出してUserインスタンスの一覧を返す

  5. map内で無事にparseJSONできたら返ってくるのはObservable<[User]>

  6. メインスレッドに戻って、あとはこいつを Observer が subscribe すれば OK

という感じです。

ユーザー一覧の表示

TableViewController をつくって、ユーザーの一覧をいい感じに表示できるようにします。

コードが長いので、関係ありそうなとこだけ抜粋します。

self.refreshControl!.rx_controlEvents(.ValueChanged).startWith({ print("Start loading...") }())
  .flatMap {
      return User.fetch()
  }.subscribeNext { [unowned self] result in
      self.users = result
      self.refreshControl!.endRefreshing()
  }.addDisposableTo(self.disposeBag)

いよいよ Observable を Subscribe していきます。まずは初期表示時と pull to refresh した時にデータを良い感じに表示できるようにしたいです。

とりあえず pull to refresh するというイベントを起点に考えてみました。

  1. pull to refresh されたというイベントを Observable にしたいのでrx_controlEventsメソッドを使用

  2. pull to refresh されたタイミングでUser.fetch()を呼び出す

  3. subscribeNext(AnonymousObserverという匿名の Observer をつくって該当の Observable を勝手に subscribe してくれるやつ)内で返ってきたユーザー一覧を TableViewController 自身のusers(型は[User])にセット

  4. お作法的にdisposeBagをつくっておいてaddDisposableToを呼んでおけば不要になったタイミングで勝手に subscribe やめてくれる(はず。。)

ただこれだと pull to refresh しないとデータを Fetch できないので初回何も表示されず、かなりツラくなります。

なので、startWithで初回の pull to refresh を無理矢理発火させて初回も Fetch できるようにしました。

rx_controlEventsの返り値はControlEvent<Void>となっており、ControlEvent

public struct ControlEvent<PropertyType> : ControlEventType {
    public typealias E = PropertyType

    let source: Observable<PropertyType>

    init(source: Observable<PropertyType>) {
        self.source = source
    }

    /**
    Subscribes an observer to control events.

    - parameter observer: Observer to subscribe to events.
    - returns: Disposable object that can be used to unsubscribe the observer from receiving control events.
    */
    public func subscribe<O : ObserverType where O.E == E>(observer: O) -> Disposable {
        return self.source.subscribe(observer)
    }

    /**
    - returns: `Observable` interface.
    */
    public func asObservable() -> Observable<E> {
        return self.source
    }

    /**
    - returns: `ControlEvent` interface.
    */
    public func asControlEvent() -> ControlEvent<E> {
        return self
    }
}

またstartWithについては

extension ObservableType {

    /**
    Prepends a sequence of values to an observable sequence.

    - parameter elements: Elements to prepend to the specified sequence.
    - returns: The source sequence prepended with the specified values.
    */
    public func startWith(elements: E ...)
        -> Observable<E> {
        return StartWith(source: self.asObservable(), elements: elements)
    }
}

となっています。

これらのコードからrx_controlEventssourceObservable<Void>となりそうなので、startWith({ print("Start loading...") }())としてクロージャを強引に実行したらうまく Fetch してくれました。

あとmapではなくflatMapを使っていますが、mapだとsubscribeNextで返ってくるのがRxSwift.ObserveOnSerialDispatchQueue<Swift.Array<User>>のような意味不明なものになります。

今回は pull to refresh のイベントを起点にしており、そこでObservable<Void>が返ってきているので、入れ子の Observable を良い感じにマージしてくれるflatMapを使わないと欲しい値が手に入らないのではないかと思われます。(参考)

RxSwift.ObserveOnSerialDispatchQueue<Swift.Array<User>>に関してはまだちょっとコードを追えていないです。。

取得したユーザー一覧をもとにセルを作成・表示

正直 ViewModel 必要ない感満載なのですが、一応 ViewModel を介してUserモデルからセルを表示してみました。画像表示には SDWebImage を使用しています。

class UserTableViewCellModel {

    // MARK: - Properties

    let model: Variable<User>
    let name: Observable<String>
    let avatarUrl: Observable<NSURL>

    let disposeBag = DisposeBag()


    // MARK: - Initializers

    init(model: User) {
        self.model = Variable(model)
        self.name = self.model.map { return $0.name }
        self.avatarUrl = self.model.map { return NSURL(string: $0.avatarUrl)! }
    }

}
class UserTableViewCell: UITableViewCell {

    // MARK: - Properties

    let disposeBag = DisposeBag()

    var viewModel: UserTableViewCellModel? {
        didSet {
            guard let vModel = self.viewModel else { return }
            vModel.name.bindTo(self.nameLabel.rx_text).addDisposableTo(self.disposeBag)
            vModel.avatarUrl.subscribeNext {
                self.avatarImageView.sd_setImageWithURL($0, placeholderImage: UIImage(named: "DefaultImage.png"))
            }.addDisposableTo(self.disposeBag)
        }
    }

    static let rowHeight: CGFloat = 80


    // MARK: - IBOutlets

    @IBOutlet weak var avatarView: UIView!
    @IBOutlet weak var avatarImageView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!


    // MARK: - Lifecycles

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.avatarImageView.frame = self.avatarView.bounds
    }

}
  1. Controller から渡ってくるUserモデルを初期値として Observable を作成したかったので、Variable<User>を定義

  2. 1 の Variable からnameavatarUrlの Observable をそれぞれ作成

  3. View 側で ViewModel の上記の Observable たちを subscribe してnameavatarUrlをセット

Variable については

Variables represent some observable state. Variable without containing value can't exist because initializer requires initial value.

です(だんだん雑になってきた。。)

vModel.name.bindTo(self.nameLabel.rx_text)の部分はvModel.name.subscribeNext { self.nameLabel.text = $0 }と同じようなことを簡易的に書いている感じでしょうか。ラベルのテキストなど、ただ値をセットするだけのときによく使いそうです。

ちなみにこれは Rx ではなく SDWebImage の話ですが、デフォルト画像をセットしてない場合、スクロールしないと画像表示してくれないんですね。。地味にちょっとハマりました。(参考)

あと更に余談ですが、ImageView の Constraint をちゃんと設定しているはずなのに ImageView の位置が右に若干ずれる事件が発生しました。 これも同じようなことで困っている人がいて解決しましたが、これまた地味にハマりました。(参考)

ユーザーの削除(別ユーザーの表示)

iOS でよくあるスワイプで削除ボタンが出るやつを使ってユーザーを削除したら別のユーザーを表示してあげるようにします。

self.tableView.rx_itemDeleted.subscribeNext { [unowned self] indexPath in
    var data = self.users
    let nextIndex = Int(arc4random_uniform(18) + 11) // Select from remaining users
    let prevData = data[indexPath.row]

    data[indexPath.item] = data[nextIndex]
    data[nextIndex] = prevData
    self.users = data
}.addDisposableTo(self.disposeBag)

ユーザーが削除された時のイベントの Observable はrx_itemDeletedで取れるので、そいつを subscribe してやります。

実は API を叩いた時にユーザーは 30 人返ってきているのですが、アプリ上でははじめの 10 人しか出していない(元の参考にしたアプリは 3 人だったのですが、TableView で 3 人は寂しかったので 10 件)ので、余っている人の中から適当に選んで入れ替えて表示しています。この辺はかなり雑なのでバグとかあるかもしれません。。

ユーザーのプロフィールページへ遷移

最後にセルを選択したら該当のユーザーの GitHub プロフィールページに遷移できるようにします。iOS9 からSFSafariViewControllerが使えるので iOS9 の場合はそれを使うようにしました。

self.tableView.rx_itemSelected.subscribeNext { [unowned self] indexPath in
  guard let user = self.userForIndexPath(indexPath) else { return }
  guard let url = NSURL(string: user.url) else { return }

  if #available(iOS 9.0, *) {
      let safari = SFSafariViewController(URL: url)
      safari.delegate = self
      self.presentViewController(safari, animated: true, completion: nil)
  } else {
      UIApplication.sharedApplication().openURL(url)
  }

  self.tableView.deselectRowAtIndexPath(indexPath, animated: false)
}.addDisposableTo(self.disposeBag)

削除時と同様にrx_itemSelectedから Observable を作れるので、それを subscribe してユーザーのプロフィールページを SafariView または Safari に遷移して表示するようにしています。

あとセルの選択状態は残らないようにdeselectRowAtIndexPathを呼んでいます。

一般的にはセル選択時の挙動ってtableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)で書くと思うんですが、それをrx_itemSelectedで書くのって本当に良いんですかね?削除時の挙動も。。アップルに逆らいまくっている気がして妙な罪悪感を感じます。

まとめ

以上、RxSwift を使ってリアクティブっぽくアプリをつくってみました。Rx 関係のライブラリは機能が豊富すぎて難しくてかなりツラいですが、うまく動くと最高に気持ちいいです。実装も楽しくて妙な中毒性があります。

今回のアプリの元となった記事でも

現代のアプリは、高度にインタラクティブな体験をユーザーに与えるために、多数のリアルタイムイベントを扱っている。我々はこれを適切に取り扱うツールを探しており、リアクティブプログラミングがその答えなのだ。

と書いてありますし、ぜひ使いこなしたいもんです。

まだまだ理解しきれていない部分が多いのでおかしな点などがありましたら、僕が傷つかない程度に優しくご指摘ください。GitHubへの Issue や PR も大歓迎でございます。

参考

iPhoneアプリ開発初心者がSwift + CoreDataでシンプルなTODOアプリをつくってみた

(2015/11/7 追記)Swift2.0 に対応しつつ、リネームやリファクタリングなど修正を加えたものをGitHubにあげております。この記事内で説明している通りに実装しても動作しないと思うので、もしよろしければ GitHub のコードも参考にしてみてください。


僕は普段、Rails とか JavaScript とか触っているようなへっぽこ Web エンジニアでして、iOSAndroid アプリの開発経験はありません。

HogeClass *hoge = [[HogeClass alloc] init]; // (゚Д゚≡゚Д゚)マジ?

て感じでした。

でも最近は Objective なんたらの 100 億倍書きやすそうな Swift という言語が出ていたりするので、楽しそうだしちょっと手を出してみるかと。

そこでまず iOS 開発勉強用のサンプルとして Swift でシンプルな TODO アプリをつくってみたので、今回はその時の実装メモを書きたいと思います。

iOS 開発とか1ナノもわからない状態からスタートしたので、同じような境遇の人のお役に立てたら幸いです。

事前準備

Swift どころか、Objective なんたらも C もまともにやったこと無い人間だったので、さすがにいきなり開発はツラいと。

そこでまずは鉄板のドットインストールさんで事前に Swift やら iOS の開発がどんな感じかをざっくり学びました。(プレミアム会員なりましたよ)

僕は「ストーリーボードって何ですの?」レベルだったのでかなり参考になりました。

その後、Swift の言語仕様を学びたい欲が高まってきたので、以下の本を読み始めました。

個人的に Swift はいろんな言語のいいとこ取りしてるような感じで面白い言語だなぁという印象でした。

あとハマるのはツラいので最低限のデバッグ方法をなんとなく調査。

こちらこちらを見る感じだとブレークポイント仕込んで、po ◯◯ってやれば値をいじくれそうな気配がします。 JS の DEBUG を Chrome でやるような感じでなんとなくできそうです。po!!

そんな感じで本を読みながら、ブレークポイントを仕込みながら、楽しく TODO アプリ開発スタートです。

作成したアプリ

今回作成したものはこんな感じです。

f:id:d_animal141:20150228000945g:plain

コードはこちらに公開しております。

TODO を新規に登録できて、それを編集できて、スワイプしたら DELETE ボタン出てきて削除できて、みたいな。

いい感じのチュートリアルを見つけたのでそちらをモロパクリした次第です(以下、チュートリアルはこの記事のことを指すものとします)。英語です。

CoreData tutorial in Swift 5 using NSFetchedResultsController – Ravi Shankar

基本的に上記チュートリアルを参考にしながら、改造していくスタイルで開発しました(チュートリアルに載っているコード、たまに動かなかったので気をつけてくださいw)

あとチュートリアルと僕の作成したもので命名がちょいちょい違うところあると思いますが、特に深い理由はないのでお気になさらず。

ちなみに CoreData なるものを使っていて割とそこがツラかったりしましたが、まあ何とかなると思いますw

プロジェクト作成

Create a new File -> New -> Project でテンプレートを選択します。

今回は Master-Detail Application ではなく、Single View Application で一から作っていきます。

あと Use Core Data のチェックを忘れずに。

UI 開発

チュートリアルに図が豊富に載っているし、ドットインストールでストーリーボードの使い方も勉強したので、UI まわりはほとんど詰まることなく進めることができました。

強いて言うなら最初、プロジェクト作成後に自動で作られる ViewController を消して、新たに TableViewController を追加するところ。 僕は viewController のルートを表すストーリーボードの → マークが消えて「どこいったぁーーーー」と荒れ狂ったのですが、そういう人は新しく追加した tableViewController を選択した状態で、

attributes inspector -> is initial view controller

にチェックを入れると幸せになれると思います。

とりあえず TODO 一覧画面、詳細画面を作成して、+ボタンで遷移できるところまで作ったとします。また、一覧、詳細の viewController はそれぞれTodosViewController, TodoDetailViewControllerとします。

CoreData の準備

続いて CoreData の準備にとりかかります。

といっても1ナノも使い方がわからないので、まずはざっとCore Data プログラミングガイドこちらの記事に目を通します。

とりあえず

  • CoreData は OR マッピングフレームワークであり、永続ストアには主に SQLite を使用する
  • 今回特に使いそうなのは NSManagedObject、NSManagedObjectContext、NSEntityDescription、NSFetchRequest、NSFetchedResultsController あたり

ということがわかりました。

そして今回使いそうなクラスについて

  • NSManagedObject

モデルクラス。CoreData で永続化するオブジェクトは必ずこのクラス、またはこのクラスのサブクラスのオブジェクトでなければならない。

  • NSManagedObjectContext

データ操作を行うためのコンテキスト(作業領域)。NSManagedObjectに対する操作はすべてこのコンテキストを介して行われる。CRUD 操作、Undo Redo、Validation などを担当。

  • NSEntityDescription

エンティティ記述という、エンティティの定義を管理するクラス。エンティティとは CoreData で永続化の対象となるデータのこと。エンティティとNSManagedObjectオブジェクトの関係は DB でいうところのテーブルとレコードの関係みたいな感じですかね。

  • NSFetchRequest

データの検索条件を管理するクラス。ここで指定した条件が SQL に変換されてデータ検索に使われる。

  • NSFetchedResultsController

NSManagedObjectオブジェクトを監視するコントローラークラス。NSManagedObjectオブジェクトが挿入、変更、削除された時にNSFetchedResultsControllerDelegateオブジェクトに通知する。

ふむ。

プロジェクト作成時に Use Core Data をチェックした時点で CoreData を使うための設定はある程度やってくれているみたいですね。

では上記を踏まえ、チュートリアルに沿って進めていきます。

まずは xcdatamodeld ファイルを更新します。チュートリアルではエンティティを Tasks としていますが、僕は Todos として、Attributes は String 型の content を持つものとしました。

Todos.swift はとりあえずこんな感じになります。

import Foundation
import CoreData

class Todos: NSManagedObject {

    @NSManaged var content: String

}

ちなみにここで@NSManagedというものがついていますが、もともと Objective なんたらでは NSManagedObject のサブクラスのプロパティは@dynamicという属性をつけていたらしく、「ここはゲッターセッターの自動生成は無しにして、NSManagedObjectの実装に任せましょう」みたいな意味だったらしいです。(参考)

これが Swift ではNSManagedObjectのサブクラスにのみ利用可能な@NSManagedという属性が使われるようになったようです。

では、いよいよ TODO を登録するところを実装していきます。

TODO の登録

TODO を登録できるようにするためにTodoDetailViewControllerを修正していきます。

まずNSManagedObjectContextオブジェクトを用意します。

let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext

iOS では 1 アプリにつき必ず 1 つの UIApplication が存在しており、そのシングルトンオブジェクトはUIApplication.sharedApplicationで取得できます。(参考)

そいつからAppDelegateインスタンスmanagedObjectContextを取得します。

そしてテキストフィールドに入力したテキストを保存できるようにメソッドを追加していきます。

func createTodo() {
    let entity = NSEntityDescription.entityForName("Todos", inManagedObjectContext: managedObjectContext!)
    let todo = Todos(entity: entity!, insertIntoManagedObjectContext: managedObjectContext!)

    todo.content = self.textField.text
    managedObjectContext!.save(nil)
}

func dismissViewController() {
    self.navigationController?.popViewControllerAnimated(true)
}

@IBAction func save(sender: AnyObject) {
    self.createTodo()
    self.dismissViewController()
}

おそらくお作法なのだろうと、真似して保存するところまで書いてみます。(エラーハンドリング等はとりあえず無視)

まずエンティティを取得して、それを使ってNSManagedObjectオブジェクトを取得。そいつにテキストフィールドの値を入れてNSManagedObjectContextオブジェクトにて保存すると。

@IBAction func save(sender: AnyObject)で Save ボタンを押したときの処理が書けるので、TODO の保存と詳細画面を閉じる処理を追加します。

登録した TODO の取得

次にTodosViewControllerの修正です。

こちらの viewController でも CoreData をインポートし、NSFetchedResultsControllerDelegateプロトコルに適合させていきます。またデータ操作のためのNSManagedObjectContextオブジェクトも必要です。

import CoreData

class TodosViewController: UITableViewController, NSFetchedResultsControllerDelegate {

    let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
    var fetchedResultController: NSFetchedResultsController = NSFetchedResultsController()

これだけだとfetchedResultControllerは無力なので、getFetchedControllerメソッドを定義して、viewDidLoadの中でゴニョゴニョやります。(viewDidLoadがいつ呼ばれるか等、iOS のイベントに関してはこちらを参考にさせて頂きました)

override func viewDidLoad() {
    super.viewDidLoad()

    fetchedResultController = self.getFetchedResultController()
    fetchedResultController.delegate = self
    fetchedResultController.performFetch(nil)
}

func getFetchedResultController() -> NSFetchedResultsController {
    fetchedResultController = NSFetchedResultsController(fetchRequest: self.todoFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
    return fetchedResultController
}

func todoFetchRequest() -> NSFetchRequest {
    let fetchRequest = NSFetchRequest(entityName: "Todos")
    let sortDescriptor = NSSortDescriptor(key: "content", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]
    return fetchRequest
}

まずtodoFetchRequestにて検索条件を作成します。そしてそいつをセットしてNSFetchedResultsControllerオブジェクトを作成。performFetchで検索条件をもとにデータ取得といった感じでしょうか。 あーキツくなってきたぞw

登録した TODO の一覧表示

次、一覧表示しますよ。まず、ストーリーボードの TodosViewController の Cell を選択した状態で、identifier を Cell としてください。

そんでからチュートリアルのコードをパクります。どん。

//こいつは必要なさそうな?
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    let numberOfSections = fetchedResultController.sections?.count
    return numberOfSections!
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let numberOfRowsInsection = fetchedResultController.sections?[section].numberOfObjects
    return numberOfRowsInsection!
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
    let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos
    cell.textLabel?.text = todo.content
    return cell
}

func controllerDidChangeContent(controller: NSFetchedResultsController!) {
    tableView.reloadData()
}

こちらの記事を参考に tableView まわりのメソッドを整理していきます。

  • numberOfSectionsInTableView

テーブル全体のセクションの数を返すように実装する。

  • tableView(tableView: UITableView, numberOfRowsInSection section: Int)

セクション無しのテーブルを作成する場合はこのメソッドでセルの数を返すように実装する。テーブルにセクションを付ける場合はnumberOfRowsInSectionで指定されたセクションの項目数を返すように実装する。

  • tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) cellForRowAtIndexPathで指定したインデックスパスのセル(UITableViewCell)を作成し、そのインスタンスを返すように実装する。

ただ今回、別にセクション分けされた tableView を作っているわけではないのでnumberOFSectionsInTableViewは要らない気がしました。実際消しても何も問題起きないので僕は削除しました。

あと、tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)の以下の部分。

let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos

ここで Swift には NameSpace があるので、このダウンキャストはモジュール名.クラス名じゃないと使えないと。

てなわけで xcdatamodeld ファイルを再度開き、Data Model Inspector から上記に従ってクラス名を変更します。僕はiOSTodosというプロジェクト名だったのでiOSTodos.Todosです。

ここまでで新規登録と登録した TODO の一覧表示はできるかと。あーーキツイ!!

登録した TODO の削除

スワイプしたら DELETE ボタンが出てきて、クリックしたら削除できるあれですね。

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    let managedObject: NSManagedObject = fetchedResultController.objectAtIndexPath(indexPath) as NSManagedObject
    managedObjectContext?.deleteObject(managedObject)
    managedObjectContext?.save(nil)
}

こんな感じになりました。コンテキストを介して、対象のNSManagedObjectオブジェクトを削除して保存。新規登録と同じ感じですね。

登録した TODO の編集

TODO を編集できるようにします。一覧画面からセルを選ぶと詳細画面に遷移して、編集、保存を行えるようにします。

まずストーリーボードの TodosViewController の Cell から TodoDetailViewController に向かってセグエを作成します。あとでセグエを特定するために identifier が必要になるので、identifier は edit とします。

次にTodosViewControllerの修正をします。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "edit" {
        let cell = sender as UITableViewCell
        let indexPath = tableView.indexPathForCell(cell)
        let todoDetailController: TodoDetailViewController = segue.destinationViewController as TodoDetailViewController
        let todo: Todos = fetchedResultController.objectAtIndexPath(indexPath!) as Todos

        todoDetailController.todo = todo
    }
}

prepareForSegueは画面遷移が始まった時に呼ばれるメソッドですね。

クリックされた Cell をもとにindexPathを取得して TODO を特定します。そして、segue.destinationViewControllerで遷移先の viewController が取得できるのでそいつに TODO を渡しています。 TodoDetailViewControllerのほうでtodoを定義しないといけません。

TodoDetailViewControllerの修正です。

var todo: Todos?

override func viewDidLoad() {
    super.viewDidLoad()

    if todo != nil {
        self.textField.text = todo?.content
    }
}

一覧画面から TODO が渡ってきた場合、テキストフィールドにそいつのcontentをセットしています。

そして編集メソッド。

func editTodo() {
    todo?.content = self.textField.text
    managedObjectContext!.save(nil)
}

入力したテキストフィールドの値で上書き保存します。

Save ボタンをおした時の挙動が新規登録、編集の2パターンになったので修正します。

@IBAction func save(sender: AnyObject) {
    if todo != nil {
        self.editTodo()
    } else {
        self.createTodo()
    }
    self.dismissViewController()
}

これで新規登録、一覧表示、削除、更新、ひと通りの処理ができるようになりました。キツかった。。

テキストフィールドで Return した時にキーボードが閉じるようにする

現状ではテキストを入力し終わってドヤ顔で Return してもキーボードが閉じず、恥ずかしい思いをしてしまいます。

TodoDetailViewControllerを修正していきます。

class TodoDetailViewController: UIViewController, UITextFieldDelegate {
    略

    func textFieldShouldReturn(textField: UITextField!) -> Bool {
        self.view.endEditing(true)
        return false
    }

UITextFieldDelegateプロトコルに適合して、Return した時に呼ばれるtextFieldShouldReturnを実装しておきます。これでキーボードが閉じるはず。

まとめ

以上、チュートリアルを参考に TODO アプリを作ってみました。作る過程で様々な本や記事に触れ、少しだけですが iPhone アプリ開発のことを知ることができました。やはり今までやったことない開発は楽しいし、Swift という言語自体にもかなり惹かれているので今後も勉強を続けていきたいなと思いました。ただどうしても現状 Objective なんたらの知識が求められる場面が多いのでそこはツラいですねw 諦めてそちらも勉強していきます。。

ちなみに他にもテキスト空の状態で登録できないようにしたり、ちょいちょい改良はしてみたのですが、長くなるので後はこちらで見ていただければと思います。

また本記事や公開しているコードにおいて、初心者ゆえの間違いも多々あるかもしれません。僕は非常に傷つきやすいので、もし発見された方は優しくコメントして頂けると幸いです。

参考

Chromeのコンソール上でXPathのテストをする

最近、仕事で Web スクレイピングの機能を実装する機会があって XPath を使う必要に駆られました。そこで ChromeXPath が正しく機能しているかテストする便利な技があったことを知ったのでメモです。

$x を使用する

Chrome のコンソールから、$xを使えば OK。

たとえば/html/bodyでちゃんと body 要素が取得できるか確認したい場合は、

$x('/html/body')

としてやれば body 要素がちゃんと返ってきます。こいつは便利。知らなかった。。

まとめ

スクレイピングする時ぐらいしか使わないかもしれませんが、もし複雑な XPath を書く必要に迫られて困っている人は試してみてください!

参考

procとlambdaの挙動の違い

Ruby の Proc オブジェクトはKernel#proc(またはProc#new)で作るパターンとKernel#lambdaで作るパターンがありますが、それらは地味に結構挙動が違っていたりします。

そこで今回はこのKernel#procKernel#lambda でつくった時の Proc オブジェクトの挙動の違いについて見ていこうと思います。

return の時の挙動が違う

def proc_hoge
  proc { return 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { return 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=>1
lambda_hoge #=> 'hogehoge'

こんな感じで proc のreturnProc#callを読んだコンテキストから抜けます。

よって、もしトップレベルで

proc { return 1; puts 'top level!' }.call #=> LocalJumpError

とやるとLocalJumpErrorとなります。

一方、lambda の方はKernel#lambdaで作成したオブジェクトの制御を抜けるだけです。

トップレベルで呼び出しても問題ありません。

-> { return 1; puts 'hoge' }.call #=> 1

break の時の挙動が違う

def proc_hoge
  proc { break 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { break 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=>LocalJumpError
lambda_hoge #=> 'hogehoge'

次にbreakの挙動ですが、proc の場合はbreakで常にLocalJumpErrorが発生します。トップレベルだろうがメソッドの中だろうがエラーです。

一方、lambda の場合はreturnと同様、制御を抜けるだけです。

これはかなり挙動が違うので注意する必要がありそうですね。

対策としてはnextを使うとどちらの場合でも制御を抜けるだけになるので、それを使うといいと思われます。

def proc_hoge
  proc { next 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { next 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=> 'hogehoge'
lambda_hoge #=> 'hogehoge'

引数に対する厳密さが違う

proc { |x, y| [x, y] }.call(1, 2, 3) #=> [1, 2]
proc { |x, y| [x, y] }.call(1) #=> [1, nil]
proc { |x, y| [x, y] }.call([1, 2]) #=> [1, 2]

# lambda { |x, y| x }.call(1, 2, 3)と同じ
-> (x, y) { x }.call(1, 2, 3) #=> ArgumentError
-> (x, y) { x }.call(1) #=> ArgumentError
-> (x, y) { x }.call([1, 2]) #=> ArgumentError

proc に引数を渡すときは

  • 仮引数の数より多く引数が渡された時、余分な引数は無視する
  • 引数の数が足りない時はnilを渡す
  • 配列が 1 つだけ渡されると展開される

のような特徴があります。

一方、lambda の場合は上記はすべてArgumentErrorになります。なかなか手厳しいですね。

まとめ

以上がKernel#procKernel#lambda でつくった時の Proc オブジェクトの主な違いです。全体的に lambda でつくった Proc オブジェクトのほうがよりメソッドぽい雰囲気がしますね。

参考は安定のこちら。いつもありがとうございます。

Rubyのcase文はwhen節に渡すオブジェクトによって振る舞いが異なる

今回は Ruby の case 文についてです。

Ruby の case 文はどのように値を比較するか

Ruby の case 文では case 節に比較対象となる値を、when 節に case 節の値と比較する値を記述します。 んで、when 節の値をレシーバとして、===メソッドによって比較が行われ、最初に真となった when 節に続く処理が実行されます。

hoge = 'hoge'

case hoge
when 'foo'
  puts 'foooo!'
when 'bar'
  puts 'baaar!'
when 'hoge'
  puts 'hogeee!'
else
  puts "わかりまへーん"
end

#=> hogeee!

このように===で比較するわけで、数値や文字列を比較する時は単純に==的な比較を行います。

しかし、実はいくつかのクラスでは===の振る舞いが異なります。具体的にはRange, Regexp, Proc, ModuleClassです。

Range クラスのオブジェクトと比較する場合

score = 43

case score
when (1..40)
  puts '追試ですよー'
when (41..80)
  puts '微妙ですよー'
when (81..1000)
  puts 'いい感じですよー'
end

#=> 微妙ですよー

Range===では引数が自身の範囲内に含まれていたら真を返します。今回はscore = 43なので追試は間逃れています。

Regexp クラスのオブジェクトと比較する場合

lang = 'ruby'

case lang
when /php/
  puts 'phpですよー'
when /ruby/
  puts 'rubyですよー'
else
  puts 'そんなの知りませんよー'
end

#=> rubyですよー

Regexp===は引数の文字列がマッチした場合に真を返します。今回は簡単な例ですが、ちゃんと正規表現を書けばかなり柔軟な比較ができそうです。

Proc クラスのオブジェクトと比較する場合

obj = 'hoge'

case obj
when proc { |x| x.is_a? String }
  puts 'Stringでしたよー'
when proc { |x| x.is_a? Numeric }
  puts 'Numericでしたよー'
else
  puts 'よくわからないですよー'
end

#=> Stringでしたよー

Proc===では右辺の値を引数にしてProcの処理を実行します。

よって上記のように条件判断のためのProcオブジェクトを仕込んでおくと、case に渡された値を引数として処理を実行するので、柔軟な条件分岐が可能になります。

Class や Module クラスのオブジェクトと比較する場合

obj = 'hoge'

case obj
when String
  puts 'Stringでしたよー'
when Numeric
  puts 'Numericでしたよー'
else
  puts 'よくわからないですよー'
end

#=> Stringでしたよー

ClassModule===では右辺のオブジェクトが左辺のクラス(もしくはそのサブクラス)のインスタンスだった場合、真を返します。 なのでString === Stringfalseですが、String === 'hoge'trueになります。

これは地味に間違えそうなやつですね。

まとめ

クラスによって比較の仕方が違うというのは、知らないとハマりポイントになりそうなので注意が必要です。また、例えばクラスの異なるオブジェクト同士を比較する場合は、どちらをレシーバにするかによって結果が変わる可能性もあるのでそこも注意が必要そうです。

今回も参考にしたのは安定のパーフェクト Ruby でございました。