ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

iTunes ConnectにアプリをアップロードしてTestFlightでテスト

概要

iTunes ConnectにアプリをあげてTest Flightする方法がわからなかったので、多分これでいけるんじゃないかという手順をまとめました。個人的にメモしておきます。間違い、訂正ありましたら教えていただきたいです。

前提としてApple Developer Program登録済みです

当記事で使用しているXcodeのバージョンは8.3.3です、7以下のバージョンでは「3. XcodeからArchiveをアップロード」で言及しているAutomatically manage sigining機能は使えないようです

手順

0. 既存アプリのリネーム(optional)

簡単のため既存のReact Nativeアプリを再利用します。他の環境の方は無視していただいて結構です。

React Nativeアプリのリネームは簡単です。react-native-renameをインストールし、コマンドを発行します。

$ npm install -g react-native-rename
$ cd path/to/project/root
$ react-native-rename 新しいアプリの名前

1. Apple Developer Portalで新しいApp IDを作成

Developper Portalにログインし、Certificates, Identifiers & Profiles画面のサイドメニューIdentifiers > App IDsをクリックします。画面右上の+を押してApp ID作成に必要な情報を入力していきます。以下は各入力項目の説明です。

f:id:ushumpei:20170710170637p:plain

  1. App ID Description
    • name: 整理しやすいような名前をつける
  2. App ID Suffix (以下どちらかを選択、今回はExplicit App ID)
    • Explicit App ID > Bundle ID: reverse-domain name style stringとか一意になるように名前をつける
    • Wildcard App ID > Bundle ID: com.domainname.*のように指定すると、App IDを複数の(iTunes Connectのレコードという意味での)アプリで使用できるらしいですが、今回は使用しません。

その他は特に変更しませんでした、お好みで設定してください。

2. iTunes Connectで新規アプリレコードを作成

iTunes Connectにログインし、マイ App画面の左上の+を押して新規アプリに必要な情報を入力していきます。

f:id:ushumpei:20170710170736p:plain

今回は、

  • プラットフォーム: iOS
  • 名前: 新しいアプリの名前
  • プライマリ言語: 日本語
  • バンドルID: 先ほどApp IDを作成した時のバンドルIDを選択。
    • セレクトボックスにはname - bundle_idのように表示されます
    • 選択肢に表示されない場合はリロードして見てください
  • SKU: バンドルIDと同じもの
    • SKUはiTunes Connect IDと呼ばれるiTunes Connect内で一意になる値のようです。

を入力しました。

3. XcodeからArchiveをアップロード

3.1. アプリの情報の編集

新しいアプリの名前.xcodeprojファイルをxcodeで開いてGeneralタブへ行き項目を編集します。(react-nativeであればプロジェクトルート以下のios/新しいアプリの名前.xcodeprojにあります)

f:id:ushumpei:20170710170904p:plain

  • Identity
    • Bundle Identifier: iTunes Connectで新規アプリレコード作成の際に入力した値
    • Version: 0.0.1
    • Build: 0.0.1
      • VersionとBuildは変えなくてもいいですがせっかくなので。
  • Signing
    • Automatically manage sigining: チェックを入れます、プロビジョニングファイルの作成などやってくれるのでおすすめです。
    • Team: 自分のチームを選択してください

以上で設定終了です。

3.2. Archiveのアップロード

画面上部、Product > Archiveを選択しアーカイブを実行します。しばらく時間がかかる処理です。アーカイブが完了すると、ウィンドウが立ち上がります。

注意: Archiveが選択できない場合があるので、こちらを参考にしてください。

f:id:ushumpei:20170710170940p:plain

アップロードしたいArchiveを選択し、ウィンドウ右のUpload to App Store...を押すとアップロードの準備に入ります(先にValidate...を押してからの方が堅実だと思います、今回はやりませんでしたが)。

