Xcode10 にしたら Multiple commands produce ... Info.plist で消耗した

個人プロジェクトを Xcode10 対応してたらこんなエラーが出た。

:-1: Multiple commands produce '/Users/hoge/Library/Developer/Xcode/DerivedData/KotaichiDex-eiyecmvfctikcwcspitwpzskudue/Build/Products/Debug-iphonesimulator/PokemonRealm/PokemonRealm.framework/Info.plist':
1) Target 'PokemonRealm' (project 'Pods') has copy command from '/Users/hoge/Xcode/KotaichiDex/Pods/PokemonRealm/PokemonRealm/Info.plist' to '/Users/hoge/Library/Developer/Xcode/DerivedData/KotaichiDex-eiyecmvfctikcwcspitwpzskudue/Build/Products/Debug-iphonesimulator/PokemonRealm/PokemonRealm.framework/Info.plist'
2) Target 'PokemonRealm' (project 'Pods') has process command with output '/Users/hoge/Library/Developer/Xcode/DerivedData/KotaichiDex-eiyecmvfctikcwcspitwpzskudue/Build/Products/Debug-iphonesimulator/PokemonRealm/PokemonRealm.framework/Info.plist'

PokemonRealm というのは private pods として利用しているライブラリ。こいつの Info.plist がどうこう怒られている。

PokemonRealm の .podspec はこう書いていた。

Pod::Spec.new do |s|
  ...
  s.source_files       = "PokemonRealm/*"
  ...
end

こうしていると、 PokemonRealm/Info.plist もライブラリ管理されてしまって、それが重複してしまうのでダメらしい。

なので、 Swift ファイルだけ対象にするようにした。

Pod::Spec.new do |s|
  ...
  s.source_files       = "PokemonRealm/*.swift"
  ...
end

そしたら無事エラーが消えて Xcode10 でビルドできた。

参考

github.com

聴覚情報処理障害かもしれない

poem.mizdra.net

この記事を読んで、心当たりがありすぎる。自分は耳が悪いのかも知れないと思いつつも、聴力検査は問題がなかったし、みんなそういうものなのかなと思っていた。しかし症状を見ると聴覚情報処理障害っぽく思える。

例えばこれは1年半前のツイート。

これは今でも全く治ってない。いつからこの症状が出ているのかもわからない、大学生の時はすでに悩んでいた気がする。

相手の話の一部分が聞き取れないことが頻繁にあって、聞き流してしまう事も多い。聞き流した結果会話が途切れて、変な空気になる。変な空気になるともう聞き返せないし、相手が何を言っていたかわからないので次の会話をはじめにくい。そうして人間とのコミュニケーションが嫌になっていく。

聴覚情報処理障害の特徴チェックリストはこうなった。(そもそもこのチェックリストの信憑性が怪しい気もするが、それはそれ)

  • [ ] 言葉の発達が遅い
  • [x] 聞き返しが多い
    • 相手の話の一部分だけ全く理解できないことが多々ある
  • [x] 騒がしい場所での聞き取りが苦手
    • 駅で電車がきてる時は絶対に話しかけないようにしているし、大人数での飲み会はストレス
  • [x] 文字情報の方が正確に理解できる
    • これはそもそもリアルタイム言語処理より文字情報の方が繰り返し読めるので当然では?
  • [x] 音韻が似ている言葉を聞き間違える
    • 字幕を見ると違う単語であることが多々ある
  • [x] 言葉での指示への反応が遅い
    • 相手の真意を確かめるために自分の解釈を相手に伝えて正しいか確かめることをよくする
  • [x] 聴覚情報の記憶力が弱い
    • ちょっと長い話をされると古い話が頭から抜けていく。
    • みんなそういうものだと思ってたが違うのか?

こうして見ると完全に聴覚情報処理障害なのか?とも思ったが、本当に聴覚情報処理障害の人はコールセンターで働く事も困難らしい。私は新卒の頃2ヶ月コールセンター研修を行なったが特に問題はなかった。なので私は軽度の聴覚情報処理障害か、ただの勘違い野郎ということになる。

しかし実際のところめっちゃ困っている。人と会話するのが億劫になるし電話がめちゃくちゃ嫌だ、なるべく文字情報でのコミュニケーションで済ませたい。

