子育てしながらエンジニアしたい

現在 7 歳 女の子の子育て中エンジニアによる、技術系 + 日常系ブログ。

Web ページから iOS アプリを起動したい - Custom URL scheme

やりたいこと

Web ブラウジングをしていて、ボタンやリンクを押すとアプリが起動する機能があります。
それを iOS で実現したい!ということで、その方法を調べました。
なお、ただ起動するだけでなく、起動時の URL も取得したかったので、その方法も含みます。

実現する方法

調べた感じでは、2 つの方法があるようです。

後者の Universal Links は iOS 9 で導入された機能のようで、UX 的にはこちらのほうが良さそうです。
ただ試すのにいろいろ制約 (SSL 接続必須とか、独自ドメインとか) があったので、今回は Custom URL scheme をやってみることにしました。
事情により Swift/Objective-C の両方でやる必要があったので、両方のやり方を紹介します。

Custom URL scheme

Custom URL scheme とは

ディープリンクとは、アプリの特定の画面に遷移(遷移先のアプリ側で実装が必要です)させることのできるリンクのことです。 iOSでは、 cm-app:// のような、アプリ固有の Custom URL Scheme を実装することで、これを実現できます。

ディープリンク(Custom URL Scheme)でアプリを起動する より

ウェブのリンクは通常 http:// とか https:// とかで始まります。
これをアプリ固有の myapp:// とか hogehoge:// とか (これを Custom URL scheme と呼ぶらしい) にすることで、この scheme を持ったアプリがインストールされていれば起動されます。

注意点としては、以下のようなことがあげられます。

  • 同じ scheme を持っているアプリがあった場合、どちらが起動するかわからない
  • scheme がなかったらエラーになる

後者に関してはいろいろ対策を考えられている方もいらっしゃるようです。

今回はここまで踏み込まず、アプリの起動と、そのときの URL の取得までを行います。

アプリに Custom URL scheme を設定する

これは非常に簡単です。
プロジェクトの Info -> URL Types を開き、+ ボタンを押します。
すると下記のようになるので、ここの URL Schemes に使いたい scheme 名を入力します。

f:id:edosha:20170502162802p:plain

ここでは sctest としました。

リンクを作成する

これもとっても簡単です。
html に以下のように記述すれば OK です。

<a href="sctest://">Launch!!</a>

iOS 側でこのリンクをクリックすると、以下のような画面になります。

f:id:edosha:20170502163340p:plain

これだけで、アプリの起動までは完了です。

起動時の URL を取得する

ただ今回はこれだけではなく、起動時の URL をアプリ側で取得したかったのです。
それによってアプリの動作を変えるってことも簡単にできるので。
たとえば html ファイルのリンクを以下のようにして、ユーザ名やパスワード等をアプリに渡すことを考えます。

<a href="sctest://user:password@192.168.100.100:5555/#fragment">Launch!!</a>

Swift/Objective-C それぞれやり方が微妙に違ったので、どちらも紹介します。

Swift

AppDelegate.swift ファイル内に AppDelegate クラスがあります。
そのクラスのどこかに以下のような記述を入れます。

func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    print("scheme: \(url.scheme!)")
    print("user: \(url.user!)")
    print("password: \(url.password!)")
    print("host: \(url.host!)")
    print("port: \(url.port!)")
    print("fragment: \(url.fragment!)")
        
    return true
}

こうすると Xcode の出力部に以下のように出力されます。

f:id:edosha:20170502165800p:plain

なお url から取れる情報は、Apple の開発者ページが参考になります。
NSURL

実装は下記のページを参考にしました。
とても詳しく書いてあるのでおすすめです。
ディープリンク(Custom URL Scheme)でアプリを起動する

Objective-C

AppDelegate.m ファイルのどこかに以下のような記述を入れます。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
    NSLog(@"scheme: %@", url.scheme);
    NSLog(@"user: %@", url.user);
    NSLog(@"password: %@", url.password);
    NSLog(@"host: %@", url.host);
    NSLog(@"port: %@", url.port);
    NSLog(@"fragment: %@", url.fragment);
    
    return YES;
}

こうすると、ターミナルのログに以下のように出力されます。

f:id:edosha:20170502170242p:plain

Swift と同じですが、url から取れる情報は、Apple の開発者ページが参考になります。
NSURL