チームを聞かれるので自分のものを選択すると、アップロード画面になります。内容が問題なければUploadを押してください。これも、しばらく時間がかかる処理です。アップロードが完了しビルドが作成されるとiTunes Connectからメールが来るので、気楽に待ちましょう。

ただし、失敗することもあるので、20分くらい音沙汰がなければアップロード画面の結果にエラーがないか確認しましょう。(Validate...してからのアップロードなら、多少は安心していいかもしれません)

注意: Uploadを押した後、処理の表示がAuthenticating with the iTunes storeで止まってしまうことがあるようです。全てに対応できる方法かはわかりませんが、この辺りが参考になりました

4. iTunes Connect でTestFlightのテストを開始

4.1. TestFlightの準備

再びiTunes Connectにログインし、マイ Appで先ほど作成したアプリを選択します。タブからTestFlightを選択すると、アップロードしたビルドが表示されているかと思います。

f:id:ushumpei:20170710172810p:plain

ビルドに「輸出コンプライアンスがありません」と警告が表示されています、この状態ではまだTestFlightでテストを開始することができません。ビルドの値を押すと、画面が切り替わり、コンプライアンス情報を提出というボタンを押すとモーダルが表示されます。今回特に暗号化していないのでいいえを選択しました。モーダル右下の内部テストを開始を押すと、テストの準備が整いました。

f:id:ushumpei:20170710173317p:plain

4.2. テストへの招待

サイドメニューのテスター & グループの小項目iTunes Connect ユーザを追加外部テスターを追加を選択しテストへの招待を行います。今回はiTunes Connect ユーザを追加から自分に対して招待を行いました。

5. 終わり

TestFlightからメールが届くので指示に従い、TestFlightアプリをインストールしている端末からテストが開始できるようになります!

感想

おそらくはこれでいいだろうというものをまとめてみました。自分の環境ではダメだったなど、ご指摘いただければ幸いです。

特に使いだしてから色々いじった気がするのでXcode周りの手順がやや不安です。

FlatListのデータ更新時に再描画されない

概要

ちょっと困ってしまって、検索が時間かかったので他の人の手助けになればと思いメモします。

問題

React NavigationとFlatListでリスト編集サンプルアプリを作成した時に、FlatListのデータ更新時に再描画されない問題に遭遇しました。

  • データをFlatListで表示する画面がある
  • 他の画面でデータを更新する
  • FlatListの画面を開くとデータが描画されていない(更新したものだけでなく、元からあったものの描画されない)
  • ちょっとスクロールすると再描画される

なんだこれ?と悩んでしまいました。

解決

検索するとextraDataを設定しろとか色々見つかったのですが、このIssueから解決できました。

github.com

コメントで言及されているケースと同じ状態だったのでFlatListremoveClippedSubviews={false}を追加してあげると再描画されるようになりました。

感想

removeClippedSubviewsは高速化のためのフラグだそうです。FlatListがラップしているVirtualizedListのドキュメントにこのプロパティに関する言及があります。またこのページに「バグを引き起こすかも」、という注意がされているのも確認できます。

上記の方法でもうまくいかないケースがあるそうなのでIssueを読んでみると何か糸口が見つかるかもしれません。

React Navigationで画面遷移してみる

React Nativeで画面遷移したかったのでまとめました。

内容としてはcreate-react-native-appで作ったアプリで、2つのナビゲーター(StackNavigatorTabNavigator)を使ってみた勉強記事です。

注意としてはDrawerNavigatorは使わないことと、redux等との連携も書いてないことです。

記述方法等は公式ドキュメントをご確認ください。

概要

公式ドキュメント で紹介されている画面遷移用のライブラリはいくつかありますが、 クロスプラットフォームで動作するものと考えると選択肢に入るのは以下の二つかなと思いました。 二つとも綺麗なドキュメントが用意されています。

どちらを選んでもよかったのですが、 create-react-native-appで使ったアプリを書きたいと思ったので、 javascriptだけで書かれているReact Navigationで書くことにしました。

