DRYな備忘録

Don't Repeat Yourself.

【iOS】ボタン長押しの実装【Xcode10.3】【Swift5】

Xcodeは変化が早いので記事の日付に注意してください

ゴール

  • 長押ししてアラートとか出るボタンを設置する

tl;dr

override func viewDidLoad() {
    super.viewDidLoad()
    let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(onLongPress))
    self.button.addGestureRecognizer(recognizer)
}

@objc func onLongPress() {
    let alert = UIAlertController(title: "Long Press", message: "親父にも長押しされたことないのに!", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    self.present(alert, animated: true, completion: nil)
}

以下読まなくていいです。

1. ボタンの設置・追加

最近のXcodeはオブジェクトの追加をするペインが無い。Cmd + Shif + Lで、かつてオブジェクト選択のペインだったものがダイアログとして出てくる。

f:id:otiai10:20190808105440p:plain
Cmd + Shift + L

ここで、ButtonをStoryboardにドラッグアンドドロップして、適当にテキスト変えて、終了。

f:id:otiai10:20190808105756p:plain

2. 通常のTouchイベントの登録

Storyboardを表示した状態で、右上の「◯◯」を押すと、Storyboardに紐付いているViewControllerが反面に開かれる。

f:id:otiai10:20190808110042p:plain

で、ButtonとViewControllerを紐付ける。

f:id:otiai10:20190808110925p:plain

ちょっとAlert出すコード書く。

@IBAction func onTouchDownButton(_ sender: Any) {
    let alert = UIAlertController(title: "Touch Down", message: "押したねっ", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "close", style: .default, handler: nil))
    self.present(alert, animated: true, completion: nil)
}

こうなる

f:id:otiai10:20190808112127g:plain

3. 長押しのRecognizerを設置

とりあえずさっき作って紐付けたTouch Downイベントは消して、以下のようなコードを書く。

このとき、self.buttonは、上記のような要領で、Ctrl押しながらUIパーツをViewControllerにひっぱってきて置いたもの。

override func viewDidLoad() {
    super.viewDidLoad()
    let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(onLongPress))
    self.button.addGestureRecognizer(recognizer)
}

// func onLongPress(_ sender: UILongPressGestureRecognizer) {
@objc func onLongPress() {
    let alert = UIAlertController(title: "Long Press", message: "親父にも長押しされたことないのに!", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    self.present(alert, animated: true, completion: nil)
}

こうなる

f:id:otiai10:20190808114936g:plain

雑感

  • Xcodeは補完がきもちいいですね
  • Swiftはguardが好きです
  • iOS開発たのしい

DRYな備忘録として

任意のURL(に限らず文字列)のQRコードをコマンドラインで生成する

という文言でググればいくらでも出てきますけど。

tl;dr

% pip install segno
% segno "https://before11.hatenablog.com/" --output myblog.png --scale 8

そしたらこういう画像が myblog.png として得られる

f:id:otiai10:20190625135536p:plain

おしまい

ログ

% pip install segno
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won\'t be maintained after that date. A future version of pip will drop support for Python 2.7.
You are using pip version 19.0.3, however version 19.1.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

# アアアッ、すみません

% which pip
/Users/otiai10/.venv/default/bin/pip

# venv使っててこれかよ

% cd ~
% rm -rf .venv
% mkdir ~/.venv
% cd .venv
% virtualenv create default -p /usr/local/bin/python3
% source ./default/bin/activate
% which pip
/Users/otiai10/.venv/default/bin/pip
% python -V
Python 3.6.8

# オッケ

% pip install segno
% segno --help
% segno "https://before11.hatenablog.com/" -o myblog.png
% gat myblog.png

# 小せえ!

% segno "https://before11.hatenablog.com/" -o myblog.png --scale 8
% gat myblog.png

# いい感じ

ついでなので上記のログに出てきた gat を紹介させてください。

github.com

DRYな備忘録として

Node.jsのchild_process間で、標準出力のpipe

これをやりたい↓

cat ./testdata.txt | grep otiai

JavaScriptで、

const cat = spawn('cat', ['./testdata.txt']);
const grep = spawn('grep', ['otiai']);
cat.stdout.pipe(grep.stdin);
grep.stdout.on('data', (chunk) => {
    console.log('[grep]');
    console.log(chunk.toString());
});

f:id:otiai10:20190530121345p:plain

おわりです。うごくやつです。

github.com

DRYな備忘録として

Node.jsデザインパターン 第2版

Node.jsデザインパターン 第2版

Node.js超入門[第2版]

Node.js超入門[第2版]

はじめてUNIXで仕事をする人が読む本

はじめてUNIXで仕事をする人が読む本

ElectronデスクトップアプリによるGoogleのOAuth2クライアント実装

目的

  • 僕が、ElectronデスクトップアプリによるOAuthプロセスを知る

うるせえ動くもん見せろ

はい。

github.com

ゴール

  • 手元のElectronアプリで、ログインユーザ(この場合、僕自身)のAPIトークンで、GoogleのなんらかのAPIが叩ける
    • ← 登録したアプリ下でわざわざ作成したAPIトークンではないのがミソ

本稿でやらないこと

  • OAuthとはなにかのもっとわかりやすいやつ(こんど別エントリで書きたい)
  • 得られたAPI Tokenの永続保存(electron-json-storageでも使う)
  • Expire時のRefreshの実装 (割愛)

参考資料

目次

  1. デスクトップアプリの設計概要
  2. Google Cloud Console での準備
  3. 最低限のElectronアプリと動線を作成
  4. OAuthクライアントの実装

1. デスクトップアプリの設計概要

こういうものをつくっていきます

f:id:otiai10:20190502154537j:plain
アプリのペーパープロトタイプ(検索の索をまちがえている)

  1. メインのウィンドウでトップ画面が開く
  2. Googleでログイン」的なボタンを押すと別ウィンドウが開く
  3. そこでGoogleにログインして、権限も許可する
  4. 成功するとそのウィンドウは消えて、メインのウィンドウで認証情報が得られる
  5. なんらかのAPI(たとえばYouTube検索API)が使える

みたいな感じで。

2. Google Cloud Console での準備

この手順はもちろん「OAuthプロバイダ」によって操作が異なります

が、

OAuthのプロトコルに則る以上、以下のものを得る・設定する手順には変わりありません

  1. クライアントID(ひつようですね)
  2. クライアントシークレット(ひつようですね)
  3. リダイレクトURI(指定可能だが、登録するひつようがある)

今回は、GoogleをOAuthプロバイダとしてこれをやっていきます。

OAuthプロトコルとしては些末なことなのでここに折りたたんでおきます😉

ⅰ) プロジェクトを作成 - Google Cloud Platform ここに行く

