ぼくのSierraアップデート記

諸事情でSierra & Xcode8に開発環境をアップグレードしたので人柱記録です。 と言っても大した問題はなく、普段使っているツールは大体対応してくれていました。 gitup も問題なしでした。 SwiftLintなど開発用のコマンドラインツール群も問題なし。 Siriを試そうと思って「Hey Siri」と言ったら家中のSiriが一斉に反応してしまって焦りました。

唯一アップグレード前に確認し忘れていたのはXVimでしたが、以下の手順の通りXcode8にインストールできました。(ElCapitanでも同じ状況だとは思います。)

XVim/INSTALL_Xcode8.md at master · XVimProject/XVim · GitHub

Xcodeを署名し直すとは随分と大胆ですね。。再署名自体は数分かかっていましたが、無事。

他にもあれ大丈夫だった?とか確認できますので、気軽に聞いてくれていいですよ。

追記

Homebrewの/usr/local問題がありましたが、すぐに解決しました。

$ brew update
Error: /usr/local is not writable. You should change the ownership
and permissions of /usr/local back to your user account:
  sudo chown -R $(whoami) /usr/local
$ sudo chown -R $(whoami) /usr/local
Password:
$ brew update
Updated Homebrew from dfcbeff to 12aad5c.

brew updateをしたら、/usr/local から /usr/local/Homebrew への移行も勝手にやってくれました。

==> Migrating HOMEBREW_REPOSITORY (please wait)...
==> Migrated HOMEBREW_REPOSITORY to /usr/local/Homebrew!
Homebrew no longer needs to have ownership of /usr/local. If you wish you can
return /usr/local to its default ownership with:
  sudo chown root:wheel /usr/local

/usr/localの権限は元に戻していいとのことだったので、戻しておきました。

$ sudo chown root:wheel /usr/local

追記 2016/10/02

ちょっと古いバージョン(7.3)のXcodeiPhoneアプリをリリースする必要があったのですが、Submitで以下のエラーになってしまいました。

xcode7 - ERROR ITMS - 90167 No. app bundles found in the package - Stack Overflow

Sierraだけで発生する問題のようです。なんと公式版でも解決されていません。。
コメントにある通り、Xcode7系でビルドしたxcarchiveを、Xcode8のOrganizerからSubmitするとうまくいきました。

RxSwiftのbindToについて

RxSwiftにはbindToが大きく分けて2種類あります。

引数にobserverを取るものと、binderを取るものです。 引数にvariableを取るものは基本observableを取るものと動きが同じです。

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

引数にobserver/variableを取るもの

内部的に即subscribeが呼ばれる。

実装はこうなっています。(複数ありますが、簡単なほうを抜粋)

    /**
    Creates new subscription and sends elements to observer.
    
    In this form it's equivalent to `subscribe` method, but it communicates intent better, and enables
    writing more consistent binding code.
    
    - parameter observer: Observer that receives events.
    - returns: Disposable object that can be used to unsubscribe the observer.
    */
    @warn_unused_result(message="http://git.io/rxs.ud")
    public func bindTo<O: ObserverType where O.E == E>(observer: O) -> Disposable {
        return self.subscribe(observer)
    }

A=>B というシーケンスを作って即実行したい時に使えます。

引数にbinderを取るもの

内部的には引数のbinder: Self -> R のクロージャがselfを引数に実行されるだけ。

実装はこうなっています。(複数ありますが、簡単なほうを抜粋)

    /**
    Subscribes to observable sequence using custom binder function.
    
    - parameter binder: Function used to bind elements from `self`.
    - returns: Object representing subscription.
    */
    @warn_unused_result(message="http://git.io/rxs.ud")
    public func bindTo<R>(binder: Self -> R) -> R {
        return binder(self)
    }

A=>Bというシーケンスだけ作っておいて、後で別のシーケンスとつなげたり、subscribeのタイミングをコントロールしたりする時に役立ちます。