React Navigation

React NavigationはReact Nativeで画面遷移を実装するために、いくつかのナビゲーター(navigator)というコンポーネントを利用します。

ナビゲーターはルーティングしたり画面に引数をあげたりジェスチャーを制御したり、画面遷移に必要なものを提供してくれるものです(ざっくりです)。

標準で使えるナビゲーターとしては、StackNavigatorTabNavigatorDrawerNavigatorです。それぞれ以下のような特徴があります。

  • StackNavigator: 画面を積んでいくタイプのナビゲーター。Screen1 -> Screen2とかScreen1 -> Screen3とか画面を積んでツリーを作っていき、行きつ戻りつしていくという感じです。描画した画面内のコンポーネントを押したら別の画面に遷移させる、といったWebのリンクと同じようなケースで使う感じです。
  • TabNavigator: タブタイプのナビゲーター。[Screen1、Screen2、Screen3]のように並列に画面を依存させずに扱い、切り替えていく感じです。画面遷移はTabNavigator自身が責任を持つ感じです。
  • DrawerNavigator: ドロワータイプのナビゲーター。自分的にはタブが横なら、ドロワーは縦という程度の位置が違うくらいの認識です。ただ、タブに比べて登録できる画面数が多い(多くても比較的わかりにくくならない)ので、例えばユーザーによって画面数が変化するタイプのものであれば、これを使うのかなと思います。

通常はこれらをネストさせるなどしてアプリケーションの画面遷移を作っていきます。カスタムしたナビゲーターを作ることもできるようですが、まだいいかなと思います。(丸いナビゲーターとかARっぽくてカッコ良さそう)

初心者的にはネストさせ方がちょっと不明で困ります。よく見るアプリケーションでは、

  • TabNavigator(下タブ) or DrawerNavigatorをルートの親ナビゲーターとして選ぶ
  • それぞれの子にScreen(普通の画面)、StackNavigator、TabNavigator(上タブ)を任意に登録
  • そのそれぞれの子に画面を登録

のネストが2段くらいの構成になっていると思いますのでそれを真似します。

疑問: 認証、設定などの画面はどのように制御するのがいいのでしょうか?設定画面はモーダルを使って表示するのはよく見る気がします?認証画面もモーダルを使った表示を見ることがありますが、一瞬アプリが表示されてからシュルッと画面を乗っけるとかだと、裏側で画面が動かないように制御しなければいけない気がします。トップレベルのに認証用のナビゲーターをかませてネストを3段にするとか?

準備

ドキュメントを参考にインストールします。

$ create-react-native-app demo-application
$ cd demo-application
$ npm install --save react-navigation

StackNavigator

単純な使い方として、リストを表示して、押したらその詳細画面に遷移する例を書きます。

自動生成されるApp.jsに以下のコードを記述します。

gist.github.com


StackNavigator

という感じになりました。今回は単純な例なので遷移先のDetailは使い回しです。

TabNavigator

2つの画面を持つTabNavigatorを作成します。

  • App
    • MathematicsList: 先ほど作った数学リスト
    • AddMathItemScreen: 数学項目追加画面

やることは3つです、

  1. TabNavigatorのコンポーネントを作成
  2. 項目追加機能の実装
  3. 項目削除機能の実装

Step1 - TabNavigatorのコンポーネントを作成

App.jsを修正してTabNavigatorを描画するコンテナーコンポーネントを作成していきます。

...
import { TabNavigator } from 'react-navigation'; /* 追加 */

/*
 * TabNavigatorを作成
 * StackNavigatorと基本は同じ
 * 第二引数で画面下タブに表示されるアイコン色とラベル非表示を設定
 */