ログの確認は以下のページを参考にしました。
iOS Simulatorのシステムログを確認する

さいごに

思ったよりも簡単にできてしまいました。
いや〜、iOS アプリすごいですね...

Xcode で Emacs キーバインドを使いたい - Hammerspoon を使う

Hello, Xcode

仕事で iOS アプリを作ることになりました。
当然ながら開発環境は Xcode になります。
今までで最も長い組み込み開発では、Emacs を使ってきました。
Python 開発は、Spyder を使っているものの、keyhac というソフトウェアを使うことで Emacsキーバインドにしていました。

というわけで XcodeEmacs キーバインドで使いたい!というのが今回の趣旨です。

キーマップの変更手段

El Capitan 以前は Karabiner というソフトを使って、キーマップを入れ替えるのが定番だったようです。
しかし macOS Sierra 以降では使えないとのこと...!!
後継のKarabiner Elements というソフトウェアもリリースされていますが、残念ながら Modifier (Ctrl とか Shift とか) を使ったキーリマップができないようです。

Windows で愛用している keyhac を使ってみましたが、いろいろうまくいかず断念。。

そして候補にあがってきたのが Hammerspoon です。

Hammerspoon

Hammerspoon とは

This is a tool for powerful automation of OS X. At its core, Hammerspoon is just a bridge between the operating system and a Lua scripting engine. What gives Hammerspoon its power is a set of extensions that expose specific pieces of system functionality, to the user.

Hammerspoon

基本的には Mac の自動化ツールだそうです。
Lua スクリプトで動作を記述し、それを OS に渡すというブリッジの役目をします。
API Document を見るとかなりいろいろできるのですが、今回はキーリマップのためだけに使います。

Hammerspoon のインストー

公式ページ から zip ファイルをダウンロードします。
Hammerspoon.app をアプリケーションディレクトリにコピーすれば OK です。

なお初回起動のときに、Hammerspoon への制御許可を求められます。
システム環境設定 -> セキュリティとプライバシー -> アクセシビリティで Hammerspoon にチェックを入れれば OK です。

f:id:edosha:20170501150426p:plain

キーバインドの実現

こちら の記事をかなり参考にさせていただきました。
というかコア部分はほとんど上記のスクリプトで、一部追加しただけです。

キーバインド

このスクリプトでは Xcode を開いているときに以下のキーバインドを実現します。

  • Ctrl + Space
    • Mark set
  • Ctrl + W
    • Cut
  • Alt + W
    • Copy
  • Ctrl + Y
    • Paste
  • Ctrl + X, Ctrl + F
    • Open quickly
  • Ctrl + X, Ctrl + S
    • Save file
  • Ctrl + X, N
    • 次のタブへ
  • Ctrl + X, P
    • 前のタブへ
  • Ctrl + X, K
    • タブを閉じる
  • Alt + Shift + ,
    • ファイルの先頭へ移動
  • Alt + Shift + .
    • ファイルの末尾へ移動
  • Ctrl + /
    • Undo
  • Ctrl + '
  • Ctrl + S
    • Search
  • Ctrl + K
    • Kill line
  • Alt + V
    • Page up
スクリプト

実際のスクリプトこれ です。
これを User/.hammerspoon/init.lua にコピーしてください。

f:id:edosha:20170706110234p:plain:w400

(ちなみにこの隠しフォルダは、Shift + Command + . (ドット) で見えます)

ちなみに私は Lua スクリプトの文法を全く知りませんが、スクリプトの改変はなんとかできました。
直感的だと思うので、見ればわかるかなと思います。


おわりに

Xcode での Emacs キーバインドができて、かなり快適になりました。
Hammerspoon 自体は応用範囲が広そうなので、もっといろいろできそうです。

Kaggle 初挑戦: タイタニック号の生存予測その 3 - ニューラルネットワークによる予測 -

Kaggle: タイタニック号生存予測シリーズ

ニューラルネットワークとは

ニューラルネットワークとは、脳神経系をモデルにした情報処理システムのことです。学習能力を持ち、必要とされる機能を、提示されるサンプルに基づき自動形成することができます。文字認識や、音声認識など、コンピュータが苦手とされている処理に対して有効です。

村上研究室 ニューラルネットワーク より