聴覚情報処理障害だと英会話は致命的なのか?と思うがどうなんだろう。日本語ですら聞き取れないのに英語はもっと聞き取れないのでは、という気もする。

全て気のせいであってほしい。

Tesseract-OCR-iOS を使う

【Swift】文字認識ライブラリ、TesseractOCR for iOSを試してみた - Qiita に使い方があるが、少し古かったので Xcode9.4, Swift4 で動かすための手順。

Podfile

pod 'TesseractOCRiOS', '4.0.0'

そして pod install、終わったら xcworkspace を開く。
Build すると warning が大量に出るが気にしてはならない...

traineddata の入手

https://github.com/tesseract-ocr/tessdata/tree/bf82613055ebc6e63d9e3b438a5c234bfd638c93 から必要なデータを入手。
注意点があって、https://github.com/tesseract-ocr/tessdata_best から DL したやつは動かない。
Using traineddata from tesseract-ocr · Issue #299 · gali8/Tesseract-OCR-iOS

今回は eng と jpn を DL した。

Project に traineddata を追加

https://github.com/gali8/Tesseract-OCR-iOS/wiki/Installation にあるように、このようなディレクトリ構成にする。

f:id:star__hoshi:20180819223629p:plain

注意点は、 tessdata をディレクトリにすること。 Group にしたら動いてくれなかった。

OCR する

Qiita には Bridging-header の話があるが、Swift 4 でははもう不要。

import UIKit
import TesseractOCR

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let tesseract: G8Tesseract = G8Tesseract(language: "eng+jpn")
        tesseract.delegate = self
        tesseract.image = UIImage(named: "ep.jpg")!
        tesseract.recognize()

        print(tesseract.recognizedText)
    }
}

extension ViewController: G8TesseractDelegate {
    func shouldCancelImageRecognitionForTesseract(tesseract: G8Tesseract!) -> Bool {
        return false // return true if you need to interrupt tesseract before it finishes
    }
}

結果

ポケモンGOのエーフィを読み込んだらこんな感じになった。

some("CPー889 丶\' 蓼蘭\n工一フイ \'\n92/92HP\n28.77kg @ Q9ーm\n一 重さ 工スバ一 高さ\n\n")

f:id:star__hoshi:20180819224115p:plain

Firebase SDK for Cloud Functions を 1.0 に Migration した

The Firebase Blog: Launching Cloud Functions for Firebase v1.0 にあるように、 Cloud Functions の SDK が 1.0 になった。と言っても SDK が 1.0 になっただけで、 Cloud Functions のベータが外れたわけではない。

Migration

Firebase SDK for Cloud Functions Migration Guide: Beta to version 1.0  |  Firebase という親切なドキュメントがある。
Firestore を使っている Project で、実際に 1.0 に Migration した。

noImplicitAny

まず、 TypeScript で使おうとするとエラーが出る。

github.com

any 型が使われてしまっているようで、 tsconfig に "skipLibCheck": true を追加してひとまずしのぐようにした。

New initialization syntax for firebase-admin

今まではこう書いていた admin の initializeApp が簡潔になった。

// before
admin.initializeApp(functions.config().firebase)

// after
admin.initializeApp()

こうなったのは、 functions.config().firestore が廃止され process.env.FIREBASE_CONFIG を使うようになったからのようだ。
今後 projectId などが必要な場合は環境変数から取るように、と書かれている。

let firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG);
/* {  databaseURL: 'https://databaseName.firebaseio.com',
       storageBucket: 'projectId.appspot.com',
       projectId: 'projectId' }
*/

SDK changes by trigger type

今までの Event trigger では、 event というパラメータだけを使っていたが、それが contextsnapshot (or change) という 2 つのパラメータに変更された。

// before
exports.dbWrite = functions.firestore.document('/path').onWrite((event) => {
  const beforeData = event.data.previous.data(); // data before the write
  const afterData = event.data.data(); // data after the write
});

// after
exports.dbWrite = functions.firestore.document('/path').onWrite((change, context) => {
  const beforeData = change.before.data(); // data before the write
  const afterData = change.after.data(); // data after the write
});

context

型定義はこうなっている。