const Tab = TabNavigator({
  List: { screen: MathematicsList },
  AddItem: { screen: AddMathItemScreen },
}, {
  tabBarOptions: {
    activeTintColor: '#037aff',
    inactiveTintColor: '#737373',
    showLabel: false,
  },
});
/*
 * 二つのタブでデータを共有するためにAppコンポーネントをコンテナコンポーネント化
 * 今の所stateを持っている以外はTabNavigatorをラップしているだけ
 */
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      mathematics,
    };
  }

  render() {
    /* screenPropsで各子供にmathematicsを渡している */
    return <Tab screenProps={{ mathematics: this.state.mathematics }} />;
  }
}

TabNavigatorStackNavigatorコンポーネントなのでjsx記法でrenderに書くことができます。screenPropsに値を入れると各タブでデータを受け取ることができるようです。ここでは各タブにmathematicsデータを渡すために、TabNavigatorの設定をして一旦Tabと名付けた後に、Appコンポーネントをコンテナ化し内部でscreenPropsを渡しました。

TabNavigatorで登録された画面にnavigationOptionsを設定していきます。AddMathItemScreenは以下のように作成しました。

...
import { Entypo } from '@expo/vector-icons';
...
/*
 * 項目追加画面を作成
 * 現在はタブアイコンの設定のみのモック
 */
const AddMathItemScreen = () => (
  <View style={styles.container}>
    <Text style={styles.paragraph}>This is AddMathItemScreen</Text>
  </View>
);
AddMathItemScreen.navigationOptions = {
  tabBarIcon: ({ tintColor }) => <Entypo size={24} name="add-to-list" color={tintColor} />,
};

tabBarIcontintColorTabNavigatorのオプションで設定していて、activeinactiveに応じて異なる値が送られてきます。Entypocreate-react-native-appで作成すると初めから使えるexpoライブラリに含まれているカスタムフォントのコンポーネントです。

AddMathItemScreenと同様にMathematicsListにもタブアイコンの設定を行います。

/*
 * StackNavigatorを変数に格納
 */
const Stack = StackNavigator({
  Detail: { screen: DetailScreen },
  List: { screen: ListScreen },
}, {
  initialRouteName: 'List',
});
/*
 * StackNavigatorをラップするコンポーネント
 * screenPropsにより親からもらったpropsを子にそのまま流している(手抜き)
 * mathematicsリストをAppコンポーネントに持たせているため、
 * ここで設定したものがListScreenに渡っていく。
 */
const MathematicsList = ({ screenProps }) => (
  <Stack screenProps={screenProps} />
);
/*
 * タブアイコンの設定
 */
MathematicsList.navigationOptions = {
  tabBarIcon: ({ tintColor }) => <Entypo size={24} name="list" color={tintColor} />,
};

また、親からscreenPropsが渡されてくるのでそれをListScreenに渡すためにここでも一旦StackNavigatorを変数に格納して、MathematicsList内で受け渡しを行います。(もっといい方法があると思うんですが。。。ちゃんとやるときは多分reduxとか使うのでこの辺りはなあなあにしておきます)

ちなみにListScreenは次のように値を受け取ります。

