LGTM iOS版をリリースしました

f:id:toshi0383:20161014153142p:plain

どうも @toshi0383 です。この度めでたくLGTM のiOS版をリリースしましたのでお知らせ致します。

https://itunes.apple.com/jp/app/lgtm-super-quick-lgtm-image/id1161220259?ls=1&mt=8&at=10l8JW&ct=hatenablog

早いもので、mac版をリリースしてからもう1年経っていたんですね。

LGTM Macアプリを作りました - 落ち着こう

こちらはランディングページです。結構かっこいいだろう。

LGTM - LGTM image picker for OSX/macOS

iOS版の見た目はmac版とほぼ同じですが、お気に入りの同期機能を先行して盛り込んでいます。

もともと仕事のプロジェクトチーム内でプルリクエストにおもしろ画像を貼るのが流行っていたというのが元になっているので開発者用ツールとも言えるのですが、単純に使っているだけでもちょっと楽しいので、エンターテインメントのカテゴリにしました。

みなさんのおもしろ画像検索の助けになれば幸いです。

この楽しさをもっと色んな人に共有したいと思い、iOS版は基本無料とし、mac版も少しの間値下げしました。 この機会に面白画像を閲覧して楽しんで見てはいかがでしょうか。

開発の様子

この機会に中のコードを全てSwift2.2からSwift3に移行しました。正直そろそろアプリもライブラリもBreaking Changeが面倒臭いので早いとこObjective-Cくらい枯れてきてほしいものですね、なんて。。

お気に入り機能は、出たばかりのRealm Mobile Platformを利用しています。 今回本当にタイミングが良くて、ちょうどiOS版の開発に取り掛かろうという時に公開されて、そのままエイヤ!で導入しました。 我ながら、新しいものはとりあえずソッコーで使ってみないと気が済まないタイプなのだな、と。。 アプリ側のコードは認証のコードが増えたくらいで済んだので、Realm使っていてよかったーと思いました。

最初Realmのサーバをどこに立てようか迷ったのですが、Realmのslackチャンネルで質問したところDigitalOceanが手軽で良さそうとのことだったので、試して見ています。Realmのslackチャンネルはたくさんの方が参加していて、いち開発者としてとても心強い存在です。

DigitalOcean自体セットアップはとても簡単でした。環境ごとにチュートリアルの記事が充実していて、ほぼコマンドをコピーペーストしただけで終わりました。

あと開発者目線的なポイントとしては、iPad対応でしょうか。近い将来、Xcodeなどの開発環境がiPadに載ることを見越して(載るのか??)、LGTMとしてはiPadマルチタスク対応は外せないと考えていました。時間かかるかと思ってたんですが、全てstoryboardで実装したところ、サイズクラスのことはほとんど気にすることなく勝手にあの縦長の画面に対応することができました。storyboardの威力を改めて思い知った気がします。ま、iPad用のデザインをしていないからというのが大きいのでしょうけど。

まとめ

mac版をリリースした頃はiOSXcodeもまだまだ不慣れでしたが、今では経験も積んで、随分と開発スピードも上がりました。これもひとえに普段勉強会やSNSでお世話になるiOSコミュニティの皆さんのおかげだと思っています。これからも楽しんでアプリ開発続けていければと思っておりますので、宜しくお願い申し上げます。

あと、よかったらストアのレビュー書いてください!😉

https://itunes.apple.com/jp/app/lgtm-super-quick-lgtm-image/id1161220259?ls=1&mt=8&at=10l8JW&ct=hatenablog

クロージャでプロパティを初期化する

Caution! この記事はまだSwift2.2です!

普段何気なく使っているのですが、たぶん初めて見た人は混乱するだろうなと思ったので。

extension UIImage {
    private static var contentPlaceholder: UIImage = {
        return UIImage(named: "placeholder_b.png")!
    }()
    private static var personPlaceholder: UIImage = {
        return UIImage(named: "placeholder_a.png")!
    }()

    private var data: NSData {
        let provider = CGImageGetDataProvider(self.CGImage)
        return CGDataProviderCopyData(provider)!
    }

    var isPlaceholder: Bool {
        return self.data.isEqualToData(UIImage.contentPlaceholder.data)
            || self.data.isEqualToData(UIImage.personPlaceholder.data)
    }
}

一回メモリに載せたらそのあとはそのまま取っておきたいような時は通常普通にstatic変数でいいと思うのですが、初期化処理が1行で書けないみたいな時にこういう書き方ができます。(上の例は1行で書けそうですが)

なお上の場合それが目的ではないですが、lazyロードになりますね。

どこかでこの static varlazy var に再代入した後の挙動についてのスライドを見た記憶があるんですが、見つけられませんでした。

tvOS-10-Sampler

Hey guys I just created a tiny sample app demonstrating tvOS 10 new APIs.

github.com

I open-sourced it because the AVContentProposal API is still buggy and I wanted feedbacks from other developers from the world.

So this is the crash you can experience in tvOS10 Simulator. gyazo.com

The same app won't crash on the device, but user will left with weird screen like this. f:id:toshi0383:20160924190748j:plain

Sorry my AppleTV is in Japanese, but you can see the ContentProposalViewController in front of the first view.

This is the stacktrace.

2016-09-24 19:01:29.520 TVOSTest[87178:254281] Presenting view controllers on detached view controllers is discouraged <TVOSTest.ViewController: 0x7fb02c026e00>.
2016-09-24 19:01:29.543 TVOSTest[87178:254281] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutXAxisAnchor:0x60800086f280 "AVFocusProxyView:0x7fb02b542db0.left"> and <NSLayoutXAxisAnchor:0x60800086f540 "AVPlayerLayerView:0x7fb02b40ec50.left"> because they have no common ancestor.  Does the constraint or its anchors reference items in different view hierarchies?  That's illegal.'
*** First throw call stack:

It's obviously AutoLayout's error, but there is nothing I can do because I'm not using any AutoLayouts in this app...

So it's depends on you whether you should use AVContentProposal or not. I will update if I found any workarounds.

Happy tvOS development!!!

ぼくの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アプリ開発者の皆さんはご注意ください。