みずぴー日記

人間の再起動ボタンはハワイのビーチにある

📱Zoi for iPhone

NEW GAME! のコマ検索 - みずぴー日記の成果を用いて、iPhoneからNEW GAME!のコマ検索を行なえるようにした。

経緯

ニコニコでやっているアニメの一挙放送を見ると、PCの前に4〜6時間ほど拘束される。 そのときに、なんとなくXcodeを立ちあげてコードを書きはじめてしまった。

機能

コマのインクリメンタル検索

NEW GAME! のコマ検索 - みずぴー日記に各コマのセリフを入力済みなので、それを移植してインクリメンタルに検索できるようにした。

f:id:mzp:20160305085536p:plain

またUIActivityViewControllerを用いたアプリ間連携も行なえる。

f:id:mzp:20160305085447p:plain

Spotlight対応

App Searchに対応したため、Spotlightから検索ができる。

f:id:mzp:20160305085729p:plain

Spotlightのインデックスに登録するタイミングは、悩んだが、

  • 初回起動時
  • 明示的に再インデックスを指示した時

の2つにした。 最初は、初回起動時のときのみにしていたが、インデックス処理でエンバグしたときの対応が面倒だったので、再インデックスボタンを追加した。

インデックスへの追加はApp Search プログラミングガイド: 検索の基本にあるコードをほぼそのまま使っている。

func add(items : [ZoiJson.Item], complete: () -> ()) {
  let si = items.map { self.searchableItemFor($0) }
  CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(si) { error in
    if error != nil {
      print(error?.localizedDescription)
    } else {
      complete()
   }
}

private func searchableItemFor(item : ZoiJson.Item) -> CSSearchableItem {
  let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypePNG as String)
  attributeSet.title = item.script
  if let image = ZoiImage.image(item) {
    attributeSet.thumbnailData = UIImagePNGRepresentation(image)
  }
 return CSSearchableItem(uniqueIdentifier: item.path, domainIdentifier: "zoi", attributeSet: attributeSet)
}

newgame:// 対応

ぐらばくさんが提案していた、newgame:// をカスタムURLスキームとして取り込んだ。(参考: ぐらばく on Twitter: "newgame://3/65/2/2 もいい https://t.co/hC6t3KxYj0")

https://twitter.com/mzp/status/702863419226005505

newgame://巻数/ページ番号/列/行 というフォーマットだが、目次等の存在により書籍のページ番号と先頭からページ数は一致しないので、そこは補正した。 また、ページ内の何コマ目なのかは保持しているが、何列目かは保持していないので、そこの補正も行なっている。

    func toPath(url : NSURL) -> String? {
        if let vol = url.host,
           let paths = url.pathComponents,
           let page = Int(paths[1]),
           let col = Int(paths[2]),
           let row = Int(paths[3])
        {
            let pageStr = NSString(format: "%03d", page + 2)
            let pos = (col - 1) * 3 + row
            return "data/vol\(vol)/vol\(vol)_\(pageStr)_\(pos).jpeg"
        } else {
            return nil
        }
    }

OnDemandResource対応

Testflightにアップロードしようとしたが100MBを越えてしまった。そこで、OnDemandResourceを利用し、必要になったタイミングで画像を取得するようにした。

OnDemandResourceの使い方はオンデマンドリソースでiOSアプリを軽くする - Qiitaを参考にした。

またOnDemandResourceで取得した画像を、ImageViewに非同期で設定するために https://github.com/Haneke/HanekeSwiftを用いてる。標準ではOnDemandResourceからの取得はできなかったので、独自のFetcherを作った。

import Foundation
import Haneke

class OnDemandResource {
    func fetch(fail : NSError? -> Void, succeed : Void -> Void) {
        let request = NSBundleResourceRequest(tags: Set(arrayLiteral: "zoi"))
        request.conditionallyBeginAccessingResourcesWithCompletionHandler() {
          resourceAvailable in
            if resourceAvailable {
                succeed()
            } else {
                request.beginAccessingResourcesWithCompletionHandler() { error in
                    if error == nil {
                        succeed()
                    } else {
                        fail(error)
                    }
                }
            }
        }
    }
}