いわゆる人工知能の一つのアプローチです。
実は大学時代 (もう10年前か...)、人工知能の一分野である、強化学習というのを研究していました。
当時は実装が大変で、かなり挫折してました...
それが今は Python という言語に強力なライブラリがあるおかげで、とても簡単に実装できるようになっていました。
Python で作るニューラルネットワークの本「ゼロから作る Deep Learning」を買ったので、
せっかくなのでタイタニック号生存予測をニューラルネットワークでやることにしました。

ゼロから作る Deep Learning

Amazon でも大評判のこの本、読んでみました。
たしかに説明もわかりやすく、かつ Python の実装もちゃんと書かれているので、とても理解しやすいです。
また、素晴らしいのはコードが Github で公開されていることです。

Gitlab - ゼロから作るdeep learning

MITライセンスで自由に使えるとのことで、ありがたいです。
今回はこのソースからニューラルネットワークを使わせてもらうことにしました。

データの加工

前回の記事のように、まずはデータを加工します。

edosha.hatenablog.jp

前回の記事では試行錯誤しながらデータを加工しているので、これをまとめました。

# coding: utf-8
import pandas as pd
"""Titanic データの前処理をする

同じディレクトリに train.csv, test.csv を置いて使う。

Available functions:
    wrangle_data - データを加工する。返り値は train_df, test_df
                   (pandas のデータフレーム)
"""

def wrangle_data ():
    # Preprocess training data
    train_df = pd.read_csv ('train.csv')
    test_df = pd.read_csv ('test.csv')
    
    train_df['Title'] = train_df.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
    test_df['Title'] = test_df.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
    
    # train_df
    title_list = train_df['Title'].sort_values (inplace=False).unique ()
    pclass_list = train_df['Pclass'].sort_values (inplace=False).unique ()
    for title in title_list:
        for pclass in pclass_list:
            guess_df = train_df[(train_df['Title'] == title) & (train_df['Pclass'] == pclass)]['Age'].dropna ()
            train_df.loc[(train_df['Age'].isnull()) & (train_df['Title'] == title) & (train_df['Pclass'] == pclass), 'Age'] = guess_df.median ()
            
    # test_df
    title_list_t = test_df['Title'].sort_values (inplace=False).unique ()
    pclass_list_t = test_df['Pclass'].sort_values (inplace=False).unique ()
    for title in title_list_t:
        for pclass in pclass_list_t:
            guess_df = test_df[(test_df['Title'] == title) & (test_df['Pclass'] == pclass)]['Age'].dropna ()
            test_df.loc[(test_df['Age'].isnull()) & (test_df['Title'] == title) & (test_df['Pclass'] == pclass), 'Age'] = guess_df.median ()
            
    # test_df 'Title' == 'Ms' の補完
    guess_df = train_df[train_df['Title'] == 'Ms']['Age'].dropna ()
    test_df.loc[test_df['Age'].isnull(), 'Age'] = guess_df.median ()
       
    # train_df 'Embarked' の補完
    freq_port = train_df.Embarked.dropna().mode()[0]
    train_df['Embarked'] = train_df['Embarked'].fillna (freq_port)
    
    # test_df 'Fare' の補完
    guess_df = test_df[test_df['Pclass'] == 3].dropna()
    test_df.loc[test_df['Fare'].isnull(), 'Fare'] = guess_df['Fare'].median()
    
    df_list = [train_df, test_df]
    for df in df_list:
        df.loc[df['Age'] <= 8, 'Age'] = 0
        df.loc[(df['Age'] > 8) & (df['Age'] <= 16), 'Age'] = 1
        df.loc[(df['Age'] > 16) & (df['Age'] <= 32), 'Age'] = 2
        df.loc[(df['Age'] > 32) & (df['Age'] <= 48), 'Age'] = 3
        df.loc[(df['Age'] > 48) & (df['Age'] <= 64), 'Age'] = 4
        df.loc[(df['Age'] > 64) & (df['Age'] <= 80), 'Age'] = 5
        df['Fellow'] = df['SibSp'] + df['Parch']
        df['IsAlone'] = 0
        df.loc[df['Fellow'] == 0, 'IsAlone'] = 1
        df['Sex'] = df['Sex'].map ({'female': 0, 'male': 1}).astype (int)
        df['Embarked'] = df['Embarked'].map ({'C': 0, 'Q': 1, 'S': 2}).astype (int)
        df.loc[df['Fare'] <= 7.91, 'Fare'] = 0
        df.loc[(df['Fare'] > 7.91) & (df['Fare'] <= 14.454), 'Fare'] = 1
        df.loc[(df['Fare'] > 14.454) & (df['Fare'] <= 31), 'Fare']   = 2
        df.loc[df['Fare'] > 31, 'Fare'] = 3
        df['Fare'] = df['Fare'].astype(int)
        
    train_df = train_df.drop (['PassengerId', 'Ticket', 'Cabin', 'Name', 'Title', 'SibSp', 'Parch', 'Fellow'], axis=1)
    test_df = test_df.drop (['Ticket', 'Cabin', 'Name', 'Title', 'SibSp', 'Parch', 'Fellow'], axis=1)
    
    return train_df, test_df

