ぽっちぽちにしてやんよ

技術ネタとかアプリに関する話とか

React Nativeで画像をdata-uriにしてハンドリングする

こんにちは,ぽちです.

来週7/12のReact Native Meetup#6でLTします.

ネタはたぶんExpo関連の何かをゆるっと話します.

react-native-meetup.connpass.com

本題です.

React Nativeでインターネット上の(Assetじゃない)画像ファイルをTwitterなどにシェアをしたい時ことが有ったのですが,その時にちょっとハマった知見を共有します.

困ったこと

WebサービスのAPIを叩くと,画像のリソースは基本的にURLで返されます.

React Native上表示させるのであれば,返されたURLをImageコンポーネントのsourceアトリビュートに{uri: xxxx}という形で突っ込めばいいのですが,それを何処かに投稿するとなるとURLのままだと困るケースがあります.

Node.jsだと単純に画像のURLにGETしてpipeでStreamを繋いで投稿先にPOSTするとかが使えるかもしれませんが,React Nativeだと同等の手段がなく,困りました.

ShareコンポーネントはURLが受けれる

今回必要になったのは,Twitterへのシェアだったので,

Share コンポーネントを使えばURLのままでも行けるのでは?と思ったのですが,クライアントによってはいい感じで表示してくれなかったため,data-uriの形にして画像としてShareコンポネントに突っ込みたいからでした.

解決法その1: Expo.takeSnapshotAsync

まず初めに思いついたのは,Expoのモジュールにある画面のスナップショットを撮るメソッドtakeSnapshopAsyncを使って,Imageコンポーネントで描画し,そのImageコンポーネントのスナップショットを撮るという手法でした.

<Image
  source={{ uri: this.state.thumbnail }}
  resizeMode="contain"
  style={styles.thumbnail}
  ref="thumbnail"
/>

という形で,得られた画像のurlをthis.state.thumbnailなどに入れておき,それをImageコンポーネントで描画します.

takeSnapshopAsyncで指定するために,refアトリビュートを設定しておきます.

share() {
  Expo.takeSnapShotAsync(this.refs.thumbnail, {
    format: 'jpg',
    quality: 0.9,
    result: 'data-uri',
  })
  .then((image)=>{
    Share.share({
      message: 'message',
      url: img,
      title: 'Share'
    }, {
      dialogTitle: 'Share',
      excludedActivityTypes: [],
      tintColor: 'green'
     });
  });
}

このようにtakeSnapShotAsyncresultプロパティにdata-uriを指定して画像を取得してShareコンポーネントのurlプロパティに設定すればTwitterなどのアプリに画像をシェアすることが出来ます.

しかし,実際にシェアされた画像を見ると・・・

f:id:poChi:20170705135745p:plain

このように白帯が入ってしまっています.

これはImageコンポーネントのスタイルにbackgroundColor: 'transparent'を指定しても同様です.

自分はこれがどうしても気に食わなかったので別の方法を探しました.

解決法その2: buffer

次は,bufferというnpmモジュールを使いました.

GitHub - feross/buffer: The buffer module from node.js, for the browser.

これは,node.jsの標準で用意されているBufferをブラウザにも持ってくるという意図で作られているモジュールです.

これを使って,画像のarraybufferを取得しdata-uriに変換して渡してやろうと考えました.

import axios from 'axios';
import { Buffer } from 'buffer/';
axios
  .get(this.state.thumbnail, {
    responseType: 'arraybuffer'
  })
  .then(response => {
    const image = Buffer.from(response.data).toString('base64');
    Share.share({
      message: `message`,
      url: `data:${response.headers['content-type'].toLowerCase()};base64,${image}`,
      title: 'Share'
    }, {
      dialogTitle: 'Share',
      excludedActivityTypes: [],
      tintColor: 'green'
    });
});

buffer/となっているのはTypoではなく,そう指定するようにドキュメントに書かれています.

画像のarraybufferの取得は GitHub - mzabriskie/axios: Promise based HTTP client for the browser and node.js を使って,responseTypearaybufferを指定してGETすることで取得しました.

request.jsとかsuperagentとかfetchでも出来るような気もします.

得られたarraybufferをBuffer.fromに入れ,toString('base64')で変換します.

このままだと,data-uriではないので,data:${response.headers['content-type'].toLowerCase()};base64,${image}という形にして,data-uriとしてShareコンポーネントのurlプロパティに入れます.

これで元画像を白い帯などが付与されることなく共有できます.

React Nativeで画面を作ってるときにキーボードでボタンが隠れて困るときに使えるKeyboardAvoidingViewが良いという話