/* 引数screenPropsが親から渡ってくる */
const ListScreen = ({ navigation, screenProps }) => (
  <FlatList
    data={screenProps.mathematics} /* mathematicsを取り出す */
...


TabNavigator1

上記の変更をまとめたのが以下になります。

gist.github.com

2つのタブが画面下に表示され、片方の画面は先ほどのStackNavigatorがネストされている状態になっています。

追記: タブアイコンの設定箇所をTabNavigatorの定義箇所に変更することも可能なようです。StackNavigatorに関しても同様で、タイトルなど動的に変更する必要があるかどうかで分けるという感じです。

const Tab = TabNavigator({
  List: {
    screen: MathematicsList,
    navigationOptions: {
      tabBarIcon: ({ tintColor }) => <Entypo size={24} name="list" color={tintColor} />,
    },
  },
  AddItem: {
    screen: AddMathItemScreen,
    navigationOptions: {
      tabBarIcon: ({ tintColor }) => <Entypo size={24} name="add-to-list" color={tintColor} />,
    },
  },
}, {
  initialRouteName: 'List',
  tabBarOptions: {
    activeTintColor: '#037aff',
    inactiveTintColor: '#737373',
    showLabel: false,
  },
});

Step2 - 項目追加機能の実装

項目追加機能を実装するために、Appコンテナコンポーネントからstateをいじれる関数を、TabNavigator経由でAddMathItemScreenコンポーネントまで渡します。

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      mathematics,
    };
    this.addNewMathItem = this.addNewMathItem.bind(this);
  }

  /* 子供に渡す関数を作成 */
  addNewMathItem({ title, detail }) {
    this.setState({
      mathematics: [...this.state.mathematics, { title, detail }],
    });
  }

  render() {
    /* screenPropsで各子供にmathematics、addNewMathItemを渡す */
    return <Tab screenProps={{ mathematics: this.state.mathematics, addNewMathItem: this.addNewMathItem }} />;
  }
}

受け取ったAddMathItemScreen側でタイトル、詳細のテキスト入力、追加リクエスト処理を記述します。追加が完了したらListScreenに遷移するようにAlertOKボタンをトリガーに設定します。見栄えに関して多少のスタイルも追加しています。

class AddMathItemScreen extends React.Component {
  constructor(props) {
    super(props);
    this.state = { title: '', detail: '' };
    this.handleOnPress = this.handleOnPress.bind(this);
  }

  handleOnPress() {
    const { title, detail } = this.state;
    if (!title) return Alert.alert('Error', 'titleは必須です');

    this.props.screenProps.addNewMathItem({ title, detail });
    Alert.alert(
      'Notice',
      '項目を追加しました!',
      [{ text: 'OK', onPress: () => this.props.navigation.navigate('List') }],
    );
    this.setState({ title: '', detail: '' });
  }

  render() {
    return (
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View style={styles.container}>
          <View style={styles.inputGroup}>
            <Text style={[styles.paragraph, styles.label]}>title</Text>
            <TextInput
              blurOnSubmit
              onChangeText={title => this.setState({ title })}
              style={[styles.textInput, styles.heading]}
              value={this.state.title}
            />
          </View>
          <View style={styles.inputGroup}>
            <Text style={[styles.paragraph, styles.label]}>detail</Text>
            <TextInput
              blurOnSubmit
              multiline
              onChangeText={detail => this.setState({ detail })}
              style={[styles.multiTextInput, styles.paragraph]}
              value={this.state.detail}
            />
          </View>
          <Button onPress={this.handleOnPress} title={'Add item to list'} />
        </View>
      </TouchableWithoutFeedback>
    );
  }
}


TabNavigator2

まとめると次のようになっています。

gist.github.com

Step3 - 項目削除機能の実装

項目追加機能同様、Appコンテナコンポーネントからstateをいじれる関数を渡します。削除の方法としては、配列のインデックスをキーに各詳細画面の削除ボタンから実行します(画面遷移で操作を限定させないと容易にバグりそうですね、理詰めでバグらせられそう)

削除メソッドを追加し、screenPropsで渡します。