ちょっと例が雑ですが、以下のようにログイン状態の時とそうでない時でシーケンスを変えるなんて使い方はいかがでしょうか。

    var loggedIn = true
    override func viewDidLoad() {
        super.viewDidLoad()

        let number1Seq01 = number1.rx_text.bindTo(verifyString).retry(3)
        if loggedIn {
            number1Seq01
                .bindTo(doThisOnlyWhenLoggedIn)
                .bindTo(number4.rx_text)
                .addDisposableTo(disposeBag)
        } else {
            number1Seq01
                .bindTo(number4.rx_text)
                .addDisposableTo(disposeBag)
        }
    }

    func verifyString(observable: ControlProperty<String>) -> Observable<String> {
        return Observable.create {
            observer in
            /* Do something useful... */
            return observable.subscribeNext {
                value in
                print(#function)
                observer.onNext(value)
            }
        }
    }
    func doThisOnlyWhenLoggedIn(observable: Observable<String>) -> Observable<String> {
        return Observable.create {
            observer in
            /* Do something useful... */
            return observable.subscribeNext {
                value in
                print(#function)
                observer.onNext(value)
            }
        }
    }

まとめ

先日参加した 第2回RxSwift勉強会 @ Sansanにて「bindToはsubscribeと同じ」という言及があって、アレそうだったっけ?と思って再度検証してみました。

厳密にはsubscribeと同じ意味のものと、そうでないものがありました。bindToのbinderファンクションの仕組みを利用する事で、複雑なシーケンスも普通の変数として取り回す事も出来るようになりました。

実際には、私は起動シーケンスの処理でこの Observable.create とbindTo を多用しています。色々な処理を様々な依存関係で記述する必要があるような時に、あると便利な道具だと思います。

ご意見募集中です。

同じワークスペース内で作ったSwiftモジュールを使う

こういう感じの構成で、libの中をPokemonKitとしてモジュールにして、sourcesをアプリにしたいと思った。

app $ tree
.
├── Makefile
├── README.md
├── build.ninja
├── lib
│   └── pokemon.swift
└── sources
    └── main.swift

モジュールにしなければ、swiftc lib/pokemon.swift sources/main.swift -o build/main で済む話だし、どこかのGitリポジトリのライブラリを持ってくるのであればSwiftPMを使えばいい話である。

どうしてもSwiftPMを使いたくないというケースも考慮して、一応このまま突き進もう。

module = swiftc -emit-module -module-name PokemonKit -o build/PokemonKit
module-obj = swiftc -emit-library -emit-object -module-name PokemonKit
app-obj = swiftc -I build/ -emit-object
executable = swiftc
lib = lib/pokemon.swift
app = sources/main.swift

rule kit
    command = $module -o build/PokemonKit $in && mv build/PokemonKit build/PokemonKit.swiftmodule && $module-obj $in -o $out

rule app-obj
    command = $app-obj $in -o $out

rule app
    command = $executable $in -o $out

build build/pokemon.o: kit $lib
build build/main.o: app-obj $app
build build/main: app build/pokemon.o build/main.o

default build/main

ninja-buildを使って見た。これで差分ビルドにも対応している。

-emit-module の時にswiftdocも一緒に生成するのだが、出力場所を指定する方法がわからなかった。仕方ないので -o をえいやで指定すると .swiftmoduleのサフィクスがつかなかったので、自分でリネームする処理を入れた。惜しい。

ビルド方法は以下のようになっている。

$ cat Makefile
emitted = build/*

.PHONY: clean build

clean:
    rm $(emitted)

build:
    ninja -j 1

ninja -j 1 で並列にビルドしないようにしている点がポイントである。 こうしないと、pokemon.oとmain.o を同時に作ろうとして失敗する。

ビルドした状態。

app $ tree
.
├── Makefile
├── README.md
├── build
│   ├── PokemonKit.swiftdoc
│   ├── PokemonKit.swiftmodule
│   ├── main
│   ├── main.o
│   └── pokemon.o
├── build.ninja
├── lib
│   └── pokemon.swift
└── sources
    └── main.swift

動かしてみよう。

app $ ./build/main
[ATTACK] Pikachu:Lv.1:Scratch!
[ATTACK] Bulbasaur:Lv.5:Scratch!
[ATTACK] Pikachu:Lv.11:ThunderShock!
[ATTACK] Pikachu:Lv.4:Scratch!
[ATTACK] Pikachu:Lv.20:ThunderShock!

ちなみにコードはこんな感じである。

import PokemonKit

let a = PokeDeck(pokemons: [
    Pikachu(level: 1),
    Bulbasaur(level: 5),
    Pikachu(level: 11),
    Pikachu(level: 4),
    Pikachu(level: 20)
])

for pokemon in a.pokemons {
    do {
        try pokemon.attack(move: ElectricMove.ThunderShock)
    } catch {
        try! pokemon.attack(move: NormalMove.Scratch)
    }
}

無駄に凝ってしまった。

記事にしてしまったし一応コードをアップしておいた。ご参考まで。

https://github.com/toshi0383/PokemonAttackDemo

参考

AppleTVの時刻ズレ問題

Forumに投稿しました。
https://forums.developer.apple.com/message/139631

AppleTVは、当然ですが内部に時計を持っています。Siriに「今何時?」と尋ねると、ちゃんと答えてくれます。しかし、答えてくれる時間がおかしいことが、たまにあります。

状況を箇条書きにすると、

  • AppleTV内部のクロックは、電源に刺さっていない状態だと進まない。(iPhoneは進みますよね)
  • 起動時やネットワーク接続時にNTPサーバに問い合わせて時刻を取得しているようだが、これが結構失敗したり、NTP接続自体していないように見えることがある。
  • 失敗すると、時刻がずれたままアプリが使えてしまう。
  • 時刻をシビアに扱うアプリケーションの場合、アウト。
  • ユーザが自分で端末の時刻を修正する方法は、ない。

という感じです。

解決策を効き目がない順番に記載します。

  • 待つ。ほっとく。
  • ネットワークに接続しなおす。
  • ネットワークにつながった状態で 設定=>システム=>再起動 する。

確認した範囲では、再起動以外に方法はなさそうで、アプリ側でもそのような対応を促すメッセージを表示する仕様となりました。再起動してもダメな時もありました。

今回はDRM関係の処理で再生開始時の時刻を厳しめにチェックしているところがあって、時刻がずれていると動画が再生できない!という割とクリティカルな問題に発展してしまいました。

電源を抜き差しするなんてことは一般のユーザだとそんなにないだろうという思想なのでしょうか。

正直アプリ側ではどうしようもないので、Appleさんがなんらか修正してくれることを期待しています。

ちなみにAppleTVの時刻は上記のSiri Remoteか、XcodeのDevice画面のコンソールで確認できます。 tvOSアプリ開発者の皆さんはご注意ください。

コードを綺麗にする誘惑に君は耐えられるか

try! Swiftのセッションの翻訳を読んで、ちょうど今考えていたことと関連したので、記事にしようと思いました。 https://realm.io/jp/news/tryswift-daniel-steinberg-blending-cultures/realm.io

まだObjective-Cでメンテナンスが続いているアプリもたくさんあるわけですが、せっかくSwiftにしたんだったら、新しいパラダイムも取り入れていきたいですよね。 でも実装スピードを優先するのであれば、それぞれのメンバーが慣れている方法でとりあえず突き進んで仕舞う方がいいと思います。

フェーズ1はとりあえず動く事を優先して、コードはあえてぐちゃぐちゃのまま残す。 そうすればとりあえず期限内に完成して余裕が生まれるはずなので、その隙にガンガンリファクタを入れてスタイルを統一するなり新しい技術を試すなりすればいいと思います。

Swiftは頑張れば頑張るほど綺麗なコードが書けるので、誘惑に負けてしまう人も多いと思います。私がその例です。 SwiftLintを入れてチームのコードスタイルを統一する人もいると思います。私がその例です。 もっと綺麗に書ける!と思ってRxSwiftを導入するチームもあると思います。私がその例です。 しかしあまり綺麗なコードにこだわっていると、一歩一歩が重たくなってしまいます。コードの再利用を考えるのはほどほどにして、とにかくその機能なり画面を完成させましょう。

ただあまりぐちゃぐちゃすぎるとあとでリファクタできなくなるので、開発の早い段階でコードレビューなり勉強会なり開いて意識の統一を図るべきだと思います。 そうすればメンバーの開発スピードが上がってきた頃には、コードレビューが要らなくなると思います。実際いまは、速度を最大限優先してコードレビューはなしにしています。気になったら勝手に見てね、という感じです。不具合や仕様変更があればその人が責任持って直せばいいので。

特に画面遷移の処理を共通化するのは、簡単そうに思えて、そして実装するとなんとなく動くのですが、全体のバランスを取るのが実は大変だったりします。こっちの画面からだと開かないんだけど。。とか、こっちの画面から遷移して閉じたら余計なものまで閉じちゃう!といった問題が起こります。後回しにした方がいいかもしれません。

開発中のプロジェクトは今ちょうど佳境に入っていまして、少し反省も兼ねて今の気持ちを綴ってみました。

ネットには綺麗なコードが溢れていますが、現実のプロジェクトでそれを最初から実現しようとするのは、もしかしたら無理があるかもしれません。現実の状況をよく見極めましょう。 自分のチームに最適な進め方を見つけて、その価値観を早いうちにチームで共有できると、ベストだと思います。

みなさんのご武運をお祈りいたします。

Tokyo iOS Meetup で発表してきました

Tokyo iOS Meetupというイベントの主催者に声をかけられて、tvOSアプリの開発について発表してきました。 www.meetup.com

内容は、先日のyidevで発表した内容を最新の情報で更新して、新たに得たtipsを追加したものになります。

なおtvOSのUIKitとAVKitについて下記の3つ話したのですが、こちらはスライドではなく口頭でパパッと説明しちゃったので、別途記事に起こそうと思っています。

  • UITabBar
  • UITableVIew
  • AVPlayerViewController

英語で発表するのなんてほとんど経験がなかったのですが、まあプログラミングネタは半分以上英語だし伝わるだろくらいのテンションで乗り込みました。 実はちょっと心配していた質疑応答も滞りなくこなすことができてよかったです。

勉強会に参加するということ

今回英語で発表してみて、なんとなくこういう勉強会についての想いを綴りたくなったので、書いてみます。

「そんないきなり知らない勉強会によく飛び込んでいけるね」と、会社の面談や家でもよく言われます。 最初こういうのに参加し始めた頃は確かに緊張しましたけど、だんだん「ああみんなオフ会みたいなもんなんだな」というのがわかってくるので、そんなに気負うこともないんですよね。 隣の席の人に話しかけたら実は既にTwitterでフォロワー同士だったとか。 基本開き直って話しかける勇気だけ持っていけば楽しめるはずです。 勉強会に行く意義?楽しいからでしょ。

ちなみに英語縛りの場合ハードルが高いかもしれませんが、英語圏の人は一般的に明るくて話好きな人が多いです。 どう話しかけたらいいかわからない人は、まずは主催者に「Thank you for great Benkyokai」とか話しかけましょう。Benkyokaiという単語を使うのがポイントです。これで相手は「あ、こいつ英語苦手なんだな」と察してくれます。私がよく使う手です。

さらに主催者というのはほぼ例外なく面倒見が良いしだいたい日本語ができるので、英語が話せなくてもなんとかなります。 どうしても通じなかったら、諦めて日本語に切り替えましょう。きっと相手が困惑して近くの日本人を引っ張ってきてくれるはずです。終わる頃には、言葉なんて関係ないんだな、ということがわかると思います。

もちろん面倒くさければ無理にコミュニケーションを取る必要もないと思います。彼らは本当に議論するのが好きなので、議論が白熱してきたら私はだいたい「まーた始まったよ」とか思いながらMacを開いてブログを読んだりしています。今回はBDDやTDDをどうやるかでアツくなっていました。

仮に一言も理解できなかったとしても、そこに行ったということはあなたの新しい行動なので、いいことしかないと思います。まあ理解できた方が楽しいと思いますけどね。

ちなみに私の英語に関しては学生時代に10ヶ月ホームステイした経験がアドバンテージにはなっています。が、流暢とは言い難いレベルだと思います。 TOEIC(Reading & Listening)は810点ですけどWWDCの動画とかHuluの映画やドラマを見まくっているせいでちょっと耳がいいだけです。 New York Timesとか読めない。 基本単語並べとけば伝わるだろくらいのノリです。

普段はこういうところでイベントを探したり企画したりしているのですが、

英語話者が集まる勉強会やイベントはこちらで探せそうでした。今回のiOS Meetupもこちらを長いこと利用しているようです。「代々木公園でお花見しようず!」みたいな集まりもありますね。

どのサイトも、イベントの後に参加者同士のつながりを促進する仕組みが用意されています。日本では調整さんなどのサイトが今でも活躍していると思いますが、こういうサイトを使うと運営も楽だし、コミュニティも成長しやすいと思います。Facebookグループでもいいですが、Facebookやりたくない人というのも一定数いると思いますので。

筒がない感じでつらつらと書いてみました。

ジャズのアドリブもそうなんですが、英語はとにかく基礎練習と同じくらい、場数を踏むことが重要です。会話というのは相手がいないと成り立たないので、単語をたくさん覚えているだけでは意味がないのです。

別に私は英会話の先生でもなんでもないですが、英会話教室に通わなくても、エンジニアの皆さんはまずはこういう勉強会に顔を出してみてはいかがでしょうか?

enumの値チェック

enumをrawValueから生成するときに、値を制限したいことはよくあると思います。 Himotokiと組み合わせることを前提に、rawValueが不正だった場合に詳細なエラーメッセージを含むDecodeError.TypeMismatch をthrowするinitializerを作ったので共有します。 なおRawValueがIntの場合限定です。有効範囲を0から11として無理やりinitializerを呼んでみて、ダメだったらエラーにするというなんとも無理やりな実装です。

こんな風に使います。

enum Status: Int {
    case Initial = 0, Active = 1, Inactive = 2
}

struct Bar: Decodable {
    let status: Status
    static func decode(e: Extractor) throws -> Bar {
        return Bar(
            status: try Status(int: try e <| "status", keyPath: "status")
        )
    }
}

以下、init(int:) の実装です。

import Foundation
import Himotoki

extension RawRepresentable where Self.RawValue == Int {

    /// Failable initializer for any RawRepresentable(such as enum) types
    /// If passed rawValue didn't match expectation,
    /// throws detailed error with `expected` and passed in `keyPath`.
    ///
    /// - parameter int: the rawValue
    /// - parameter keyPath:
    /// - throws:  DecodeError.TypeMismatch
    /// - seealso: for Error type, see Himotoki
    init(int: Self.RawValue, keyPath: KeyPath = "") throws {
        guard let _ = Self(rawValue: int) else {
            throw DecodeError.TypeMismatch(expected:
                expectedIntValues(Self.self), actual: "\(int)", keyPath: keyPath)
        }
        self.init(rawValue: int)!
    }

    init?(optionalInt: Self.RawValue?, keyPath: KeyPath = "") throws {
        guard let int = optionalInt else {
            return nil
        }
        try self.init(int: int)
    }
}

private func expectedIntValues<T: RawRepresentable where T.RawValue == Int>
    (type: T.Type) -> String
{
    return (0...11) /// Note: Assuming that the valid rawValues are between 0 to 11 .
        .flatMap{type.init(rawValue: $0)}
        .reduce("") {acc, e in "\(acc == "" ? "" : acc+",")\(e.rawValue)"}
}

フィードバックいただけると嬉しいです。