The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

gitでHEADの代わりに@を使う

いままで@派をあまり見かけたことがないので、もしかして認知度が低いのかな・・と思い今更感がすごいことを書いておきます。

ドキュメントに書いてあるように

HEADって打たなくても@(アットマーク)で代用できます。

@ alone is a shortcut for HEAD.

Git - gitrevisions Documentation

HEADでもHを入力してタブ押せば補完されますけど@のほうが1タップ少なく済むので便利です。

ただ記事とかに書くには@だとググれないのでHEADを使うべきですね・・

SwiftのDictionaryのsubscriptとupdateValueの違い

SwiftでDictionaryの値を追加、変更するのに2通りのやりかたがあります。

ひとつはsubscriptdict["key"] = "value"

もうひとつはupdateValuedict.updateValue("value", forKey: "key")

です。

どちらを使ってもDictionaryの値を変更できますが、微妙に違いがあります。

1. 戻り値

updateValueの場合は戻り値で「変更前の値」を返します。 keyが存在しない場合はnilを返します。

var dict1 = ["a": "a"]

print(dict1.updateValue("A", forKey: "a"))
print(dict1.updateValue("b", forKey: "b"))

これを実行すると

"a"
nil

となります。

2. nilをセットしたときの挙動の違い

  • subscriptの場合

    • nilをセットするとそのkey-valueがremoveされます
  • updateValueの場合は

var dict1 = ["a": "a"]

dict1["a"] = nil
print(dict1)

これを実行すると

[:]

となり、key-valueが削除されます。

var dict1 = ["a": "a"]

dict1.updateValue(nil, forKey: "a") // error: nil is not compatible with expected argument type 'String'

これはコンパイルエラーになります

var dict1: [String: String?] = ["a": "a", "b": "b"]

dict1.updateValue(nil, forKey: "b")
print(dict1)

これを実行すると

["a": Optional("a"), "b": nil]

valuenilが設定されます。

Xcode10で地味にInterfaceBuilderで変わっているところ

UIの追加

Xcode9までは右下に出ていたのが、Xcode10だと右上の方にあるボタン(○の中に□のボタン)を押すと出てくるようになりました。

  • Xcode9 f:id:yanamura:20181010164750p:plain

  • Xcode10 f:id:yanamura:20181010164843p:plain

Image Literal

Xcode9だと画像名を入力すると補完されていましたが、Xcode10だとやり方が変わりました

  • Xcode10 imageと入力すると補完でimage literalがでてくるので選択 f:id:yanamura:20181011150348p:plain

表示されたアイコンをダブルクリック f:id:yanamura:20181011150409p:plain

または、

Shift + ⌘ + Mを押すとMediaの検索が出てくるのでそこからドラック&ドロップすることでも対応できます

git2.19以降のbranchのsort

gitの2.19がリリースされました

Highlights from Git 2.19 | The GitHub Blog

個人的な目玉機能はbranchのsortが楽になったという点です。

git branch --sort=-authordate

とすると更新日順にbranchが表示されます。

更に

git config --global branch.sort -authordate

というふうにconfigに設定しておくと毎回オプションをつけなくてもgit branchするだけで更新日順に表示されます。

authordate以外には以下のようなsort optionが用意されています。

オプション 説明
--sort=numparent shows merges by how awesome they are
--sort=refname sorts branches alphabetically by their name (this is the default, but may be useful to override in your configuration)
--sort=upstream sorts branches by the remote from which they originate

Horizontal StackViewで左寄せにする

横方向のStackViewを普通に使うと余白は詰められて横幅いっぱい使うようになってしまいますが、たまに左寄せにしたい(右に余白を開けたい)場合があります。

そんなときはこのように空のViewを一番うしろに突っ込んでやることで解決できます。

let spacerView = UIView()
spacerView.setContentHuggingPriority(.defaultHigh, for: .horizontal)

stackView.addArrangedSubview(spacerView())

ただしこのやり方だとspacingを設定している場合は、余分にViewを一つ入れているため一番右に余計に一つspaceが入ってしまいます。

RxSwiftのVariableとBehaviorRelayとBehaviorSubjectの違い

動機

RxSwiftのVariableがdeprecatedになったということで、その代わりとしてBehaviorRelayに置き換えようと思ったときに、BehaviorRelayに単純に置き換えてよいのか?BehaviorSubjectもあるけどこっちはどうなんだっけ?という視点で調べてみました。

結論

単純にVariableをBehaviorRelayに置き換えて良い。

ただVariableのcompletedをトリガーになにかしていたら変更する必要がある。

Variableのvalueをmutateするようなことをしてたらちょっと影響ある。

詳細

Variable, BehaviorRelayのソースを見て違いを見ていきます。 (RxSwift 4.1.2時点)

Variable

VariableはRxSwift/Deprecated.swiftに定義されています。