こんにちは,ぽちです.

何かとReact Nativeでアプリを作っているとフォーム的なやつよく作りますよね. 上にテキストフォームを置いて,下に決定ボタンを置くデザインなどが非常に良くあるデザインですが,単純に実装してしまうと、下の決定ボタンがキーボードによって隠れてしまって使い勝手が良くない…ということ,良くありますよね. 上部に長文を書くTextInputを,下部に決定ボタンを置いたものを例に挙げると,

f:id:poChi:20170607132211g:plain

このような形で決定ボタン(Tap Hereと書いてある領域)がテキスト入力時に出てきたキーボードによって隠れてしまいます.

そんな時にReact Nativeではこの現象を回避するためのコンポーネントが用意されてたりします. それがKeyboardAvoidingViewです.

このKeyboardAvoidingViewですが,設定するbehaviorプロパティが直感的には分かりにくく「どれを使えばいいんだ!」となりがちです.それぞれbehaviorに設定した値によって動きがどう変わるのかを見ていきます.

(コードサンプルだけ欲しい方は KeyboardAvoidingViewSample · GitHub をどうぞ)

まず,KeyboardAvoidingViewを使うのでimportします.

import { KeyboardAvoidingView } from 'react-native';

KeyboardAvoidingViewを使わない場合のrender()で返すJSXのコードは,

<View style={{flex: 1}}>
  <TextInput style={{flex: 10}} />
  <TouchableOpacity style={{flex: 1, (略)}}>
    <Text>Tap Here</Text>
  </TouchableOpacity>
</View>

のような形になっています.

よく全体を<View>で囲ったりしますが(単一のコンポーネントを返さないといけないため),その直下に<KeyboardAvoidingView>で囲みます.

<View style={{flex: 1}}>
  <KeyboardAvoidingView behavior={this.state.behavior}>
    <TextInput style={{flex: 10}} />
    <TouchableOpacity style={{flex: 1, (略)}}>
      <Text>Tap Here</Text>
    </TouchableOpacity>
  <KeyboardAvoidingView>
</View>

そこで,問題となるのがbehaviorプロパティです. このプロパティには以下の文字列値を取ります.

  • padding
  • height
  • position

それぞれ図を交えて説明します.

behavior=‘padding’

大体paddingを指定しておけばうまく行くケースが多いです. 動作的には,画面を構成する要素(今回だとTextInputとかTouchableOpacityとかの部分)がflexプロパティで構成されていれば,キーボードで狭くなった領域を100%として調整してくれます.

f:id:poChi:20170607132245g:plain

そしてキーボードを閉じると,キーボードを開く前と同じ状態に戻ってきます.

behavior=‘height’

使い道がよくわからないのですが,大体の動作はpaddingを指定した際と同等の動きをします. 違うのは,キーボードを閉じたときに,キーボードを開いて狭まった領域がそのまま維持されるということです.(キーボードがあった領域がそのまま空く)

f:id:poChi:20170607132312g:plain

behavior=‘position’

positionを指定した場合は,<KeyboardAvoidingView>のプロパティにcontentContainerStyleを指定します.(positionを指定したときだけ有効なプロパティです) positionを指定し,キーボードが現れたときに<View>コンポーネントで囲まれるため,{flex: 1}heightを指定しないと要素がグシャっとなってしまうことがよく有ります. キーボードを閉じるとまた元の状態に戻ります. 細かく指定したい場合はpositionを使うと良いのではないでしょうか.

f:id:poChi:20170607132342g:plain

まとめ

キーボードでUI要素が隠れて困る場合はKeyboardAvoidingViewコンポーネントを活用しよう!

動作を確認するために使ったコードは KeyboardAvoidingViewSample · GitHub に置いておくので,いじったりして動作を色々見てもらったら分かりやすいと思います.

Expoを使っていたReact NativeのアプリでNative Modulesを使うためにdetachする

こんばんは,ぽちです.

Expoを使って開発をしていたら,途中でどうしてもNative Modulesを使いたいというケースが出てきます. その場合はdetachする必要が出てきます. detachした後はXCodeやAndroid Stuidoで自分で管理する必要が出てきますので,その辺りよく考えてからdetachの道へ進むことをオススメします.うちは極力detachの道へ進みたくありません.

detachした後でもXDEで起動とかXDEからPublishとかは出来ます. Publishは出来ると言っても,Nativeの部分はケアされないのでその部分は動きません.(クラッシュするでしょうね)

それでは,detachの流れを追いましょう. 基本的には Detaching to ExpoKit に沿ってコマンドを実行して行けばdetach出来ます.