f:id:otiai10:20190502160416p:plain
Google Console 上で新しいプロジェクトを作成(名前なんでもいい)
ⅱ) 使う予定のAPIを有効化 f:id:otiai10:20190502160658p:plain:w320 f:id:otiai10:20190502160725p:plain:w320 f:id:otiai10:20190502160935p:plain:w320
ⅲ) (番外)アプリのAPIトークンで最終目的地を確認
これは番外なんですが、とりあえずYouTubeAPIが叩けることを確認するために、アプリのAPIトークンを作成して、APIが叩けているということをクリアにしておきたい。個人的な性癖です。でもだいじ。こういうのだいじ。 f:id:otiai10:20190502161204p:plain:w320 認証情報、から f:id:otiai10:20190502161537p:plain APIキーのほうを作成して、 f:id:otiai10:20190502161723p:plain で、`curl`で試してみる。 - Search: list  |  YouTube Data API  |  Google Developers
f:id:otiai10:20190502162551p:plain
curlで叩けている(jqとかはノリです)
User Token の場合は、`Authorization Header`に入れる必要があるが、とりあえずこのAPIを叩けることは確認できた。
ⅳ) OAuthクライアントを登録
f:id:otiai10:20190502164911p:plain f:id:otiai10:20190502165218p:plain f:id:otiai10:20190502165414p:plain このように得られました。

はい!と、いうことでね!上記3点を得ることができました!

3. 最低限のElectronアプリと動線を作成

ファイルぜんぶここで晒すのめんどくさいので、そのdiffをおいておきます。

Implement minimum transitions btw windows · otiai10/electron-playground@7085aed · GitHub