/** The context in which an event occurred.
 * An EventContext describes:
 * - The time an event occurred.
 * - A unique identifier of the event.
 * - The resource on which the event occurred, if applicable.
 * - Authorization of the request that triggered the event, if applicable and available.
 */
export interface EventContext {
    /** ID of the event */
    eventId: string;
    /** Timestamp for when the event occured (ISO string) */
    timestamp: string;
    /** Type of event */
    eventType: string;
    /** Resource that triggered the event */
    resource: Resource;
    /** Key-value pairs that represent the values of wildcards in a database reference */
    params: {
        [option: string]: any;
    };
    /** Type of authentication for the triggering action, valid value are: 'ADMIN', 'USER',
     * 'UNAUTHENTICATED'. Only available for database functions.
     */
    authType?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED';
    /** Firebase auth variable for the user whose action triggered the function. Field will be
     * null for unauthenticated users, and will not exist for admin users. Only available
     * for database functions.
     */
    auth?: {
        uid: string;
        token: object;
    };

今までの event が持っていたパラメータに加えて、 Callable Functions の context が合わさったような感じ。 (Callable Functions の説明は こっち を参照)

snapshot

今までは DeltaDocumentSnapshot という型があったが、それが廃止され firebase.firestore.DocumentSnapshot が使われるようになった。
型定義はこうなっているが、前から使っていたものだし特に変更点もないと思う。

  /**
   * A `DocumentSnapshot` contains data read from a document in your Firestore
   * database. The data can be extracted with `.data()` or `.get(<field>)` to
   * get a specific field.
   *
   * For a `DocumentSnapshot` that points to a non-existing document, any data
   * access will return 'undefined'. You can use the `exists` property to
   * explicitly verify a document's existence.
   */
  export class DocumentSnapshot {
    protected constructor();

    /** True if the document exists. */
    readonly exists: boolean;

    /** A `DocumentReference` to the document location. */
    readonly ref: DocumentReference;

    /**
     * The ID of the document for which this `DocumentSnapshot` contains data.
     */
    readonly id: string;

    /**
     * The time the document was created. Not set for documents that don't
     * exist.
     */
    readonly createTime?: string;

    /**
     * The time the document was last updated (at the time the snapshot was
     * generated). Not set for documents that don't exist.
     */
    readonly updateTime?: string;

    /**
     * The time this snapshot was read.
     */
    readonly readTime: string;

    /**
     * Retrieves all fields in the document as an Object. Returns 'undefined' if
     * the document doesn't exist.
     *
     * @return An Object containing all fields in the document.
     */
    data(): DocumentData | undefined;

    /**
     * Retrieves the field specified by `fieldPath`.
     *
     * @param fieldPath The path (e.g. 'foo' or 'foo.bar') to a specific field.
     * @return The data at the specified field location or undefined if no such
     * field exists in the document.
     */
    get(fieldPath: string|FieldPath): any;

