wootan's diary

iOSアプリ開発を中心としたエンジニアブログ

relux iOSアプリでディープリンク対応

relux iOSアプリでディープリンク対応を行いました。
ちょうどiOS9.3がリリースされたあとに着手しはじめ、
iOSの不具合でUniversalLinksが全く動かずかなりハマりました。
ブラウザがまともに動かず悩まされた人も多いのではないでしょうか?

今回はディープリンクを以下と定義し対応方法を紹介したいと思います。

Universal Linksとは?

iOS9から導入された仕組みです。
Universal Linksでは通常のURLから直接アプリを起動することができるようになります。

  • アプリがインストール済:アプリが開く
  • アプリが未インストール:Webページが開く

CustomURLSchemeではSafariを経由してアプリを起動しますが、
Universal Linksではアプリにシームレスに遷移するのでUXがかなり改善されます。
また、CustomURLSchemeとは違いschemeが衝突する心配もありません。

developer.apple.com

f:id:wootan1102:20160419213700g:plain

実際の画面はこのような動きになります。
Googleの検索結果からアプリを起動しています。

Facebook App Linksとは?

Facebookアプリから直接アプリを起動することができるようになります。
Universal LinksはiOS9以降のみ対応ですが、こちらはそれ以前のOSでも動作します。

  • アプリがインストール済:アプリが開く
  • アプリが未インストール:AppStoreが開く

App Links - 参考資料 - 開発者向けFacebook

f:id:wootan1102:20160419213746g:plain

実際の画面はこのような動きになります。
Facebookからアプリを起動しています。

対応するページ

reluxの旅館・ホテルの施設詳細ページのみ対応しました。
Facebook, Instagram, Twitter などのSNSでURLがシェアされた際に
アプリが起動されることを想定しています。
※2016年4月現在

サーバサイドの対応

apple-app-site-associationファイルを作成してルート直下に配置します。

Universal Links
apple-app-site-associationファイル

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "4M2H9MG272.com.loco-partners.relux",
        "paths": [
            "/2????/",
            "/5????/"
        ]
      }
    ]
  }
}

appID: TeamID + BundleIndentifierの組み合わせです。
paths: 対応するパスを記載します。
施設詳細のURLは
https://rlx.jp/20000/ もしくは https://rlx.jp/50000/ のように2または5からはじまる5桁の数字と決まっています。
パスは一般的な正規表現には対応していないので "2????", "5????" としました。

ポイント
  • 拡張子".json"は不要
  • "*"は任意の部分文字列
  • "?"は任意の一文字
  • "NOT"をつけると除外


Facebook App Links

WebページにMETAタグを入れる必要があります。

<meta property="og:url" content="https://rlx.jp/27500" />
<meta property="og:image" content="https://s3-ap-northeast-1.amazonaws.com/relux/img/hotelpictures/RP27500_1046.jpg" />
<meta property="og:site_name" content="relux" />
<meta property="fb:app_id" content="482299358457316" />
<meta property="al:ios:url" content="rlx://hotels/27500/" />
<meta property="al:ios:app_store_id" content="843104033" />
<meta property="al:ios:app_name" content="relux" />

al:ios:url      CustomURLSchemeを指定
al:ios:app_store_id  AppleIDを指定
al:ios:app_name   アプリ名を指定

ポイント
  • FacebookDebuggerで警告がでているとうまく動作しません
  • METAタグが存在しない状態でキャッシュされると動作しません

https://developers.facebook.com/tools/debug/

iOSアプリ側の対応

Capabilitiesの設定
Associated DomainsのDomainsを設定します。
f:id:wootan1102:20160417221955p:plain
applinks:url という形で指定します。

Universal Links

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
    if ([userActivity.activityType isEqual:NSUserActivityTypeBrowsingWeb]) {
        int hotelId = [self matchHotelId:userActivity.webpageURL pattern:UNIVERSAL_LINKS_PATTERN];
        if (hotelId) {
            [self pushHotelDetailViewController:hotelId];
        } else {
            [application openURL:userActivity.webpageURL];
            return NO;
        }
    }
    return YES;
}
ポイント
  • ActivityTypeはNSUserActivityTypeBrowsingWebになります
  • 対応しないURLの場合は openURL でブラウザを開いたほうが良いです