export default class App extends React.Component {
...
  removeMathItem(index) {
    this.setState({
      mathematics: this.state.mathematics.filter((_, i) => i !== index),
    });
  }
...
  render() {
    /* screenPropsで各子供に渡している */
    return (
      <Tab
        screenProps={{
          mathematics: this.state.mathematics,
          addNewMathItem: this.addNewMathItem,
          removeMathItem: this.removeMathItem,
        }}
      />
    );
  }

次にDetailScreenのヘッダー右上に削除ボタンを設置する設定を書きます。indexListScreenからの遷移時にnavigation.state.params経由で渡すようにし、removeMathItemscreenProps経由で渡したのでそれらから取得できます。(ListScreenの変更は最後のまとまったもので確認できます)

DetailScreen.navigationOptions = ({ navigation, screenProps }) => ({
  headerRight: (
    <TouchableOpacity
      style={{ marginRight: 8 }}
      onPress={() => {
        Alert.alert(
          'Warning',
          '項目を削除しますか?',
          [
            {
              text: 'Delete',
              onPress: () => {
                screenProps.removeMathItem(navigation.state.params.index);
                navigation.goBack();
              },
            },
            { text: 'Cancel' },
          ],
        );
      }}
    >
      <Entypo size={24} name="trash" color={'red'} />
    </TouchableOpacity>
  ),
});

以上で削除機能が実装できました。コード汚いよ、コンポーネントに切り出せよ、と思いますが指針が決まらないのでとりあえず画面単位の分割に抑えています。

完成

完成しました。こんな感じで動いています。


TabNavigator3

gist.github.com

感想

引数の受け渡しはReduxとかでやりたいと思いました。なのでReduxない状態でのプロパティの渡し方は勉強不足です。すみません。

あと書き捨てのつもりだったのでgit管理していなくて、diffだけ見せるとかいう方法もあったかもという気がします。さらによくよく考えればgistの差分でもよかったです。記事が読みにくい感じですね。

React NativeのNavigatorがなくなった

ちょっと困った

SectionListが使いたいのでreact-nativeのバージョンをあげようと思ったら、 バージョン0.44にはNavigatorコンポーネントがないみたいであげられなくなっちゃいました。

なんか書いてある

Navigatorはお手軽だったので、結構使ってしまっていて他のライブラリで書き直すのはちょっと面倒です。

対策

応急処置としては指示通りにreact-native-deprecated-custom-componentsから使い、徐々に他のナビゲーションで書き換えていくつもりです。

感想

そういえば当時のドキュメントにNavigationExperimental的なコンポーネントが現れたりしてて、ナビゲーションに関して試行錯誤中な匂いはあった気がします。

これを機にナビゲーションを見直すのと、githubをwatchしていこうと思います。

追記

バージョンアップして試しにimport { Navigator } from 'react-native'してみたら、赤い画面で上の内容をディレクションしてくれますね。

動かなくなったりして迷うことはあまりないかもですね。

Reactコンポーネントをディレクトリにまとめる(ES6)

Reactのディレクトリ構成が全然わかりません。

Reactコンポーネントを分割する方法についてもメモです。ES6の文法の話だと思うのですが、正式なドキュメントが見つけられていないので間違いがあったら申し訳ないです。

書いたものをgithubに上げています。参考になれば幸いです。

webpackを使ってビルドを行なっており、設定ファイルは以下のようになっています;

  ...
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
          presets: ['es2015', 'stage-2', 'react']
        }
      }
    ]
  }

ディレクトリによる分割

例として以下のようにディレクトリを構成してみました。

src/
├── App
│   ├── App.js
│   ├── Bar.js
│   ├── Baz.js
│   ├── Foo.js
│   └── index.js
└── index.js

index.js内でimport App from './App'のようにモジュールとしてディレクトリを指定すると、ファイル./App/index.jsの内容がコンポーネントとして読み込まれます。./App/index.jsディレクトリ内のコンポーネントを組み立てておけば、呼び出し側はあたかも一つのコンポーネントのように扱うことができます。

ディレクトリでまとめることで、意味を保ったままコンポーネントの分割が行えそうです。「components」ディレクトリ以下にコンポーネントが大量に平置きされてしまうのを防げました。

感想

一方でコンポーネントの再利用性が下がる気もします。
一部のコンポーネントは他の箇所から再利用、他はディレクトリ内のコンポーネントを利用などしてしまうと、参照がわけわからなくなってしまいます。相変わらずディレクトリ構成は悩みどころです。
ちゃんと部品単位でのコンポーネント化を意識すると多少は回避できるかもしれません。