VariableはBehaviorSubjectのwrapperで、コメントに書いてあるようにBehaviorSubjectとの違いはerrorでterminateしないことと、deallocateされたときにcompleteする点にあります。

/// Unlike `BehaviorSubject` it can't terminate with error, and when variable is deallocated
/// it will complete its observable sequence (`asObservable`)

ソースを見てみます。

Variableは自身でvar _value: Eというプロパティで値を保持していて、それをSpinLock()を使って排他制御しています。(え、spinlockなの?と思ってソースをみたらRecursiveLockで安心しましたtypealias SpinLock = RecursiveLock)

public final class Variable<Element> {

    public typealias E = Element

    private let _subject: BehaviorSubject<Element>

    private var _lock = SpinLock()

    // state
    private var _value: E

    #if DEBUG
    fileprivate let _synchronizationTracker = SynchronizationTracker()
    #endif

    /// Gets or sets current value of variable.
    ///
    /// Whenever a new value is set, all the observers are notified of the change.
    ///
    /// Even if the newly set value is same as the old value, observers are still notified for change.
    public var value: E {
        get {
            _lock.lock(); defer { _lock.unlock() }
            return _value
        }
        set(newValue) {
            #if DEBUG
                _synchronizationTracker.register(synchronizationErrorMessage: .variable)
                defer { _synchronizationTracker.unregister() }
            #endif
            _lock.lock()
            _value = newValue
            _lock.unlock()

            _subject.on(.next(newValue))
        }
    }

    /// Initializes variable with initial value.
    ///
    /// - parameter value: Initial variable value.
    public init(_ value: Element) {
        #if DEBUG
            DeprecationWarner.warnIfNeeded(.variable)
        #endif

        _value = value
        _subject = BehaviorSubject(value: value)
    }

    /// - returns: Canonical interface for push style sequence
    public func asObservable() -> Observable<E> {
        return _subject
    }

    deinit {
        _subject.on(.completed)
    }
}

BehaviorRelay

VariableはRxCocoa/Traits/BehaviorRelay.swiftに定義されています。

BehaviorRelayはBehaviorSubjectのwrapperで、コメントにも書いてあるようにBehaviorSubjectとの違いはerrorやcompletedでterminateしないことです。

Variableとの違いは、

  • RxSwiftからRxCocoaに変わった
  • BehaviorRelayではcompletedがこない

ということになります。

/// Unlike `BehaviorSubject` it can't terminate with error or completed.

ソースを見てみます。

Variableのようにvalueを保持せず、acceptで値を変更するときはBehaviorSubjectのonNext()、値を取得するときにはcomputed propertyのvalueでBehaviorSubjectのvalue()を返します。

一点気をつける点は、Variableでは値の取得がvar _value: EだったのがBehaviorRelayだとcomputed propertyになっているので、型が値型の場合、前者はmutable、後者はcopyが返るのでimmutableになります。なので特にArrayとかの場合はmutable前提の使い方をしてると影響があります。

また、BehaviorRelayでは排他はかけてませんが、BehaviorSubject側でRecursiveLockを使って排他しているので大丈夫です。

public final class BehaviorRelay<Element>: ObservableType {
    public typealias E = Element

    private let _subject: BehaviorSubject<Element>

    /// Accepts `event` and emits it to subscribers
    public func accept(_ event: Element) {
        _subject.onNext(event)
    }

    /// Current value of behavior subject
    public var value: Element {
        // this try! is ok because subject can't error out or be disposed
        return try! _subject.value()
    }

    /// Initializes behavior relay with initial value.
    public init(value: Element) {
        _subject = BehaviorSubject(value: value)
    }

    /// Subscribes observer
    public func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == E {
        return _subject.subscribe(observer)
    }

    /// - returns: Canonical interface for push style sequence
    public func asObservable() -> Observable<Element> {
        return _subject.asObservable()
    }
}

まとめ

BehaviorSubject, Variable, BehaviorRelayの違い

terminate

  • BehaviorSubject
    • error, completedでterminateする
  • Variable
    • errorでterminateしない。(dealloc時にcompletedでterminateする)
  • BehaviorRelay
    • terminateしない

import

  • BehaviorSubject
    • RxSwift
  • Variable
    • RxSwift
  • BehaviorRelay
    • RxCocoa

valueの取得

  • BehaviorSubject
    • function
  • Variable
    • property
  • BehaviorRelay
    • computed property

linuxでSwiftのObjCBool.boolValueを呼ぶとerror

不具合

SwiftでObjCBoolのboolValueをこのように呼び出すと

var isDir: ObjCBool = false
let isExist = fm.fileExists(atPath: directory, isDirectory: &isDir)
XCTAssertTrue(isDir.boolValue)

osxだと大丈夫だけどlinux環境だとerrorになる・・