初めに,exp コマンドが必要なので,入れていない場合はインストールします.

$ npm install -g exp

Expoへのログインが必要なので,ログインします.

$ exp login

exp.jsonに必要な項目を追加します.

 {
   "name": "Your App Name",
   "icon": "./path/to/your/app-icon.png",
   "version": "1.0.0",
   "slug": "your-app-slug",
   "sdkVersion": "17.0.0",
   "ios": {
     "bundleIdentifier": "com.yourcompany.yourappname のようなのを入れる",
   },
   "android": {
     "package": "com.yourcompany.yourappname のようなのを入れる",
   }
 }

その後exp detachを実行すると色々処理が行われてiosディレクトリやandroidディレクトリが作成されたりします.

$ exp detach       
[exp] Making sure project is set up correctly...
[exp] Your project looks good!
Validating project manifest...
Downloading iOS code...
Moving iOS project files...
Naming iOS project...
Configuring iOS project...
Using shell config: { manifestUrl: 'exp://exp.host/@pchw/***********',
  isShell: true,
  isManifestVerificationBypassed: true }
Configuring iOS dependencies...
Cleaning up iOS...
iOS detach is complete!
Downloading Android code...
Moving Android project files...
Configuring Android project...
Naming Android project...
Cleaning up Android...
Android detach is complete!

Writing ExpoKit configuration...
Finished detaching your project! Look in the `android` and `ios` directories for the respective native projects. Follow the ExpoKit guide at https://docs.expo.io/versions/latest/guides/expokit.html to get your project running.

XDEでRestartします.

f:id:poChi:20170528231308p:plain

その後,iosディレクトリに移動して,pod installを行って依存するモジュールのインストールを行います.これは結構時間がかかります.

$ cd ios
$ pod install
Analyzing dependencies
Pre-downloading: `ExpoKit` from `http://github.com/expo/expo.git`, tag `ios/1.16.2`
Fetching podspec for `React` from `../node_modules/react-native`
Fetching podspec for `Yoga` from `../node_modules/react-native/ReactCommon/yoga`
Downloading dependencies
Installing Amplitude-iOS (3.14.1)
Installing Analytics (3.6.1)
Installing AppAuth (0.9.1)
Installing Bolts (1.8.4)
Installing Branch (0.14.12)
Installing CocoaLumberjack (3.2.0)
Installing Crashlytics (3.8.4)
Installing ExpoKit (1.16.2)
Installing FBAudienceNetwork (4.23.0)
Installing FBSDKCoreKit (4.23.0)
Installing FBSDKLoginKit (4.23.0)
Installing FBSDKShareKit (4.23.0)
Installing Fabric (1.6.11)
Installing FirebaseAnalytics (3.9.0)
Installing FirebaseCore (3.6.0)
Installing FirebaseInstanceID (1.0.10)
Installing GPUImage (0.1.7)
Installing GTMOAuth2 (1.1.4)
Installing GTMSessionFetcher (1.1.10)
Installing Google (3.0.3)
Installing GoogleMaps (2.2.0)
Installing GoogleSignIn (4.0.2)
Installing GoogleToolboxForMac (2.1.1)
Installing React (0.44.0)
Installing Yoga (0.44.0.React)
Installing lottie-ios (1.5.1)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There are 15 dependencies from the Podfile and 26 total pods installed.

補足:pod install時にエラーが起こった場合 手元の環境ではpod install時に以下のようなエラーが出ました.

$ pod install
Analyzing dependencies
Pre-downloading: `ExpoKit` from `http://github.com/expo/expo.git`, tag `ios/1.16.2`
Fetching podspec for `React` from `../node_modules/react-native`
Fetching podspec for `Yoga` from `../node_modules/react-native/ReactCommon/yoga`
[!] Unable to find a specification for `lottie-ios (~> 1.5.1)` depended upon by `ExpoKit/Core`

その場合はpod setup を実行し,その後再度pod installをすることで成功するようになりました.

$ pod setup
Setting up CocoaPods master repo
  $ /usr/local/bin/git -C /Users/pochi/.cocoapods/repos/master fetch origin
  --progress
  remote: Counting objects: 287020, done.        
  remote: Compressing objects: 100% (83/83), done.        
  remote: Total 287020 (delta 52618), reused 52644 (delta 52603), pack-reused 234331        
  Receiving objects: 100% (287020/287020), 29.79 MiB | 432.00 KiB/s, done.
  Resolving deltas: 100% (193366/193366), completed with 7824 local objects.
  From https://github.com/CocoaPods/Specs
     d03518a..3c729d8  master     -> origin/master
  $ /usr/local/bin/git -C /Users/pochi/.cocoapods/repos/master rev-parse
  --abbrev-ref HEAD
  master
  $ /usr/local/bin/git -C /Users/pochi/.cocoapods/repos/master reset --hard
  origin/master
  Checking out files: 100% (29533/29533), done.
  HEAD is now at 3c729d8 [Add] iLog 1.2.5