ポイントとしては、Electronにはmainプロセスとrendererプロセスがあり、rendererプロセスを起因としてGoogleの認証画面を開かせる流れなので、

  • トップ画面のrendererプロセスは「Googleでログイン」ボタンをきっかけとして、mainプロセスへipcRenderer.send('auth-start')などのメッセージを送る
  • mainプロセスは、ipcMain.on('auth-start', () => ...)などでその認証プロセスをスタートさせる
    • mainプロセスは、あたらしい画面(BrowserWindow)をつくり、Googleのページへ誘導する
    • この画面におけるGoogleとのやりとりが成功したトリガーを検知し、成果物を取得し、この画面を閉じる
    • mainプロセスは、この成果物をトップ画面へ win.send('auth-success', tokens)などで通知する
  • トップ画面は ipcRenderer.on('auth-success, tokens => ...)などでこのメッセージを受け取る
    • トップ画面は、トークンが得られたら、「Googleでログイン」ボタンを非表示にし、かわりに主なコンテンツを表示する

4. OAuthクライアントの実装

ファイルぜんぶここで晒すのめんどくさいので、そのdiffをおいておきます。

Implement OAuth handshake inside the app · otiai10/electron-playground@5f9b18e · GitHub

ポイントをいくつか列挙すると

  • これべんり GitHub - googleapis/google-auth-library-nodejs: 🔑 Google Auth Library for Node.js
  • プロバイダ(Google)画面における認証と権限許可が成功したことをデスクトップアプリでどう検知するか
    • ウェブサービスであれば、Callback URLないしRedirect URIなどと呼ばれるURLに、ブラウザがリダイレクトされる
    • ウェブサービスではない。BrowserWindowfile://から始まるURLにリダイレクトさせることを考えるが、これはプロバイダ側から許可されないことが多い
    • 苦肉の策ではあるが、BrowserWindowpage-title-updatedイベントを使い、Google上の認証認可が成功したことを検知する
  • 検知したら、codeを取得し、これをaccess tokenと交換してもらう
    • これはgoogle-auth-libraryがやってくれる。べんり。
  • あとはこれをrendererプロセスへ通知し、rendererプロセスにおいてYouTube Data API / Searchを叩けばよい

完成したものです

雑感

  • page-title-updatedとかダサすぎか
  • <template>タグべんり
  • fetchべんり
  • async/awaitべんり
  • OAuthについて学んだことをまとめようかと思ったけどちょっとだるいなこれは
  • なんか最近「おちついてやる」という能力が板についてきて、よい
  • GW長い
  • 令和!

DRYな備忘録として

Elasticsearch: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

tl;dr

Elasticsearchが動くコンテナの中に以下の環境変数をねじこめばよい。

参考: Running Elasticsearch 5 - Build Environment - CircleCI Discuss

# これ
# transport.host=localhost
#
# と、これ
# bootstrap.system_call_filter=false
# 
# docker run で渡す場合は、

docker run -d \
    -e transport.host=localhost \
    -e bootstrap.system_call_filter=false \
    -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:6.6.1

# となる

問題

  • CircleCI上でElasticsearchのDockerイメージがうごいてくれない
ERROR: [1] bootstrap checks failed
[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

原因

vm.max_map_countとは、

1プロセスあたり所有できるメモリマップの数で、/etc/sysctl.confで指定できる。

解決(ホストをいじれる場合)

ので、ホストの/etc/sysctl.confをいじって許容を広げればよい。

sudo sysctl -w vm.max_map_count=262144

しかしながら、CircleCIインスタンス上ではこれはpermission deniedとなる。さもありなん。

解決(Circle CI)

したがって、なんらかの方法でコンテナ側の要求を下げる必要がある。

docker run -d \
    -e transport.host=localhost \
    -e bootstrap.system_call_filter=false \
    -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:6.6.1

あるいは、CircleCI v2 であれば、dockerディレクティブの中で

docker:
  - image: docker.elastic.co/elasticsearch/elasticsearch:6.6.1
     environment:
         transport.host: localhost
         bootstrap.system_call_filter: false

などとすればよい。

DRYな備忘録として

Pythonでdictionaryの各要素に処理を加えた別のdictionaryをつくる

TL;DR

>>> { k:list(map(lambda s: int(s)**2, v.split('-'))) for (k,v) in src.items()}
{'foo': [1, 4, 9], 'bar': [16, 25, 36]}

f:id:otiai10:20190412125100p:plain

やりたいこと

入力

{
  'foo': '1-2-3',
  'bar': '4-5-6',
}

出力

{
  'foo': [1, 4, 9],
  'bar': [16, 25, 36],
}

みたいなこと。

解決

src = {'foo': '1-2-3', 'bar': '4-5-6'}

dest = {
    k: list(map(
            lambda s: int(s)**2,
            v.split('-'),
    )) for (k, v) in src.items()
}

# {'foo': [1, 4, 9], 'bar': [16, 25, 36]}

知見

  1. dictは、itemsメソッドで、(key, value)のタプルのリストが得られる
    1. 正確には得られるのはdict_itemsであり、appendなどは無いが、リストとして扱う上では困らない
  2. タプルのリストはforでそれぞれ多値を拾うことができる
    1. for (name, age) in [('otiai10', 100), ('otiai20', 200)] のように
  3. map関数でリストのそれぞれの要素に対して処理をapplyできる
    1. ただし、この返り値はmap objectであり、listではないので、list()でリストにしてやる必要がある
  4. 文字列分割はstr.split(delim)
  5. 無名関数をlambdaで作ることができる
    1. ただし、複数行のlambdaをつくることはできない?っぽいので、見通しも悪くなるので関数は別定義したほうがよい
  6. dictの初期化において、keyにも変数を使える
    1. 下記参照
>>> key = 'name'
>>> val = 'otiai10'
>>> {key:val}
{'name': 'otiai10'}

WETな備忘録として

Go1.11でAppEngineをはじめる

tl;dr

これの通りです

うごくやつです

作業環境

% gcloud -v
Google Cloud SDK 235.0.0
app-engine-go
app-engine-python 1.9.83
bq 2.0.41
cloud-datastore-emulator 2.1.0
core 2019.02.15
gsutil 4.36
% go version
go version go1.11 darwin/amd64

Overview

  1. プロジェクトの作成とRegionの設定
  2. ローカルで開発
  3. デプロイ
  4. 確認

プロジェクトの作成とRegionの設定

プロジェクトIDで、ローカルのSDKを設定しておく

% gcloud config set project otiai10-sandbox

ローカルで開発

% mkdir -p $GOPATH/src/github.com/otiai10/gae-go-sandbox
% cd $GOPATH/src/github.com/otiai10/gae-go-sandbox
% vi main.go

main.go

package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/otiai10/marmoset"
)

func main() {
    router := marmoset.NewRouter()
    router.GET("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hi"))
    })
    router.GET("/hello", func(w http.ResponseWriter, r *http.Request) {
        message := fmt.Sprintf("Hello, %s!", r.FormValue("name"))
        w.Write([]byte(message))
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    fmt.Println("Listening port", port)

    http.ListenAndServe(fmt.Sprintf(":%s", port), router)
}

ローカルでの挙動確認

% go run main.go
Listening port 8080

f:id:otiai10:20190226123753p:plain
http://localhost:8080/hello?name=otiai10

いいかんじ。

デプロイ

app.yamlつくる

% vi app.yaml
runtime: go111

おしまい。

% gcloud app deploy
# もしかすると
# gcloud auth login
# が必要かも

とすると、

Updating service [default]...failed.
ERROR: (gcloud.app.deploy) Error Response: [9] Cloud build 01b7bef8-3e62-4956-9bc6-8e3a4b4cb1fe status: FAILURE.
Build error details: go: finding github.com/otiai10/marmoset v0.4.0
go: finding golang.org/x/net v0.0.0-20190225153610-fe579d43d832
go: downloading github.com/otiai10/marmoset v0.4.0
.
Check the build log for errors: https://console.cloud.google.com/gcr/builds/01b7bef8-3e62-4956-9bc6-8e3a4b4cb1fe?project=199462931903

言われたとおりエラーログ見に行くと、

f:id:otiai10:20190226134218p:plain
上記のエラーログ

GCR(コンテナレジストリ)へのイメージのアップロードで403を食らっているような雰囲気がある。ためしに、コンテナレジストリのコンソールに行くと。

f:id:otiai10:20190226134342p:plain

ふむ。Quick Startのドキュメントをよく見ると

Before you begin

Use the GCP Console to create a Google Cloud Platform project, choose a region where you want your application's resources to be located, and enable billing:

https://cloud.google.com/appengine/docs/standard/go111/quickstart#costs

とある。たぶん、AppEngineが使うストレージとは別に、GCRが使う領域を追加するのに課金の有効化(金がかかるとは言っていない)が必要だと思われる。ので、ここ https://console.cloud.google.com/billingから、このプロジェクトの課金を有効化して、再度GCRを見に行くと、

f:id:otiai10:20190226134743p:plain

見えるようになった。気を取り直して、

% gcloud app deploy
# 中略

Deployed service [default] to [https://otiai10-sandbox.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

いいかんじにコマンドは成功した。

確認

f:id:otiai10:20190226135054p:plain

いいじゃん

雑感

DRYな備忘録として