class OnDemandFetcher<T : DataConvertible> : Fetcher<T> {
    private let resource = OnDemandResource()
    private let getValue : () -> T.Result?

    init(key: String, @autoclosure(escaping) value getValue : () -> T.Result?) {
        self.getValue = getValue
        super.init(key: key)
    }

    override func fetch(failure fail: ((NSError?) -> ()), success succeed: (T.Result) -> ()) {
        resource.fetch(fail) {
            if let result = self.getValue() {
                self.main { succeed(result) }
            } else {
                self.main { fail(nil) }
            }
        }
    }

    override func cancelFetch() {}

    private func main(f : () -> ()) {
        dispatch_async(dispatch_get_main_queue(), f)
    }
}

// 使い方
let fetcher = OnDemandFetcher<UIImage>(key: self.item.path, value: UIImage(named: "zoi.png"))
self.image.hnk_setImageFromFetcher(fetcher,
            placeholder: UIImage(named: "placeholder.jpeg"),
            format: Format<UIImage>(name: "original"))

📷 Live Photo生成、その後

📷 mov/jpegからのLive Photo生成 - みずぴー日記でmov/jpeg から Live Photo生成をできるようにしてから、いくつかの進展があった。

縦MVの発見

デレステにて縦画面MVを実現する方法が発見された。 これにより、iPhoneの画面サイズに適した動画が、簡単に得られるようになった。

togetter.com

GUIの作成

banjunによりGUIが作成された。(参考: movファイルからLive Photoを生成するLoveLiverにGUIをつけた - ツバメになったバリスタ)

https://github.com/mzp/LoveLiver/releases

これによりCLI版で面倒だった

  • Live Photoにしたいシーンの選択
  • 3秒間の動画の切り出し
  • JPEG画像の準備
  • Photos.app へのD&D

といった手間がなくなり、Live Photoを量産できるようになった。

f:id:mzp:20160221181047p:plain

Live Photoの量産

上記2つにより、大量のLive Photoが作られるようになった。

f:id:mzp:20160221182457p:plain

毎晩、iCloud Photo Sharing経由で新作が届く。

f:id:mzp:20160221182332p:plain

NEW GAME! のコマ検索

NEW GAME!の全コマをインクリメンタルに検索できるツールを作った。*1

f:id:mzp:20160207195724p:plain

経緯

NEW GAME! 3巻を読んだためNEW GAME熱が上ったので、ゆゆ式を無限に楽しみたかった話 〜 ゆゆ式 Advent Calendar 2014 20日目 〜 - non117's diaryツール*2を移植し、コマ分割およびアノテーションの付与を行なった。

最初はコマの分割だけのつもりだったが、気がついたら各セリフの入力とキャラのタグづけも行なってしまった。 入力には一週間くらいかかっている。

f:id:mzp:20160207195814p:plain:w200f:id:mzp:20160207195826p:plain:w200

アノテーションの付与が完了したので、各コマを検索するツールを作成した。

機能

セリフによるインクリメンタル検索

セリフによってコマをインクリメンタルに検索できる。また、該当のコマが単行本のどのあたりに登場しているのかも表示する。

f:id:mzp:20160207195724p:plain

また、すべてのセリフを入力しているため、セリフがないコマの検索もできる。

f:id:mzp:20160207200112p:plain

キャラ指定の検索

特定のキャラを指定した検索もできる。複数指定すれば、同じコマ内に登場しているコマを検索できる。

例えば、「ひふみ」と「ねね」にチェックをいれれば、この二人が同じコマに登場したのは一度しかないことが確認できる。

f:id:mzp:20160207200554p:plain

ページ指定の検索

特定のコマを見つけたあと、同じページにあったコマの検索もできる。 これを利用することで、「プリンを食べたねねっちの表情」といったコマを見つけることができる。