error: value of type 'ObjCBool' (aka 'Bool') has no member 'boolValue'
        XCTAssertTrue(isDir.boolValue)
                      ^~~~~ ~~~~~~~~~

原因

no memberなわけないだろと思ったら、ほんとにno memberだった・・・

ObjCBool: Add boolValue property by spevans · Pull Request #1223 · apple/swift-corelibs-foundation · GitHub

対策

4.1で治ってるので

#if !os(Linux) || swift(>=4.1)
  XCTAssertTrue(isDir.boolValue)
#else   
  XCTAssertTrue(isDir)
#endif

もしくは

たくさんの場所でboolValueを使っている場合はextensionで4.1未満の場合にboolValueを時前で実装してしまう

#if os(Linux) || !swift(>=4.1)
extension ObjCBool {    
    var boolValue: Bool { return Bool(self) }   
}   
#endif

electron-vueのproductionビルドで気をつけるところ

electron-vueでアプリを作ってみて、npm run devで開発環境で動かしたときは動いたけど、npm run buildで本番向けにビルドしたときは動かないみたいなことがちょいちょいあったので、それの対応方法です。

ファイルが見つからない

productionビルドの場合はwebpackでバンドルされてdist以下に配置されるので、devのときとディレクトリ構造が異なってきます。 なので__dirnameなどを使ってファイルのパスを指定していると駄目です。

下に記載のように__staticを用いる必要があります

Main Process · electron-vue

path.join(__static, 'image.png')

ファイルの保存ができない

nedbなどで相対パスでファイルに保存するとproductionビルドでは保存に失敗します。

これは、app.getPath()を使ってplatform固有の保存用のパスを取得する必要があります。

  const userData = app.getPath('userData');
  db = new Datastore({
    filename: `${userData}/db/log.db`,
  });

Macにインストールしようとするとセキュリティエラーがでる

作ったアプリをdmgファイルからインストールしようとすると、登録されていないアプリの警告ダイアログが表示されます。

これを出ないようにするには証明書をインストールしてビルドする必要があります。

Mac App Storeを通さずにアプリケーションを配布する手順

Apple Developer ProgramからDeveloper ID ApplicationとDeveloper ID Installerを作ってビルドするマシンに入れれば、あとはnpm run buildするとOK

Windows用のアプリが生成されない

デフォルトではMac版しかないので自分で追加する必要があります