    /**
     * Returns true if the document's data and path in this `DocumentSnapshot`
     * is equal to the provided one.
     *
     * @param other The `DocumentSnapshot` to compare against.
     * @return true if this `DocumentSnapshot` is equal to the provided one.
     */
    isEqual(other: DocumentSnapshot): boolean;
  }

change

onUpdate と onWrite 時は snapshot ではなく Change<DocumentSnapshot> が使われる、Change の型を見てみる。

/** Change describes a change of state - "before" represents the state prior
 * to the event, "after" represents the state after the event.
 */
export declare class Change<T> {
    before: T;
    after: T;
    constructor(before?: T, after?: T);
}

今まで event.data.previous, event.data とデータを取得していたが、それが before, after と取れるようになった。わかりやすくて良い。

おわり

こんな感じで Migration が完了した、特に大きな混乱もなく、動作も問題なさそう。 (noImplicitAny は困っているが...)

Cloud Functions はとにかく重複発火をなくしてほしい。

Xcode 9.3 にしたら IDEWorkspaceChecks.plist というファイルが作られたが何者なのか

Xcode 9.3 でプロジェクトを開いただけで Hoge.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist というファイルが作られた。

Release Note を見ると以下のように書いてある。

Xcode 9.3 adds a new IDEWorkspaceChecks.plist file to a workspace's shared data, to store the state of necessary workspace checks. Committing this file to source control will prevent unnecessary rerunning of those checks for each user opening the workspace. (37293167) https://developer.apple.com/library/content/releasenotes/DeveloperTools/RN-Xcode/Chapters/Introduction.html

どうやら .gitignore せずにソース管理した方がよさそう。ワークスペースのチェックを無駄に実行しないで済むっぽい (なんのことかよくわからないが...)。

ちなみに初期状態のファイルの中身はこうなっていた。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>IDEDidComputeMac32BitWarning</key>
    <true/>
</dict>
</plist>

Cloud Functions は global 変数から Function Name などを取得できる

Cloud Functions で以下のコードを実行して見ると、現在実行している関数の名前を取得できる。

console.log(global.process.env.FUNCTION_NAME) // -> createUser

他にも GCP_PROJECT, FUNCTION_MEMORY_MB, FUNCTION_TIMEOUT_SEC などが env から取得できる。実際に取得できる env は以下。

env { X_GOOGLE_CODE_LOCATION: '/user_code',
  WORKER_PORT: '8091',
  X_GOOGLE_SUPERVISOR_INTERNAL_PORT: '8081',
  X_GOOGLE_WORKER_PORT: '8091',
  FUNCTION_IDENTITY: 'hoge@appspot.gserviceaccount.com',
  X_GOOGLE_FUNCTION_REGION: 'us-central1',
  GCLOUD_PROJECT: 'hoge',
  FUNCTION_NAME: 'functionName',
  X_GOOGLE_FUNCTION_MEMORY_MB: '256',
  SUPERVISOR_HOSTNAME: '192.168.1.1',
  PATH: '/usr/local/bin:/usr/bin:/bin',
  X_GOOGLE_GCLOUD_PROJECT: 'hoge',
  FUNCTION_REGION: 'us-central1',
  PWD: '/user_code',
  FUNCTION_TRIGGER_TYPE: 'OTHER_EVENT_TRIGGER',
  FUNCTION_TIMEOUT_SEC: '60',
  X_GOOGLE_FUNCTION_TRIGGER_TYPE: 'OTHER_EVENT_TRIGGER',
  NODE_ENV: 'production',
  SHLVL: '1',
  X_GOOGLE_FUNCTION_NAME: 'functionName',
  X_GOOGLE_ENTRY_POINT: 'functionName',
  X_GOOGLE_FUNCTION_IDENTITY: 'hoge@appspot.gserviceaccount.com',
  X_GOOGLE_GCP_PROJECT: 'hoge',
  CODE_LOCATION: '/user_code',
  GCP_PROJECT: 'hoge',
  FUNCTION_MEMORY_MB: '256',
  X_GOOGLE_SUPERVISOR_HOSTNAME: '192.168.1.1',
  PORT: '8080',
  SUPERVISOR_INTERNAL_PORT: '8081',
  X_GOOGLE_FUNCTION_TIMEOUT_SEC: '60',
  ENTRY_POINT: 'payOrder',
  OLDPWD: '/var/tmp/worker/',
  _: '/usr/bin/env',
  HOME: '/tmp' }

これらを使うと、メタ的に Function Name を取得できるので、ライブラリ側で関数名を読み取ることができる。
starhoshi/fire-slack では明示的に関数名を指定しないでも勝手に Slack 通知に関数名を載せるようにした。

await Slack.send(
  { webhook: {}, ref: reference, error: Error('Invalid Request') }
)

このコードだけでこんな通知を送れる。

f:id:star__hoshi:20180329184453p:plain

env 以外にもこんなのが取得できるが、使い道はなさそう。

global { global: [Circular],
  process: 
   process {
     title: '/nodejs/bin/node',
     version: 'v6.11.5',
     moduleLoadList: 
      [ 'Binding contextify',
        'Binding natives',
        'Binding config',
        'NativeModule events',
        'NativeModule util',
        'Binding uv',
        'NativeModule buffer',
        'Binding buffer',
        'Binding util',
        'NativeModule internal/util',
        'NativeModule timers',
        'Binding timer_wrap',
        'NativeModule internal/linkedlist',
        'NativeModule assert',
        'NativeModule internal/process',
        'NativeModule internal/process/warning',
        'NativeModule internal/process/next_tick',
        'NativeModule internal/process/promises',
        'NativeModule internal/process/stdio',
        'Binding constants',
        'NativeModule path',
        'NativeModule module',
        'NativeModule internal/module',
        'NativeModule vm',
        'NativeModule fs',
        'Binding fs',
        'NativeModule stream',
        'NativeModule internal/streams/legacy',
        'NativeModule _stream_readable',
        'NativeModule internal/streams/BufferList',
        'NativeModule _stream_writable',
        'NativeModule _stream_duplex',
        'NativeModule _stream_transform',
        'NativeModule _stream_passthrough',
        'Binding fs_event_wrap',
        'NativeModule domain',
        'NativeModule tty',
        'NativeModule net',
        'NativeModule internal/net',
        'Binding cares_wrap',
        'Binding tty_wrap',
        'Binding tcp_wrap',
        'Binding pipe_wrap',
        'Binding stream_wrap',
        'NativeModule http',
        'NativeModule _http_incoming',
        'NativeModule _http_common',
        'Binding http_parser',
        'NativeModule internal/freelist',
        'NativeModule _http_outgoing',
        'NativeModule _http_server',
        'NativeModule _http_agent',
        'NativeModule _http_client',
        'NativeModule url',
        'Binding icu',
        'NativeModule querystring',
        'NativeModule crypto',
        'Binding crypto',
        'NativeModule internal/streams/lazy_transform',
        'NativeModule string_decoder',
        'NativeModule console',
        'NativeModule zlib',
        'Binding zlib',
        'NativeModule cluster',
        'NativeModule dgram',
        'Binding udp_wrap',
        'NativeModule child_process',
        'Binding spawn_sync',
        'NativeModule internal/child_process',
        'Binding process_wrap',
        'NativeModule internal/socket_list',
        'NativeModule internal/cluster',
        'Binding signal_wrap',
        'NativeModule os',
        'Binding os',
        'NativeModule https',
        'NativeModule tls',
        'NativeModule _tls_common',
        'NativeModule _tls_wrap',
        'NativeModule _stream_wrap',
        'Binding js_stream',
        'Binding tls_wrap',
        'NativeModule _tls_legacy',
        'NativeModule punycode',
        'NativeModule dns',
        'NativeModule constants' ],
     versions: 
      { http_parser: '2.7.0',
        node: '6.11.5',
        v8: '5.1.281.108',
        uv: '1.11.0',
        zlib: '1.2.11',
        ares: '1.10.1-DEV',
        icu: '58.2',
        modules: '48',
        openssl: '1.0.2l' },
     arch: 'x64',
     platform: 'linux',
     release: 
      { name: 'node',
        lts: 'Boron',
        sourceUrl: 'https://nodejs.org/download/release/v6.11.5/node-v6.11.5.tar.gz',
        headersUrl: 'https://nodejs.org/download/release/v6.11.5/node-v6.11.5-headers.tar.gz' },
     argv: [ '/nodejs/bin/node', '/var/tmp/worker/worker.js' ],
     execArgv: [ '--max-old-space-size=256' ],
     env: ...

Firestore のクエリで「存在しないこと」をチェックしたい場合、事前に null を入れておく

Firestore でこんなモデルを作ったとする。

admin.firestore().collection('user').add({
  startDate: new Date(),
  name: 'hoge'
})

このモデルに対し、「 endDate に値が入っていないもの」をクエリで取得したいとする。
パッと思いつくクエリはこう。

admin.firestore().collection('user')
  .where('endDate', '==', undefined)
  .get()

しかし、 undefined はクエリとして指定することができない。困った。

事前に null を入れておく

admin.firestore().collection('user').add({
  startDate: new Date(),
  name: 'hoge',
  endDate: null
})
admin.firestore().collection('user')
  .where('endDate', '==', null)
  .get()

is null を確認したい場合は、事前に null を入れてデータを作っておく。
後になって is null でクエリを書こうとしてもダメなので、事前に null を入れましょう。

false ならいけそうな気がしたがダメだった

データがあれば true, なければ false というような、js のようにふわっとかけないかなと思ったけどダメだった。
true / false はちゃんと真偽値しか検索に引っかからないようだ。