f:id:mzp:20160207200807p:plain

所感

 💫Tumblotte 0.1.0

💫Tumblrクライアントを作りはじめた - みずぴー日記 で書いたTumblrクライントをリリースした。

テキスト主体のブログを書きやすくすることを目指している。 そのためMarkdownのライブプレビュー機能などを実装しているが、Reblog機能などは実装していない。

f:id:mzp:20160117134517p:plain

機能

  • Tumblrへの投稿・更新
  • Markdownのライブプレビュー
  • 既存の投稿記事の取得
  • 投稿先のブログの切り替え
  • 投稿した記事をWebブラウザで開く

前回から、基本的には変わっていない。エラー処理などをだいぶまともにした。

実装していない機能

自分があまり使わないので実装してない。必要になったら実装する。

  • MacOS X以外のサポート
  • Text以外の記事

ダウンロード

https://github.com/mzp/tumblotte/releases/tag/0.1.0

前回からの変更点

dmgの作成

インストールするときに使うdmgを作った。以下のような /Applications へのコピーを促すような背景画像も作成した。

f:id:mzp:20160117134813p:plain

コード署名の追加

*.app を作るときにコード署名をするようにした。

細かい修正

自分で使っていて気になった部分をいくつか修正した。

  • 常にライブプレビューを行なうとレスポンスが悪いので、300ms以上キー入力がなかったときにプレビューを更新するようにした。(debounce)
  • メインメニューの項目を整理した。
  • ライセンスとしてMITライセンスを採用した。

 ⌨AquaSKK 4.3.4: US配列におけるAZIKの動作改善