Setup completed

ここまで出来れば,あとはビルドして実行するだけです. .xcworkspaceのファイルをXCodeで開いてビルド→実行でも良いですし,以下のようにreact-native run-iosを使うのでもいいです. XCodeを開きたくないので,react-native run-iosします.

$ cd ..
$ react-native run-ios
Scanning 550 folders for symlinks in /Users/pochi/Documents/development/github/expokit/node_modules (11ms)
Found Xcode workspace expokit.xcworkspace
(長いので省略)

** BUILD SUCCEEDED **

Installing build/Build/Products/Debug-iphonesimulator/expokit.app
Launching com.yourcompany.yourappname
com.yourcompany.yourappname: 82444

自動的にSimulatorが起動して実行されます.

この状態で,XDEの方からbundleされたjsが配信されている状態になります. もちろん,jsのコードを編集するとすぐにSimulatorに反映されます.

あとは,使いたいNative Modulesをインストールして,react-native linkrnpm linkして使うことができます.

おめでとうございます,detach成功です.

Expoを使ったReact Nativeの開発フロー

こんにちは,ぽちです.

そろそろ次のReact Nativeで作ったアプリが申請に出せそうです.

quickAnnictは今朝1.7.0の更新をリリースしました.リリースノートには書いていないですが,Expo入りしてNative Modulesを使わないようにして内部的には結構変更が入っています.

quickAnnict

quickAnnict

  • Hiroshi HORIKI
  • エンターテインメント
  • 無料

さて,React Nativeの開発なのですが,普通にReact NativeのGetting Startedを見るとreact-native initをしてXCodeでやることもあり〜という感じで色々大変なことがあります.