Facebook App Links

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    if ([url.scheme isEqualToString:CUSTOM_URL_SCHEME]) {
        int hotelId = [self matchHotelId:url pattern:CUSTOM_URL_SCHEME_PATTERN];
        if (hotelId) {
            [self pushHotelDetailViewController:hotelId];
            return YES;
        }
    }
    return NO;
}
ポイント
  • CustomURLSchemeに関する実装がされていれば、追加で実装する必要はありません

これから

ディープリンクはUXを改善できるものですが
対応するコンテンツが正しく、素早く表示されることが前提にあります。
どのページを対応するか、表示速度に問題ないかなど慎重に検討をおこなう必要があります。
reluxでは将来的に検索画面やrelux selections, relux Magazineなんかも対応したいと考えています。

rlx.jp
rlx.jp

relux 開発合宿 in 箱根湯本

先日、relux開発合宿を箱根湯本で行いました。
オフィスを離れ、リラックス&集中できる環境で開発に取り組めました。
その時の様子を紹介したいと思います。

お世話になった施設

今回お世話になったのは 「ホテルおかだ」という和風リゾートホテルで
4/15(金)〜4/16(土)の1泊2日の合宿を行いました。
手配はreluxコンシェルジュデスクにお願いしました!

www.hotel-okada.co.jp
rlx.jp

開発合宿の目的

しっかり時間をとり、普段取り組めないことを行う
優先度が低く、あとまわしになってしまっている部分を改修することにしました。
アプリチームでは「ユーザ登録情報変更画面のマテリアルデザイン対応」を行いました。

開発合宿の様子

新宿駅へ集合(1日目 07:00)

早朝に移動して10時から開発する計画です。
特急券を予約したリーダーが寝坊しましたがギリギリ間に合いました。

f:id:wootan1102:20160417230754j:plain
ロマンスカーでの移動

f:id:wootan1102:20160417230822j:plain
座席の説明

f:id:wootan1102:20160417231951j:plain
イケメンAndroidエンジニア

f:id:wootan1102:20160417230837j:plain
移動中の車内でフライング開発?

到着・開発開始(1日目 10:00)

f:id:wootan1102:20160417231106j:plain
箱根湯本駅到着

f:id:wootan1102:20160417231114j:plain
ホテルおかだ到着

f:id:wootan1102:20160417233748j:plain
会議室は部屋の近くでした

f:id:wootan1102:20160417231138j:plain
会議室は壁がホワイトボードで便利

昼食・散歩(1日目 12:00)

昼食は暁庵でお蕎麦をいただきました。
www.hakoneyumoto.com

近くにコンビニはありませんが箱根ベーカリーがあります。
メロンパンがすごく美味しいです。
少し歩くと滝もあるのでリフレッシュには最適です。

f:id:wootan1102:20160421191231j:plain
暁庵

f:id:wootan1102:20160417234106j:plain
見事なコシです

f:id:wootan1102:20160417234130j:plain
天ぷらも美味しい

f:id:wootan1102:20160417234437j:plain
玉簾の瀧

f:id:wootan1102:20160421191909j:plain
アヒルちゃん

f:id:wootan1102:20160418013521j:plain
箱根ベーカリー

開発再開

19時の夕食までかなり集中して開発できました。
途中で不具合にハマったので気分転換に足湯へ
足湯のおかげか不具合も無事に解消しました。

f:id:wootan1102:20160418000359j:plain
気持ちいい

f:id:wootan1102:20160418011901j:plain
もちろんパソコンもあります

f:id:wootan1102:20160418000236j:plain
足湯コーディング

f:id:wootan1102:20160418011825j:plain
せっかくなので見晴らし茶屋で休憩?

夕食(1日目 19:00)