US配列 + AZIKにおいて、x[ の入力を行なえるようにした。

ダウンロード

https://github.com/codefirst/aquaskk/releases/tag/4.3.4

変更内容

US配列 + AZIKでは [ がかなモードの切り替えに使われる。 そのため、[ に割り当てられている の変換が行なえない。

ddskkでは x[ で 元の [ を代用できるようにして、この問題を解決していた。(参考: SKK Manual: AZIK)

AquaSKKでもこの方式を採用することにした。

余談

🎍新年

f:id:mzp:20160109104544p:plain

Copyright(C) 2014-2016 という部分を更新したので、新年を迎えた感じになった。

状態遷移機械

変換の動作は、専用の状態遷移機械記述ライブラリで定義されている。 このライブラリに関するドキュメントは Generic State Machine Library for C++ しか残っておらず、苦労した。

分かったことをメモしておく。

  • 状態は関数で表現され、遷移のルールはその関数の中身で表現される。
  • 定義は複数のファイルにまたがって行なわれるが、CPPを用いて1つのファイルに結合された上でコンパイルされる。
  • 状態はSubStateを持てる。たとえば、かな変換状態のSubStateとしてひらかな変換状態、カタカナ変換状態、半角カナ変換状態なのが定義されていた。

 💫Tumblrクライアントを作りはじめた

記事の投稿に特化したTumblrクライアントを作りはじめた。

f:id:mzp:20160103203837p:plain

レポジトリ

https://github.com/mzp/tumblotte

動機

TumblrはリブログするだけのWebサービスでもなく、高機能なブログサービスでもあることに気がついた。

  • Markdownで書ける。
  • 独自ドメインでの運用もできる。
  • 複数のブログも管理できる。
  • テーマも多数存在する。

しかし、TumblrのWebUIは長文を書くのには向いていないので、Tumblrのクライアントを作りはじめた。

ElectronやReact.js等のJavaScriptまわりの技術にキャッチアップしたかった、という理由もある。

機能

  • Tumblrへの投稿・更新
  • Markdownのライブプレビュー
  • 既存の投稿記事の取得
  • 投稿先のブログの切り替え
  • 投稿した記事をWebブラウザで開く

Kobitoを意識しながら作りはじめた。 が、インターネットが不自由な環境で作っていたので、思ったよりUIが似ていない。

名前の由来

どういう名前にするか相談したときに、Charlotteの一挙放送をしていたので、こうなった。

知見

  • ElectronでOAuth認証をする方法は Electron. oAuth authentication with GitHubを参考にした。
  • Tumblrに投稿にするにはtumblrを使っている。しかし、いくつか機能が不足していたので、自分でforkして拡張した。mzp/node-tumblr
    • tumblrwksを使ったほうがよかったかもしれない。
  • "electron boilerplate" で検索したら、5つくらいでてきてツラかった。 結局、boilerplateは使わずに自分で書いた。

画像で見る開発の流れ

開発開始直後(2015/12/30)

f:id:mzp:20160103215512p:plain

PureCSSのEmail layoutをざっくりあてた直後。まだ表示ができるだけで、投稿等はできない。

基本機能完成(2015/12/31)

f:id:mzp:20160103215848p:plain

ライブプレビュー機能Tumblrへの投稿機能を付けた。

UIの調整(2016/1/1)

f:id:mzp:20160103220331p:plain

FontAwesomeを使って、各ボタンにアイコンを割り当てていった。

ログイン機能(2016/1/2)

f:id:mzp:20160103220524p:plain

ここまでAccessTokenは固定だったので、ログイン機能をつけた。ログイン画面をどうするか迷ったあげく、結局ボタン1個だけを配置した。

投稿先ブログの切り替え(2016/1/3)

f:id:mzp:20160103220954p:plain

投稿先のブログを切り替えれるようにした。

代償

⚠️AquaSKK with iCloud

☁️AquaSKKのiCloud対応 - みずぴー日記の内容を整理した上で、パッケージを作成した。 自分で試すためのが主な目的なので、他の人が利用することはあまり想定していない。

Release iCloud対応(1) · codefirst/aquaskk · GitHub

f:id:mzp:20151228204112p:plain

注意点

  • 今後、データの持ち方を変える可能性がある。 その際は、同期データがすべて消失する。
  • ローカルのユーザ辞書が破壊される可能性がある。実際、開発中に二度ほど破壊した。
  • その他、予期しない動作をするかもしれない。

何かあったらiCloud対応PRに書いてほしい。 対応できるかどうかは分からない。

リリースするまでに解決しなければいけない課題

ビルドに必要な証明書の管理

iCloudに接続するために、AquaSKKにひもづいた証明書が必要となった。 その証明書は、ローカルにしか存在していないので、以下のような問題が生じる。

  • AquaSKKのビルドを他の人ができない。 そのためチーム開発が困難になる。
  • TravisCI上でビルドを行なえない。

FlickSKKとの共有をどうするか

FlickSKKのビルドは@が自分のIDで行なっているので、AquaSKKiCloud領域にはアクセスできない。

辞書の形式はほぼ同一なので、なんとか共有できるようにした。個人アカウントの制約のようなものなので、起業したほうがいいかもしれない。

x86対応

iCloud対応のために用いているCloudKitがx86_64バイナリしかないため、x86に対応できない。

最近のOS Xx86では動かないようだし、対応をやめてもいいと思っている。

前回からの変更内容

レコードIDの変更

前回は見出し語は重複しないものとして、見出し語をレコードIDとして用いていた。

しかし、以下のように見出し語が重複している場合もありえるので、見出し語をレコードIDとして用いるのをやめた。

;; okuri-ari entries.
わるi /悪/
;; okuri-nasi entries.
わるi /悪/

同期用スレッドの統一

前回は、ローカルの辞書をiCloudに送信するスレッドと、iCloudから取得したデータをローカルの辞書にマージするスレッドを別々にしていた。

そのせいでコードが複雑化していたため、すべて1つのスレッドで行なうようにした。

コードの整理

今後メンテナンスすることを考えて、コードを整理した。

  • コピペですませていた部分を共通化した。
  • Objectvie-CとC++の部分を増やし、Objective-C++部分を減らした。