package.jsonのscriptsにwindows向けのを追加します。

 "scripts": {
    "build:win": "node .electron-vue/build.js && electron-builder -w",

Macだとビルドするにはwineをインストールする必要がありますので、homebrewなどを使ってインストールしておきましょう。

Electronに見えないwebviewを埋め込んで処理させる

すでにWeb版が存在してるけどショボいのでネイティブアプリでいい感じにしたいときがあったりすることもあります。

これを、Electronで作ったネイティブアプリの画面上に見えないwebviewを置くことで実現します。

方法

見えないwebviewの埋め込み

Electronアプリにwebviewを埋め込むことは簡単でただタグを使うだけでできます。

次にこれを非表示にします。 非表示というと普通はdispaly: noneを使いますが、display: noneでは残念ながらwebviewの中身が実行されません。 そこでwidthとheightを0にしてやることで非表示にします。

webviewの通信

webviewはrendererプロセスとは別プロセスになるのでwebview内のjsとrendererプロセスのjsでやり取りするにはプロセス間通信をする必要があります。

rendererプロセス→webview

webviewエレメントのsendというメソッドがあるのでこれを叩くことで、webview側にイベントを送ることができます。

webview側でこのイベントを受け取るには、受け取る用のjsファイルを用意してpreloadを使って読み込ませます。 そしてその受け取る用のjsファイルの中でipcRenderer.onを使ってrendererプロセスからのイベントを受け取ります。

webview->rendererプロセス

webviewからrendererプロセスへのイベントの送信はipcRenderer.sendToHostを使います。

rendererプロセス側でこのイベントを受け取るにはipc-messageをaddEventListererで登録すればよいです。

具体例

electron-vueでやる場合の例です。

<template>
    <div>
        <webview
                id="webview"
                v-bind:preload="preloadJS()"
                src="https://facebook.com"
                style="width:0;height:0;"
        >
        </webview>
    </div>
</template>
<script>
import path from 'path';
import { remote } from 'electron';

let webview = null;

export default {
  data: function() {
    return {
      userId: '',
      password: '',
    }
  }
  mounted: function() {
    // webviewからのイベントの受信
    webview = document.getElementById('webview');
    webview.addEventListener('ipc-message', event => {
      if (event.channel === 'login') {
        switch (event.args[0]) {
          case "loading":
            break;
          case "loaded":
            break;
          case "success":
            console.log('ログイン成功');
            break;
          case "failure":
            console.log('ログイン失敗');
            break;
        }
      }
    });
  },
  methods: {
    preloadJS() {
      return `file:${path.join(__static, 'fb.js')}`;
    },
    login() {
      // webviewへのイベントの送信
      webview.send('login', this.userId, this.password);
    }
  },
// fb.js
const { ipcRenderer } = require('electron');

document.addEventListener('DOMContentLoaded', event => {
  // rendererプロセスからのイベントの受信
  ipcRenderer.on('login', (event, userId, password) => {
    // DOMをゴニョゴニョしてログインする
  });
});

document.addEventListener('readystatechange', event => {
    if (
      window.location.href ===
      'https://facebook.com/sign'
    ) {
      // rendererプロセスへのイベントの送信
      ipcRenderer.sendToHost('login', 'loaded');
    }
});

MacでIEとEdgeのテスト環境をつくる

MSからVirtualBoxのイメージが配布されているのでそれを使います。

1. VMのダウンロード

ここからVirtualBox用のVMをダウンロード

2. VMの起動

1でダウンロードしたzipを回答すると

  • MSEdge-Win10というフォルダ内にMSEdge-WIn10-disk001.vmdkとMSEdge-Win10.ovfというファイルが生成される

  • VirtualBoxを開いてファイル→仮想アプライアンスのインポートをクリック

  • インポートしたい仮想アプライアンスでMSEdge-Win10.ovfを選択

  • インポートをクリック

で完了です。

3. Macのローカルホストを参照する

EdgeやIEで10.0.2.2:80 とかを開くとlocalhostのページが見れるはずです。

4. hostsの設定

10.0.2.2とかをいれるのは面倒なのでhostsを使います。

まずpowershellを開いて

以下のコマンドを実行して、powershellを管理者権限で開きます

Start-Process powershell -Verb runAs

開いたpowershellで以下のコマンドを実行してhostsファイルを開きます

start notepad "C:\Windows\System32\drivers\etc\hosts"

hostsに設定を書いて終了です。

10.0.2.2 hoge.com

Prettierで.vueをフォーマット

Prettierのv1.10でVueのsingle file componentをformatできるようになりました。

それまではeslintででたエラーを人手でポチポチ直してたのでこの辺りが自動化できるのは最高です。

やることはすでにprettier導入済みであれば*.vueを対象のファイルにいれるだけです。

// package.json

"format": "./node_modules/.bin/prettier --write \"js/**/*.{js,vue}\"",

lint-stagedを使ってprecommit hookしてる場合も同様です

  "lint-staged": {
    "gitDir": "../",
    "*.{js,vue}": [
      "yarn format",
      "git add"
    ],

ReactでESCキーを押したときにモーダルを閉じる

このようにdivにonKeyDownをつけた場合、キーを押してもonKeyDownは呼ばれません。

<div
  onKeyDown={this.onKeyDown}
>

tabIndexをつけて要素にフォーカスを持つことができるようにすれば解決します。

<div
  onKeyDown={this.onKeyDown}
  tabIndex="0"
>

SwiftTweets 2018 Winterの資料のまとめ

SwiftTweets 2018 Winterの資料のまとめです。

https://swift-tweets.github.io/2018-winter

発表

どうやってSwiftのOSSをメンテナンスしていくか

https://twitter.com/ikesyo/status/954687472369201155

Swift Foundationにコントリビュートする

https://twitter.com/takasek/status/954696314171740160 https://qiita.com/takasek/items/01f7746bf444bd5c85c0

LT

iOSじゃないところでSwiftを使う

https://twitter.com/hiragram/status/954699064855076864

世界をプログラミングで満たしたい

https://twitter.com/koher/status/954702886935527424

2017振り返り

2017年の振り返りです。

仕事

これまで長らくiOSをやっていましたが、4月からフロントエンド担当になりYADOKARIというメディアを開発してます。

React.jsとかVue.jsを使ったりしてますが、ES6時代だとそんなにJavaScriptについての学習コストがかからないので iOSアプリのエンジニアからのコンバートは割と楽でした。

cssのほうがむしろハードルが高かった気がしてます。(精神的にも)

アウトプット

Swiftの排他制御についてあまり詳しい情報なかったので書きました。 明日から使えない!Swiftの排他制御 | Supership Tech Blog

プライベート

子供が一歳になりました。 今年は勉強会とかもいっさい参加せず、子育てに全てを費やしました。

勉強とかはいつでもできますけど、子育ては今しかできないので最優先です。

まとめ

今年はフロントエンドと子育ての一年だったなぁといった感じでした。

正直iOSはここ最近そんなに新しいことが少なくなってきてて学びはあんまりないなぁと思っていたのでちょうどよかったです。フロントエンドから学ぶことも多々ありますし。

来年も子供の圧倒的な成長を見習いつつがんばりたいです。