if __name__ == '__main__':
    
    train_df, test_df = wrangle_data ()
    
    print (train_df.describe())
    print (test_df.describe())

ニューラルネットワークによる予測

いよいよ予測をします!
まずは使用するニューラルネットワークのライブラリを Github からダウンロードしてください。
common ディレクトリだけ使うので、それだけダウンロードしても大丈夫です。
そして、以下のようにスクリプトを配置してください。

dir
│   data_wrangle.py
│   test.csv
│   train.csv
│   train_neuralnet.py
│
├───common
│   │   functions.py
│   │   gradient.py
│   │   layers.py
│   │   multi_layer_net.py
│   │   multi_layer_net_extend.py
│   │   optimizer.py
│   │   trainer.py
│   │   util.py

train_neuralnet.py は以下のソースコードになってます。
自分の Github からもダウンロード可能です。

# coding: utf-8

"""titanic の生存予測を、ニューラルネットワークで行う

ニューラルネットワークのサイズは MultiLayerNet の引数で決まる。
"""

import numpy as np
from common.multi_layer_net import MultiLayerNet
import pandas as pd
import matplotlib.pyplot as plt
from common.optimizer import *
import data_wrangle

def change_one_hot_label(X):
    """書籍を参考。正解データを one_hot_label 形式にする"""
    T = np.zeros((X.size, 2))
    for idx, row in enumerate(T):
        row[X[idx]] = 1
        
    return T

# 前処理されたデータの取得
train_df, test_df = data_wrangle.wrangle_data ()

# データを学習物と答えに分ける
x_train = train_df.drop (['Survived'], axis=1).values
t_train = train_df['Survived'].values
t_train = change_one_hot_label (t_train)

# ニューラルネットワークの生成。
# 隠れ層は適当に 100, 100, 100 とした。
network = MultiLayerNet(input_size=6, hidden_size_list=[100, 100, 100], output_size=2, weight_decay_lambda=0.01)
# Optimizer の選択。AdaGrad() は common.optimizer 以下にある。
optimizer = AdaGrad()

iters_num = 1000
train_size = x_train.shape[0]
batch_size = 99

train_loss_list = []
train_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

# 学習
for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 勾配
    #grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)
    
    # 更新
    optimizer.update(network.params, grad)
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        train_acc_list.append(train_acc)

# train データでどのように学習されたか図示
plt.plot (train_acc_list)

# テストデータの PassengerId を退避させる
test_df_id = test_df['PassengerId']
test_df = test_df.drop ('PassengerId', axis=1)
x_test = test_df.values

# テストデータの生存予測
output = network.predict (x_test)
output = np.argmax (output, axis=1)

# 生存予測を Kaggle に投稿する形に修正する
output_df = pd.DataFrame (output, columns=['Survived'])
output_df['PassengerId'] = test_df_id
output_df = output_df.ix[:, ['PassengerId', 'Survived']]
output_df.to_csv('test_ans.csv', index=False, encoding='utf-8')

さあ、ついにこれで test_ans.csv を得ました!
これを Kaggle に投稿してみましょう!

結果は....


f:id:edosha:20170429215836p:plain

0.7799!!

いまいち!!(笑)


まだまだ全然修行がたりないようです。
でも、こうやってデータ処理を競うっておもしろいですね。
これからも頑張ります。