今の基本のReact Nativeを用いた開発ではExpo(https://expo.io/)を使って開発を行っています. Expoを使うことでXCodeとは無縁になり,実機での動作も脱XCode出来るのでいつも使っているエディタで編集→即ホットロードされる→どんどん開発が進む→やったー!という感じになって高まるのでオススメです.

「よく分からないし・・・」とか言って敬遠している場合でないです.うちはもうExpoを使わないで開発するのはクソダルいのでなんとかExpoの恩恵を受けて開発を出来るようにしています.(Native Modulesを使うとExpoの恩恵が受けれなくなる)

では,Expoを使った開発のフローはどういう感じになるかを見ていきましょう.

f:id:poChi:20170527135147p:plain

  • 左上のProjectをクリックしてNew Projectを選ぶ
    • Blankを選ぶで良い
      • Tab Navigationの方を選ぶと色々を付けてくれる(TabだけじゃなくてPush Notificationとかも)
      • が,不要なのを消すのが面倒くさい.
      • Tab NavigationはExNavigatorという,これからはreact-navigation使ってねみたいに書いてるExpo開発のモジュールを使っているのでBlankでした方がいい

f:id:poChi:20170527135217p:plain

  • ここでもうSimulatorで動かせる状態
  • 右上のDeviceを押してiOS Simulatorを選ぶ
  • 画面が立ち上がって実行される
  • エディターでmain.jsを開いて,編集する.
  • 勝手にSimulatorの画面がリロードされる
  • あとは自由にソースをいじる

Simulatorを動かしたくない場合(MBAで開発してるなどリソースが厳しいとか)

f:id:poChi:20170527135232p:plain

右上のShareを押すと「QRコードを使う」か「メールアドレスにリンクを送る」かが選べるので,iPhoneにExpoアプリを入れてQRコードを読み取るか,メールアドレスにリンクを送ってiPhoneで開いてExpoアプリが開かれるという手順でExpoアプリ上で実行が出来る.

iPhoneのExpoアプリは Expo Client on the App Store からダウンロード出来るので入れておくこと.

その後,アプリが出来上がってストアに申請するぞ!となったら React NativeのアプリをExpoで単体アプリ(Standalone App)ビルド - ぽっちぽちにしてやんよ を参考にStandalone Appのバイナリをビルドして,iTunes Connectで新規アプリを作成,ビルドしたバイナリをアップロードしたりして申請するという感じです.

React NativeのアプリをExpoで単体アプリ(Standalone App)ビルド

こんばんは,ぽちです.

最近はReact Nativeで戯れていて,quickAnnictというアプリを出してたりします.quickAnnictはAnnictというアニメ視聴管理サービスに素早くアニメ見た記録を残すためのアプリ.

https://itunes.apple.com/jp/app/quickannict/id1227142164?mt=8

quickAnnictはreact-native initで作ってたんですが,別アプリを作ったときにExpo( expo.io )の便利さに気づいてExpo入りしようと頑張ってるナウです.

んで,Expo入りしている途中でどうしても単体アプリ(Standalone App)としてビルドしないといけない理由が出てきたので,その手順を記しておきます.

ちなみにその理由とは,quickAnnictの場合は,Deep Linkingと言われるURLの先頭のスキーマをアプリ固有のやつにして,URLキックしたときにアプリが開いてくるやつです.アレがStandalone Appとしてビルドして確認しないとExpo XDEでの確認だと出来ない.

さて,手順ですが,基本はドキュメントに全部書いてます. https://docs.expo.io/versions/v17.0.0/guides/building-standalone-apps.html#content

まずexpo.ioにログインするためにexp login

$ exp login
? Username/Email Address: **********
? Password: **********

Success.

exp.jsonを編集する.

$ vi exp.json
  • bundleIdentifierを入れる
  • iconを入れる
  • nameを入れる
  • versionを入れる

exp startする.自分の場合はもう始まってた.

$ exp start
[exp] Making sure project is set up correctly...
[exp] Your project looks good!
[exp] exp is already running for this project. Exiting...

exp build:iosする.

$ exp build:ios
[exp] Making sure project is set up correctly...
[exp] Your project looks good!
[exp] Checking if current build exists...

[exp] No currently active or previous builds for this project.
[exp] Checking for existing Apple credentials...

We need your Apple ID/password to manage certificates and provisioning profiles from your Apple Developer account.
? What's your Apple ID? ***********
? Password? ***********
? What is your Apple Team ID (you can find that on this page: https://developer.apple.com/account/#/membership)? ***********
[exp] Validating Apple credentials...
[exp] Credentials valid.

? Do you already have a distribution certificate you'd like us to use,
or do you want us to manage your certificates for you? true
[exp] Generating distribution certificate...
[exp] Distribution certificate setup complete.
[exp] Validating app id...
? Do you already have a push notification certificate you'd like us to use,
or do you want us to manage your push certificates for you? true
[exp] Fetching a new push certificate...
[exp] Push certificate setup complete.
[exp] Starting build process...
[exp] Publishing...
[exp] Building iOS bundle
[exp] Building Android bundle
[exp] Analyzing assets
[exp] Uploading assets
[exp] No assets changed, skipped.
[exp] Uploading JavaScript bundles
[exp] Published
[exp] Your URL is

https://exp.host/@pchw/(ここにexp.jsonで指定したslugが入る)

[exp] Building...
[exp] Build successfully started, it may take a few minutes to complete. Run "exp build:status" to monitor it.

Team IDは Sign in with your Apple ID - Apple Developer にいくと

image

に書かれてる.

その後ビルドが出来上がるまでちょっと時間がかかるので,定期的にexp build:statusで進捗どうですか?する

$ exp build:status
[exp] Making sure project is set up correctly...
[exp] Your project looks good!
[exp] Checking if current build exists...

[exp] ============
[exp] Build Status
[exp] ============

[exp] iOS: Build in progress...

まだ出来上がってない.

$ exp build:status
[exp] Making sure project is set up correctly...
[exp] Your project looks good!
[exp] Checking if current build exists...

[exp] ============
[exp] Build Status
[exp] ============

[exp] iOS:
[exp] IPA: https://***********.ipa

おっ出来たみたいやな!

IPA: のところに書かれているURLにアクセスしてバイナリをダウンロードする.

あとはTestFlightで配布するなどする.(fastlane pilot uploadを使うと楽)

$ PILOT_IPA="/Users/pochi/Downloads/なんとかかんとか.ipa" fastlane pilot upload

まとめ

  • Expoだけでどうしても確認できない項目があったので,Standalone Appとしてビルドする必要があった
  • Expoの補助を得て,割りと簡単にStandalone Appとしてビルドが出来た
  • しかし,Expo XDE経由のjs更新すぐ確認に比べると手間がマッハ
  • 特にbuildに非同期の待ちが発生するのと,TestFlightへのアップロード→実機での確認でめちゃくちゃ時間を浪費する
  • Standalone Appとしてビルドして確認しなければならないケースはなるべく回避したほうが開発効率が良い