アプリチームはこの時点で実装はほぼほぼ終わっていました。
残りはコードレビューと動作確認のみです。
夕食もとても美味しかったです。
ここでは一旦作業のことは忘れ、先日おこなった勉強会の話題で盛り上がりました!

f:id:wootan1102:20160418012109j:plain

f:id:wootan1102:20160418012301j:plain

f:id:wootan1102:20160418013317j:plain
ビールで乾杯

開発再開(1日目 21:00)

お風呂に入り開発再開です。
会議室は0時までの利用だったので部屋に戻って作業しました。
部屋でもWiFiが使えたので快適に作業できました。

f:id:wootan1102:20160418014217j:plain
リラックスしながら開発

f:id:wootan1102:20160418014305j:plain
深夜の様子

朝食・散歩(2日目 09:00)

朝食はバイキング形式でした。
朝食後は箱根ベーカリーでカフェラテを買うついでに散歩

f:id:wootan1102:20160418014723j:plain
遅くまで作業していたので眠そう

f:id:wootan1102:20160418014756j:plain
箱根ベーカリー(2回目)でカフェラテを購入

f:id:wootan1102:20160418014849j:plain
景色も最高

f:id:wootan1102:20160421191312j:plain
癒やされます

デバッグ(2日目 10:00)

アプリチームは動作確認を終え、成果発表用の資料も完成!
しかし、Webチームで作業がコンフリクトしてしまいバタバタ
チェックアウト時間になってしまい発表会をやる時間がとれませんでした。
成果発表会のみオフィスで行うことに。。。

BBQ(2日目 13:00)

テルチェックアウト後にみんなでBBQ!
準備や後片付けをすべてやってくれるので便利
僕の愛用するCHUMSのショップもありました!
www.herofield.com

f:id:wootan1102:20160418113100j:plain
BBQスタート

f:id:wootan1102:20160418113112j:plain
登山が趣味のデザイナーが焼いてくれています

f:id:wootan1102:20160421191508j:plain
フランクフルト最高

f:id:wootan1102:20160421191546j:plain
美味しい

f:id:wootan1102:20160418113122j:plain
BBQ中でもパソコン

f:id:wootan1102:20160421191409j:plain
ダッチオーブンスチーム

成果発表会

合宿中にできなかった成果発表はオフィスで行いました。
今回行った「マテリアルデザイン対応」は後日まとめて紹介したいと思います。

f:id:wootan1102:20160418112620j:plain
発表の様子

反省点

  • 前半 Webチームのデバッグを手伝えなかった
  • 時間をしっかり切って発表会をするべきだった

良かった点

  • 差込タスクがなく快適
  • WiFi / 会議室が自由につかえて良かった
  • 合宿前に少しだけ着手していたので時間通り終えられた
  • 景色が良くリフレッシュできた
  • 足湯のおかげで(?)不具合が解消した
  • メロンパンが美味しかった

開発合宿を振り返って

今回relux開発チームでの開発合宿は初の試みでした。
やってみると想像以上に集中して取り組めたのではないかなと思います。

普段あとまわしにしてしまっている気になる部分を
改修できエンジニアとしても嬉しかったです。
やりたいことやチャレンジしたいことは他にもたくさんあるので
次回の開発合宿までにネタをいろいろ考えてみたいと思います。

リラックス&集中できる環境を用意していただきありがとうございました。

f:id:wootan1102:20160418020327j:plain

relux iOSアプリにNavigationDrawerを実装

relux iOSアプリにNavigationDrawerを実装しました。
いくつかライブラリも検討したのですが、条件に会うものがなくスクラッチ開発しました。
その時に気をつけたことや工夫したことを紹介したいと思います。

NavigationDrawerとは?

f:id:wootan1102:20160408021018g:plain

画像のように左側からスライドして表示されるメニューがNavigationDrawerです。
詳しくはGoogle design guidelinesを参照
Navigation drawer - Patterns - Google design guidelines

求める要件

  • Androidと同じような挙動になること
  • デザインを自由に変更できること
  • メニューの順序を簡単に変更できること
  • 画面端をスワイプした際に指についてくること
  • メンテナンスされていること(最新のiOSに対応していること)

レイアウト

レイアウトはあとから変更になる可能性が高いので
Storyboard(AutoLayout)を使用しています。


左側のメニュー部分はUITableView、右側の透過している部分はUIViewです。
透過している部分はTouch時にドロワーメニューを閉じるようになっています。

ユーザ情報セル

背景画像は季節毎に変更できるようにサーバから取得するようにしています。
頻繁に変わるものではないですが変更した際にすぐに反映させたかったので
画像のキャッシュ期間は1日としています。
ここでは画像の上に文字をのせるので視認性をあげるためにグラデーションをかけています。

// グラデーションをかける処理 UITableViewCellなどでは複数回呼ばれないように注意
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(0, 0, view.frame.size.width, view.frame.size.height);
gradient.colors = @[
  (id)[UIColor clearColor].CGColor,
  (id)[UIColor blackColor].CGColor
];
[view.layer addSublayer:gradient];

イコン画像は丸くトリミングし、名前と会員IDをアイコンの横にならべて表示しています。

// 画像を丸くトリミング
view.layer.cornerRadius = view.frame.size.width * 0.5f;
view.clipsToBounds = YES;

名前のフォントサイズはかなり大きめに設定していますが
スペースを十分に設けているので実機でみると違和感はありませんでした。

メニューセル

メニューのアイコンはGoogleのMaterial iconsを使用しています。
Androidアプリでよく使われるアイコンですがiOSで使用しても違和感はありません。
ただし、システムアイコンとならべるとボーダーが太いので気をつける必要があります。
design.google.com

メニューセルではRippleEffect(波紋アニメーション)はあえてつけませんでした。
技術的にはつけることは可能なのですが、
iOSらしくない動きであること
古い端末を考慮するとスペック的に厳しいことから実装しませんでした。

レイアウト順序

メニュー部分のレイアウト順序は追加・変更になることがわかっていたので
担当エンジニア以外でも簡単に変更できるように定数で持っています。

#define NAVIGATION_DRAWER_USER_INFO_ROW                  0
#define NAVIGATION_DRAWER_USER_BOOKING_CONFIRMATION_ROW  1
#define NAVIGATION_DRAWER_USER_FAVORITE_ROW              2
#define NAVIGATION_DRAWER_USER_HISTORY_ROW               3

スピード優先で実装しているとハードコーディングしてしまいがちですが
あとで困るのは自分なので こういう部分は意識して定数にするようにしています。

CustomTransitionの実装

今回使用したのは以下になります。

UIViewControllerAnimatedTransition

  • transitionDuration
  • animateTransition

UIPanGestureRecognizer

UIPercentDrivenInteractiveTransition

実装方法についてはAppleドキュメントに記載されているので
紹介はしませんが工夫した点があるので紹介します。
developer.apple.com

指にあわせて動かす工夫

実装時に一番苦労した部分でもあります。
右側の透過部分を含めて1画面としているので
そのまま実装してしまうと指についてこないように見えてしまいます。

そこで画面の幅から計算して擬似的に指についてくるように実装しました。
offset分だけずらしてcontrollerの横幅で除算することで割合を算出しています。

case UIGestureRecognizerStateChanged: {
  CGFloat width = controller.view.bounds.size.width;
  CGFloat fraction = MAX([gesture translationInView:controller.view].x + offset, 0) / width;
  if (fraction > 1.0f) {
    fraction = 1.0f;
  }
  _shouldCompleteTransition = fraction > INTERACTION_THRESHOLD;
  [self updateInteractiveTransition:fraction];
}

やってみて感じたこと

今までCustomTransitionはあまり使っていなかったのですが、実装してみるとそんなに難しくありませんでした。
CustomTransitionを実装るすことによって他のアプリとは違った動きになるので、他の画面でもどんどん取り入れていきたいと考えています。

良かったら実際のアプリを触ってみてください!
いつものreluxを、アプリでも。 | relux