ushumpei’s blog

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

ページに目次をつけて、項目を押したらスクロールする

 ものすごい小さい話ですが、毎回忘れてしまうのでメモします。

$('html,body').animate({
  scrollTop: window.pageYOffset + target.getBoundingClientRect().top - 8 // 要素がブラウザ上部ぴったりになるので少し隙間をあけたり
}, 500, 'swing');

 まずwindow.pageYOffsetについて、これはwindow.scrollYエイリアスだそうです。ページの一番上を0として、どのくらい下にスクロールしているかのピクセル値を返します。

 次にelement.getBoundingClientRect().topですが、現在クライアントが見ている画面の上端を0として、そこからどれくらい要素が離れているかをピクセル値で返します。例えば要素をスクロールして通り過ぎていたら、負の値が返ってきます。

  つまり、ページの一番上から現在表示している画面上端までの距離現在表示している画面上端から要素までの距離を足すことでページの一番上から要素までの距離を計算しているということになります。(多分絵で描くとわかりやすい???)

ページの一番上から現在表示している画面上端までの距離

f:id:ushumpei:20170925210302p:plain

現在表示している画面上端から要素までの距離

f:id:ushumpei:20170925210314p:plain

まとめ

 多分覚えたのでもう忘れないはずです。ぶっちゃけていうとすでにここに書いてありました。。。

おまけ

 デモです。

See the Pen MEbNmj by ushumpei(@ushumpei) on CodePen.

一枚にまとめたやつ

<html>
  <head>
    <style type="text/css">
      .container {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
      }
      .menu {
        background-color: #eee;
        display: flex;
        flex-direction: column;
        flex: 1;
        height: 120px;
        justify-content: space-around;
        padding: 8px;
        position: sticky;
        top: 8px;
      }
      .content {
        display: flex;
        flex-direction: column;
        flex: 3;
        justify-content: space-between;
        margin-left: 8px;
      }
      .section {
        height: 100vh;
      }
      .section > h1 {
        background-color: #eee;
        padding: 8px;
        margin-top: 0;
      }
    </style>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script>
      window.addEventListener('load', function() {
        var menuItems = Array.from(document.getElementsByClassName('menu-item'));
        var sections =  Array.from(document.getElementsByClassName('section'));
        menuItems.forEach(function(item, i) {
          var target = sections[i];
          item.addEventListener('click', function() {
            $('html,body').animate({
              scrollTop: window.pageYOffset + target.getBoundingClientRect().top - 8 // 要素がブラウザ上部ぴったりになるので少し隙間をあけました
            }, 500, 'swing');
          });
        });
      });
    </script>
  </head>
  <body>
    <div class="container">
      <div class="menu">
        <a href="#" class="menu-item">section1</a>
        <a href="#" class="menu-item">section2</a>
        <a href="#" class="menu-item">section3</a>
        <a href="#" class="menu-item">section4</a>
        <a href="#" class="menu-item">section5</a>
      </div>
      <div class="content">
        <div class="section"><h1>section1</h1></div>
        <div class="section"><h1>section2</h1></div>
        <div class="section"><h1>section3</h1></div>
        <div class="section"><h1>section4</h1></div>
        <div class="section"><h1>section5</h1></div>
      </div>
    </div>
  </body>
</html>

google app scriptで背景色の置換

google app scriptで背景色の置換スクリプトを書いたのでメモします。以下の内容が含まれています。

  • 独自(「拡張ツール」)メニューの追加
  • モーダル(背景色置換モーダル)の表示
  • 初期値の挿入(テンプレートhtmlの使用)方法

概要

 主にgoogleのガイドを参考にして作成しました。とりあえずできることと、コードを記載します。

 できること;

  • スプレッドシート内での背景色の置換、置換実行モーダルの表示
  • 選択したセルの背景色を置換元としてデフォルト値として挿入

 そんなこといいから、という方は、スプレッドシートを開いて、「ツール」>「スクリプトエディタ」を選択し、スクリプトエディタのgsファイルにjavascriptコード、新規作成でhtmlファイルを作成しテンプレートファイル(index.html)を貼り付けてください。貼り付けたあとリロードするとメニューに「拡張ツール」が現れるかと思います。現れない場合はコメント頂けると幸いです。。。

f:id:ushumpei:20170925003319p:plain

 コード;

main.gs

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('拡張ツール')
    .addItem('背景色置換', 'showModal')
    .addToUi();
}

function showModal() {
  var background = SpreadsheetApp.getActiveSheet().getActiveCell().getBackground();

  var template = HtmlService
    .createTemplateFromFile('index.html');
  template.from = background;
  
  var html = template
    .evaluate()
    .setWidth(300)
    .setHeight(150)
    .setSandboxMode(HtmlService.SandboxMode.IFRAME);

  SpreadsheetApp.getUi().showModalDialog(html, 'Replace background color');
}

function replaceBackgroundColor(from, to) {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange();
  var backgrounds = data.getBackgrounds();
 
  var replacedCount = 0;
  for(var row = 0; row < backgrounds.length; row++) {
    for(var col = 0; col < backgrounds[row].length; col++) {
      if (backgrounds[row][col] != from) continue;
      sheet.getRange(row + 1, col + 1).setBackground(to);
      replacedCount++;
    }
  }

  ui = SpreadsheetApp.getUi();
  ui.alert('Successfully replaced!', replacedCount + ' cell\'s background color replaced to ' + to + ' from ' + from, ui.ButtonSet.OK);
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
    <script>
      function handleClick() {
        document.getElementById('replace').setAttribute("disabled","disabled");
        var form = document.getElementById('form');
        var from = form.querySelector('input[name="from"]').value;
        var to = form.querySelector('input[name="to"]').value;
        if (!from || !to) return showError('Input #from and #to');
        google.script.run
          .withSuccessHandler(closeModal)
          .withFailureHandler(showError)
          .replaceBackgroundColor(from, to);
      }
      
      function closeModal() {
        google.script.host.close();
      }
      
      function showError(message) {
        document.getElementById('error').innerHTML = 'Error: ' + message;
        document.getElementById('replace').removeAttribute("disabled");
      }
    </script>
    <style type="text/css">
      html, body, #form { height: 100%; }
    </style>
  </head>
  <body>
    <div id="form" style="display: flex; justify-content: space-between; flex-direction: column;">
      <div style="display: inline-flex; justify-content: flex-end">
        <label for="from" style="flex: 1">From: </label>
        <input id="from" type="text" name="from" placeholder="from" value="<?= from ?>" style="flex: 3">
      </div>
      <div style="display: inline-flex; justify-content: flex-end">
        <label for="to" style="flex: 1">To: </label>
        <input id="to" type="text" name="to" placeholder="to" style="flex: 3">
      </div>
      <button id="replace" onclick="handleClick()">Replace</button>
      <p id="error" style="color: red"></p>
    </div>
  </body>
</html>

解説

下準備

 onOpenスプレッドシートファイルを開いたときに実行する関数を指定できます。ここではスプレドシートのUIのAPIを呼び出して、メニューの追加と、メニュー項目と選択時の実行関数の登録を行なっています。

 メニュー項目が選択された際に実行される関数は、選択中のセルの背景色取得、テンプレートへの値挿入、モーダルの表示などです。対応するコードを以下に記載します;

 まず現在選択中のセル(光っているセル)の背景色を取得するためにgetActiveCellを使ってセルを取得します。そのあとにgetBackgroud#?????? 形式のカラーコードを取得し格納します。

  var background = SpreadsheetApp.getActiveSheet().getActiveCell().getBackground();

次に、作成済みのhtmlテンプレートを読み込み、プレースホルダfromに対し背景色を割り当てます。プレースホルダーの指定は、

<input id="from" type="text" name="from" placeholder="from" value="<?= from ?>" style="flex: 3">

のようにvalue=<?= form ?>となっていて、この仕組みを使って置換前の背景色のデフォルト値、として現在選択中のセルの背景色を割り当てます。参考

  var template = HtmlService
    .createTemplateFromFile('index.html');
  template.from = background;

最後にモーダルの表示処理を行います。

  var html = template
    .evaluate()
    .setWidth(300)
    .setHeight(150)
    .setSandboxMode(HtmlService.SandboxMode.IFRAME);

  SpreadsheetApp.getUi().showModalDialog(html, 'Replace background color');

 evaluateメソッドを呼び出すことで、テンプレートからhtmlオブジェクトを生成します、モーダルとして表示するためにサイズを設定し、実行方式をiframeに指定しました。実行方法に関しては他に選択肢があるようで、調べきれていないです。なので特に深い意味はありません、チュートリアルに沿って、iframeを指定しただけです。

モーダル

 モーダルの記載方法に関しては1ページのhtmlを書いて行く感じです。heightwidthが決まっている以外はwebと同じ感覚だと言えます。ただし、少し違うのは、スクリプトエディタで定義した関数を呼び出す方法があるということです。方法としては提供されているAPIを使用します;

      function handleClick() {
        document.getElementById('replace').setAttribute("disabled","disabled");
        var form = document.getElementById('form');
        var from = form.querySelector('input[name="from"]').value;
        var to = form.querySelector('input[name="to"]').value;
        if (!from || !to) return showError('Input #from and #to');
        google.script.run
          .withSuccessHandler(closeModal)
          .withFailureHandler(showError)
          .replaceBackgroundColor(from, to);
      }

 handleChlick関数はユーザーが置換実行する際に押下するボタンに紐づいている関数です。概要としては、表示されたモーダルに対象の背景色、変換後の背景色を入力し、「置換」を押下したら、シート内の全てのセルに対して、背景色の置換を行うためのものです。処理の流れとしては二回連続のクリック禁止、入力した値の取得、軽いバリデーション、google.script.runAPIを使用した背景色置換スクリプトの実行です。

 詳細が必要なのはgoogle.script.runかと思います。このAPIgoogle.script.run.hogehoge()と記載することで、スクリプトエディタで定義したhogehoge関数を呼び出すことができます。またここでは、実行の成功時(withSuccessHandler)、失敗時(withFailureHandler)にコールバックを指定しています。コールバック関数は index.htmlで定義したものが指定可能です。

 以下はモーダルに値を入力して実行した際に呼び出されるスクリプトです。

function replaceBackgroundColor(from, to) {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange();
  var backgrounds = data.getBackgrounds();
 
  var replacedCount = 0;
  for(var row = 0; row < backgrounds.length; row++) {
    for(var col = 0; col < backgrounds[row].length; col++) {
      if (backgrounds[row][col] != from) continue;
      sheet.getRange(row + 1, col + 1).setBackground(to);
      replacedCount++;
    }
  }

  ui = SpreadsheetApp.getUi();
  ui.alert('Successfully replaced!', replacedCount + ' cell\'s background color replaced to ' + to + ' from ' + from, ui.ButtonSet.OK);
}

 特に目新しいことはないかと思います。alertgetUiでUIから使用可能です。

感想

 まず不安に思ったこととしては、「それ標準機能であるよ」と言われることです。その辺どうなのでしょうか、、、たまにコード書きたい欲がまさってしまい、ちゃんとした検索を行わないという傾向があります。

 すごく感心したのが、google app scriptのモーダルでhtmlを表示できること、テンプレートを使用できることです。テンプレートを使用できるということは、webアプリケーションに近い何かを作れることだと思います。例えばSQLドライバーを作ってみたりすると楽しいかもです、スプレッドシートをデータベース、シートをテーブルと見立てて、とか思ったりします。

 javascriptでユーザーインプットを受け取る方法は、alertとかpromptとか柔軟性がなくて困ることがありました。webだと画面側をリッチにすればいいのですが、spreadsheetだとそうもいかないので、それを解消するためにガイドが役に立つのだと思います。

PhRUG(Philippine Ruby Users Group) September 2017 Meetupに行ってきた

 最近フィリピンに滞在しています。2017/09/21にフィリピンのRubyユーザーグループのイベントが行われるということで、海外勉強会ってどんな感じだろう?日本と違いがあったりするのかな?とか思い参加してきました。たくさん写真撮ってくるの忘れました。

 とりあえず結論、強く思ったのは、フィリピンでも日本でも、技術系の勉強会は大体同じ雰囲気、ということです。

概要

 イベントはmeetupから発見しました。PhRUG September 2017 Meetup  会場のホストはShield Foundryというベンチャー企業で、HPも綺麗な感じです、ただHPを見ても何を作っているかはよくわかりませんでした。場所はShield Foundryのオフィス、マニラのBGC(Bonifacio Global City)にあります。BGCにはいったことなかったですが、未来っぽい町でした。外資系企業がたくさんあるみたいで、金持ちが多いみたいな話です。

 余裕を持って会場へ向かったのですが、交通渋滞、ビルを間違える、などのアクシデントで、会場に着いたのは開始30分後くらいになってしまいました…

 会場のShield FoundryはいかにもITベンチャーといった感じでした。広さは3~40畳くらいで、オフィススペースとキッチンスペースに分かれています。オフィススペースには長机に沢山の椅子(アーロンチェア?)、各椅子の前の机にiMacとモニターがずらりと並んでいます。それぞれ漫画とか本とかも乗っていました。その他、バランスボールあり、サッカーボールあり、あとサッカーのアレ(二人でバーを回してボールを蹴り合うゲーム)もありました。テック系は世界共通なんだな、と少し安心しました。

 受付を済ませてプレゼンしているキッチンスペースに入ると、ビール手渡され、真ん中のダイニングテーブルにはピザが置いてあり、ご自由にどーぞといった感じでした。  

イベント

f:id:ushumpei:20170923211805j:plain

 最初の発表は、会場に到着したときは既に始まっていて、ちょっと詳細がわからなかったです…。発表者はエンジニアの一日、のようなスライドを表示していました。とりあえず自分はタガログ語が一切できないので、英語でプレゼンしていてホッとしました。ただ英語がそこまでできるわけでもないし、トピック不明だったのでなんとなく頷いたりして過ごしました。

 2番目の発表はrailsのパフォーマンス改善やテストなどの発表でした。bootsnap, PhantomJS, rack-mini-profiler, New Relic, その他諸々、をそれぞれいい感じに使って、計測、改善したよ!という感じです。そうですよね。納得な感じです。

 3番目の発表はjupyter/notebookを使って見た系の発表。マークダウン + コードでドキュメントかけるやつです。これ自体は先輩に教えてもらっていて知っていたのですが、バグレポートとかに使って、楽に可視化していこうよ!といった、やっぱりそう考えるよね的な共感を持ちました。

 4番目の発表は、開発Tipsの紹介でした。gitprecommit-hookでコミット前にrubocopしてます、とか。railsのサーバープロセスの優先順位あげたらなおった、とか。やっぱりコミット前にちゃんとしておきたいと思うよね!という再度、強い共感を持ったりしました。

感想

 会が終わった後にエンジニアの方にフィリピンのプログラミング言語事情について尋ねて見たところ、大企業とか、求人の多さで言えばjava, phpベンチャーruby, pythonといった感じです。scalaはとてもいい言語だと思うけど学習コストが高いのと、まだ事例をそこまで聞かない、とのことでした。日本では既に事例があるので、このあたりの情報共有とか何かできるかも(逆もあるだろうし。そういえばtwitterは?scalaの事例としてはどう捉えていたのだろう?国内事例がない、という感じなのだろうか?)。

 フロントエンドのjavascriptフレームワークに関しては、angular2, reactjavascriptフレームワークはリッチすぎて、本当に欲しいものがその中の1つだったりする、という日本でも割と聞く話を伺うことができました。ionicでネイティブを、とかいう話題もちらほら聞くようです。

 ここまで書いて、強いて挙げるならフィリピンと日本のIT界隈の相違点はなんなのだろうか?という方向に思考が向かいました。フィリピンの方が学歴に関して厳しいのかな?という予想を最近持っていますが、まだちゃんと議論したことがないので結論としては不十分と思います。さらに交流を重ねなければと思います。

 参加する前はブログを書くつもりがなかったため、少々微妙なレポートになってしまいました。来月もグループがあるそうなので、もし行くとなればもっといい感じにレポートしたいと思います。meetup便利です。

iOS11アップデートについていくためのSwift入門(主観)

 2017/09/20に、iOS11がリリースされました。今回のアップデートでは機械学習やSiriアプリ、ARなど様々な新機能が使えるようになりました。リンクを見ていて、「ちょっとSwift読めるようになっておかないと、おそらく半年くらいつまらなくなってしまう…上がってくるニュースを見ているだけになってしまう…」と思ったのがこの記事を書こうと思ったきっかけです。

 リファレンスとしてはAppleのサイト、The Swift Programming Language (Swift 4): A Swift Tourを使います。学習の目的はSwiftのコード(主に今後発表されるであろう「iOS11で追加された〇〇を使って見た系の記事のコード」)を読めるようになることです。

 Hello, world!を目標、と思ったのですが、ベタ書きで終わってしまいました。

print("Hello, world!")

概要

 リファレンスの内容は、Swiftを書き始めるのに十分な知識を得るためのツアーです。記事がSwiftのプロジェクトとしてまとめられていて、ダウンロードできるのでそれを使って学習していきます。

 セクションは次のようになっています;

  • Simple Values
  • Control Flow
  • Functions and Closures
  • Objects and Classes
  • Enumerations and Structures
  • Protocols and Extensions
  • Error Handling
  • Generics

 本記事ではリファレンス内に記載されている情報で、主に書き方に関することをメモしていきます。すでに序文の時点で以下の二点が記載されています;

  • グローバルスコープの処理は勝手に実行されるので、エントリーポイントとしてのmain関数が必要ない
  • 文末にセミコロンは必要ない

Simple Values

  • 変数宣言について、定数はlet、通常の変数はvarで宣言する: var hoge = 1とか。
  • 明示的な型の宣言はlet piyo: Double = 70 (後置型宣言)
  • \()で文字列に変数を埋め込める。"template \()"
  • 3ダブルクォート"""で複数行の文字列を囲むことができる
  • 配列リテラル["hoge", "piyo",]が使用可能
  • [“hoge”: 1, “piyo”: 2]辞書も可能
  • 空の辞書は[:]

Control Flow

  • if, switch, for-in, whileとかありますがこの辺は不思議なさそう
  • 型?でOptionalな変数を宣言できる(例var hoge: String? = "hoge")
  • if let hoge = { ... }でOptional変数から値を取り出すことができる、取り出すことができたらブロック内の処理を実行するという記法
  • if var piyo = { ... }ともかけるが、多分そんなに使わない
  • 取り出した変数のスコープはブロック内
  • その他にOptional変数を扱う方法としては、??を使って、defaultValue ?? optionalValueと言うように記述すると、値が取り出せる時のみ使用される
  • switchcasedefaultで構成していきます。breakは不要、defaultは必須です。
  • 0..<4で、0,1,2,3の範囲のレンジを作成できる

Functions and Closures

  • 関数定義はfunc、引数の定義はhoge: String(後置型宣言)、アロー->で戻り値の型を記述: func hoge(hoge: String) -> String { ... }
  • 引数を名前付きで渡すことができるhoge('fuga', piyo: 'giyo')これは定義時にfunc hoge(_ hoge: String, piyo fuga: String)と言うようにする。引数のラベルと言うもので、_はラベルなしを意味する。
  • 高階関数も作れる: (Int) -> Intとかで型を宣言
  • クロージャ使える:
{ (hoge: Int) -> Int in // クロージャの型と変数名
  return hoge * 2
}
  • 場合によっては型も省略できるらしいので、{ hg in ... }とかブラケットとinが出てきたらクロージャとして読む。引数も$0, $1と順番で記載されることがあるので、{ $0 < $1 }とか書かれていても混乱しないように。

Objects and Classes

  • クラスのインスタンス化はClassName()
  • フィールドはベタ書きでいいが、外部からアクセスするにはgetterメソッドが必要
  • イニシャライザー(コンストラクター)はinit、クラス内のメソッドでインスタンス自身を参照するにはself、デイニシャライザーはdeinit
  • 継承はclass SubClass: SuperClass { ... }
  • setter定義時のnewValueパラメーターは予約語
  • method?.hogemethodの戻り値があればhogeを呼び出すと言うことができる

Enumerations and Structures

  • enumはcaseで値を振っていく。ラベルから数値を取り出すのはrawValue、数値からラベルを作成するにはイニシャライザを使う(この時の値はOptional)。
  • structで構造体を定義。だいたいの機能はクラスと同じだが、構造体は常に値渡しされる。
  • class, struct, enumは似ている

Protocols and Extensions

  • protocolでインタフェースのようなものが定義できる
  • メソッドにmutating宣言することで、インスタンスプロパティを変更するメソッドを定義できる(クラスの時は不要、struct,enumの時だけ)
  • extensionで既存の型に機能拡張を行える、Protocolの機能を追加するのにも使える

Error Handling

  • do...catch構文でかく、怪しいところにtryをかく
  • try?でOptional値を取り出す

Generics

  • ジェネリクス<Item>で、Itemを変数のようにしようできる(数学でいう変数的な、x的な): var hoge = [Item]()Item型の配列を宣言とか。
  • where句もジェネリクス関連、親、子などの型の検証を記述できる

感想

 以上、自分がSwiftを読む際に詰まりそうなところをまとめ終えました。逆に言えば詰まらなそうなところは、私の主観の範囲で無視しています。

 Optional型の存在がちょっと気になりました。だいぶ面白そうです。またクラス、構造体、列挙型に関する扱いも違った見方を得られて興味深いです。自分としては列挙型はそこまで重要視していなかったのですが、Swiftをかくことでもっと本質をつかむことができるかもと、少し前のめりになりました。

 iOS`界隈の盛り上がりに少しでもついていきたいと思った次第です。

文法の勉強のため簡単なObjective-CのコードをXcodeで実行してみた

 前回、React Nativeのソースコードを読もうとして挫折したので、基礎文法を勉強するためにObjective-Cで何か書いて見ます。

 その前にObjective-Cwikiを少し読んで感じをつかみます。とりあえず読むために必要そうなこと3つです。

  1. C言語としてもいける、コンパイラディレクティブを駆使する
  2. メッセージ式 [Object method:arg1:arg2]でメソッド呼び出し
  3. クラス定義は定義部(.h)と実装部(.m)に分かれている

書いて見る

環境: Xcode: Version 8.3.3 (8E3004b)

 ではとりあえずhello worldします。別にアプリケーションが書きたいわけではないので、コマンドラインツールプロジェクトとして書きます(もっと言えばコマンドラインツールが書きたいわけではないですが、実行の構成方法が不明だったため一旦これでいかせていただきます)

 まずXcodeを起動してFile > New > Projectを選択します。モーダルが表示されるので、macOSタブのCommand Line Toolを選択しNextを押します。名前などは自由に設定します、ただし使用言語はObjective-Cにして、初期設定を終えます。

f:id:ushumpei:20170919225414p:plain

 次にコードを書いていきます。すでにmain.m(エントリーポイントとなるファイル)が生成済みだったので、そこに処理を記述していきます。クラスの定義の練習をするためにHelloクラスを作成しましたのでそれを呼び出します。(@コンパイラディレクティブの他にもリテラルを使用するときの接頭辞として使う、いやおそらくコンパイラディレクティブでリテラルを変換している?)

  • main.m
#import <Foundation/Foundation.h>
#import "Hello.h"

int main(int argc, const char* argv[]) {
    @autoreleasepool {
        Hello* object = [[Hello alloc] init];
        NSString* message = @"Hello world!";
        [object setMessage:message];

        [object say];
        NSLog(@"[object message]: %@", [object message]);
    }
    return 0;
}

 次にHelloクラスをNew > FileからmacOSタブのCocoa Classを選択します。こうするとHello.hHello.mファイルが生成されます。内容を記述していきます。以下を定義しています、

  • 挨拶文を保持するmessage変数
  • 挨拶文を取得するmessage関数
  • 挨拶文を格納するsetMessage関数
  • 挨拶文をコンソールに出力するsay関数

です。

  • Hello.h
#import <Foundation/Foundation.h>

@interface Hello : NSObject {
    NSString* message;
}

-(NSString*) message;
-(void) setMessage: (NSString*) s;
-(void) say;

@end
  • Hello.m
#import "Hello.h"

@implementation Hello

-(NSString*) message {
    return message;
}

-(void) setMessage: (NSString*) s {
    message = s;
}

-(void) say {
    NSLog(@"say: %@", message);
}

@end

動かして見る

 Xcodeのウィンドウ上部メニュー左辺りにある▶︎(Runアイコン)を押してコードを実行します。

f:id:ushumpei:20170919230326p:plain

感想

 コンパイラディレクティブをちゃんと覚えていけばある程度読めるようになると思いました。@autoreleasepoolGCということでいいのかな?いやちゃんと覚えないといけないですね。とりあえず公式ドキュメントをこの辺から探して見ます。

 書けるようになる必要があるか?という問いに関しては一瞬、「まあでもSwiftあるし、、、」とか思いましたが、まだまだ使われているため、やっておいて損はなさそうな気がします。

React Nativeの画像遅延読み込み(ライブラリのソースを読んで見る)

 この記事を要約すると、「画像遅延読み込みの方法が知りたくて、ライブラリのソースコードを読んで、Objective-Cのコードにたどり着いて、次に進めなくなってしまい一旦諦めたけれど、これを糧にもっと勉強しようという気になった」という自己満話です。

概要

 少し前までWebサービスのユーザーのインターネット回線速度をそこまで強く意識したことがありませんでした。

 もちろんパフォーマンス計測はしていましたが、サーバー側の処理を改善することで対処していて、あくまで一般的なユーザーは回線がある程度の速度であることを仮定していましたし、回線が弱すぎるのはユーザー側の問題としている部分がありました。

 この回線の強弱に関する考え方が私が感じた、モバイルアプリとWebの大きな違いでした。

 モバイルでは回線が弱くなることが容易に起こりうるし、そのせいでアプリが操作不能になってしまうことは、ユーザーにとってストレスだと思います(操作不能にする方がいいケースもあると思います)。いや、といよりモバイルから閲覧するWebサービスとアプリとの違いでいうと、アプリだとオフライン状態でも使えるように作れる部分があるので、極力そうしていった方が親切だし、ユーザーも安心だよね、という話です。

 多分モバイルアプリ開発者の方々からすると当然すぎることだと思うのですが、私がこのことに気がつくためには、いつまでたっても画像が読み込まれないまま動かない、という経験が必要でした。(頭が悪いです)

読んでみる

 そんなこともあり遅延読み込みのライブライreact-native-image-progressとかに触れたのですが、そもそもどうやって遅延読み込みを実現しているか、ソースを読んでみようというのが今回の内容です。

 とりあえず一番はじめのリリース0.1.0を読む;

  • 基本的にはreact-nativeImageコンポーネント
  • Imageの読み込み開始、進捗、終了イベントハンドラにコールバックを登録している
  • Imageのprogressイベントについて苦労したんだろうなと感じられる
  • 遅延読み込み方法はImageのコードを読まなければわからない

 ということなのでreact-nativeImageコンポーネントのソースを読む;

  • RCTImageViewを読むべきとわかる

 ここからObjective-Cです、RCTImageView.mを読む;

  • RCTDirectEventBlockで詰まる…

すみません、詰まりました。

感想

 知らないことがたくさん見つかりましたし、いかに色々なことを知らないままで使っていたかわかりました。予想として遅延読み込みに関してはjavascriptfetchメソッドとかで実装されてて、簡単に読めないかな、とか思って軽く記事を書き始めたのですが、普通にネイティブでした。

 結局わからずじまいになってしまい、かなりダメダメなので、しばらくいろんなソースを読んでみることにすると思います。Objective-Cの基本的な構文も抑えて今回の詰まったところを解消したいです。あとReact Nativeの仕組みもちゃんと知りたくなりました。

 余談ですがFacebookのライセンスの話、どう捉えるか考えなければ。。。

ハノイの塔

なんだか久しぶりにjavascriptハノイの塔を解いて見ました。以下の関数は、number枚のハノイの塔をtime回操作した時の状態を計算するものです。特に目新しいものではないと思います。

const hanoiSnapshot = (number, time) => {
  // 円盤の移動が完了した以降の操作はエラーを表示(良いやり方かどうかは悩む)
  if (time > Math.pow(2, number) - 1) throw new Error(`${Math.pow(2, number) - 1}回目の操作時点ですでに塔は完成しています`);

  // 三本柱の配列データを定義
  hanoi = [[], [], []];

  // 1~number番目の円盤それぞれの場所を計算して柱に格納して行く
  for (let i = 1; i <= number; i++) {

    // 移動する方向
    const direction = Math.pow(-1, number + i + 1);

    // 移動した回数
    const moveCount = Math.floor((Math.pow(2, i - 1) + time) / Math.pow(2, i));

    // 1番目の柱を位置0とした時の円盤の絶対位置
    const absolutePosition = moveCount % 3;

    // 方向を加味して場所を0~2に正規化して決定
    const position = (direction * absolutePosition < 0) ? (3 + direction * absolutePosition) % 3 : absolutePosition;

    // 柱に円盤を追加する
    hanoi[position].unshift(i);
  }

  return hanoi;
}

感想

イメージとしては円盤は方向と速度を持っていて、3つの柱を移動しつつ、くるくる回って行る感じです。数学で三乗根の乗算を考えるときに書くような図をイメージするとわかりやすい気がします。

wgetでサイズ確認

ものすごく小さな話かつググったら速攻出ますが、自分にとっては切実な問題なのでメモします。

結論から言うと--spiderオプションです。

$ wget --spider ***URL.zip***

Spider mode enabled. Check if remote file exists.
--2017-08-31 09:49:43--  ***URL.zip***
...
Length: 11121426 (11M) [application/zip]
Remote file exists.

補足(curl)

たまにwget入ってないよ、となる時があるのでcurlの場合も追記します。普通にヘッダ取得しているだけです。

$ curl --head ***URL.zip***

HTTP/1.1 200 OK
...
Content-Length: 11121426
...

(単位byte)

ちなみにファイルのダウンロード自体はcurl -O ***URL.zip***なのですね。勉強不足です。

感想

一日の通信量上限のあるネット環境で生活しているため、迂闊に大きいファイルをダウンロードすると終わってしまう、と言う背景です。

URLの拡張子がzipな理由は特にないです。

辞書.appのデータ構造の補足

注意: 結論としては補足になっていません。。。RELAX NGXMLスキーマここで勉強した際のメモ書き程度になっています

先日書いた辞書.appの記事ではあまり辞書を作ることに関して調べきれていなかったところがあるので追記します。先日の記事でも単語と意味を登録することはできましたが、MyDictionary.xmlの構造(辞書のデータ構造)について補足していきたいと思います。

MyDictionary.xmlの構造(辞書のデータ構造)

MyDictionary.xmlRELAX NGというxml schema言語で記述されています。xmlに明るくないためやや中途半端な説明になりますが、RELAX NGは外部のデータセットを使用する前提で使われることが大半のようです(参考)

xmlなのでデータを定義するために、簡単な方法を提供してくれているそうです。例えば子要素はelement、属性はattributeなどがあり、それらに対し1つ以上持つ<oneOrMore>、つけてもつけなくても<optional>、どちらか一方だけ<choice>、などの制約をツリーとして書いていくことができます。私のイメージではタグを定義して作っていく感じです。

まず最初の方の行を見ていきます。

<?xml version="1.0" encoding="UTF-8"?>
<d:dictionary xmlns="http://www.w3.org/1999/xhtml" xmlns:d="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
...
</d:dictionary>

ここでは、2つのネームスペース、xmlhttp://www.apple.com/DTDs/DictionaryService-1.0.rngのを宣言しています。またxmlns:dの部分で、d:というプレフィックスを宣言しているため、データセットを使用する際は語頭にd:をつけて使用していくことになります。(ようやくd:entryなどd:をつけて記述していた意味がわかりました)

続いてデータセットを見てどんな定義があるのかを見ていきます。内容は/Dictionary Development Kit/documents/DictionarySchema/modules/dict-struct.rngに記載されております。(http://www.apple.com/DTDs/DictionaryService-1.0.rngはアクセスしても404でちょっと分かっていません。ネームスペースとしてだけ使っているのでしょうか?)

webで公開されているものもあるので、適宜そちらも参照ください。

まず初めに<grammer>が全体を囲っていることから、Named patternsと呼ばれるタグを分割して定義できるようにするパターンで記述していることがわかります(リンク先の セクション4. Named patterns)。

<grammer>は、1つの<start>と複数の<define>を持ちます。<define name="hoge">と定義したものは、<ref name="hoge">により参照することができるため、定義を組み合わせて最終的な<start>を組み立てていく形になります。

ここで定義されている主なものを見てみましょう;

dictionary

<define name="dictionary">
    <element name="dictionary" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
        <zeroOrMore>
            <ref name="style"/>
        </zeroOrMore>
        <ref name="dictionary.attlist"/>
        <oneOrMore>
            <ref name="entry"/>
        </oneOrMore>
    </element>
</define>

dictionaryの定義は、

  • 実態はdictionaryタグ。アップルのネームスペースを属性として指定している。(ネームスペースの必要性の理解が曖昧です。名前が一般的なので、重複しないようにという慣習的なことでしょうか?それかプレフィックスを共通化するためとか関係している?)
    • タグは0個以上のstyleを使用できる
    • タグは1個以上のentryを使用できる
    • タグはdictionary.attlist属性リストを使用できる
      • dictionary.attlistはXTHMLのバージョンと国際化指定を属性として持っているようです

refでは参照先がelementなのかattributeなのかわからないため、どう表現したら適切か悩みます(おそらく言語で表現しにくいからこそ、この記法が使われているのでしょうが)。

entry

<define name="entry">
    <element name="entry" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
        <ref name="entry.attlist"/>
        <zeroOrMore>
            <ref name="index"/>
        </zeroOrMore>
        <ref name="Flow.model"/>
    </element>
</define>

entryの定義は、

  • 実態はentryタグ。
    • 0個以上のindexが使用可
    • Flow.model(なんだこれ。内部にhtml書けることと関係している?)が使用可
    • entry.attlistが使用可
      • entry.attlistid.attrib属性が使用可能(<index id="hoge"とか書ける)
      • parental-control属性が使用可能(任意)
      • title属性が使用可能(任意) <- (なぜなくてもいいのだろう?)

index

<define name="index">
    <element name="index" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
        <ref name="index.attlist"/>
    </element>
</define>

<define name="index.attlist">
    <attribute name="value" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
        <text/>
    </attribute>
    <optional>
        <attribute name="title" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
            <text/>
        </attribute>
    </optional>
    <optional>
        <ref name="parental-control.attrib"/>
    </optional>
    <optional>
        <ref name="priority.attrib"/>
    </optional>
    <optional>
        <attribute name="anchor" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
            <text/>
        </attribute>
    </optional>
    <optional>
        <attribute name="yomi" ns="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
            <text/>
        </attribute>
    </optional>
</define>

indexの定義は

  • 実態はindexタグで、属性としてvalue、オプションの属性としてtitleparental-controlpriorityanchoryomiなどが使用可能

といった形になっています。

つまり?

つまり今のところ、そういうデータ構造になっているということがここを読むとわかるようになったというだけのことでした。。。

これらのデータ構造が辞書データとして取り込まれた時に実際どのように扱われるかをみていく必要があり、補足し切れていないです。

データ構造の読み方はわかった、ならばタグとしてどのような形をしているか、その子要素、属性はどのように扱われるか、それらのことを次にまとめていきたいと思います。

外国語学習のためにMacの辞書.appに追加する辞書を作成する

概要

Macでは設定すると三本指タップで単語の辞書検索が行えます、文章書いている時や読んでる時に作業を中断せずに意味を調べられるのですごく便利に使わせていただいています。

最近フィリピンで生活しているので、タガログ語を読み書きすることがたまにあります。わからない単語を調べるために辞書.appに追加する辞書ファイル(.dictionary)が欲しかったのですが、ネットを探しても見当たりませんでした。

html形式の辞書を見つけたので、Appleの公式ガイドライン(Dictionary Services Programming Guide)から辞書ファイルを作成してみました。その時の手順をメモしておきます。

注意: 以下の手順を行うにはApple Developer Programへの登録が必要です

Dictionary Development Kitのダウンロード

ガイドラインの指示に従い、Downloads for Apple DevelopersからAuxiliary Tools for Xcode - February 2012を検索してダウンロードします。

ダウンロードしたdmgを開くと、Dictionary Development Kitディレクトリがあるので、/Developer/Extras/以下にコピーします。その後、その中にあるproject_templateを自分用の辞書に書き換えていきます。(テンプレートなのでコピーとかして使っていきます)

テンプレートの中身はこんな感じです。

.
├── Makefile
├── MyDictionary.css
├── MyDictionary.xml
├── MyInfo.plist
└── OtherResources
    ├── Images
    │   └── dictionary.png
    ├── MyDictionary.xsl
    └── MyDictionary_prefs.html

2 directories, 7 files

バリデーションツール: jing.jar

辞書ファイルはxmlで、RELAX NGという記法で作成します。xmlフォーマットバリデーションのためjingを、githubから落としてきてビルドしておきます。

$ git clone git@github.com:relaxng/jing-trang.git
$ cd jing-trang
$ ./ant

javaがいります。jdkインストールしてJAVA_HOMEの設定もしておきましょう。

辞書の作成

MyInfo.plistの編集

製作者などのメタ情報を記載していきます。特に配布の予定はないのでざっと変更したくらいです。各キーの説明はCreating Dictionaries > Table 2-1を参照ください。

一旦make

まだ辞書の内容はテンプレートのままですが、完成イメージを見たいのでとりあえずmake & make installしてみます。

$ make
"""/Developer/Extras/Dictionary Development Kit"/bin"/build_dict.sh"  "My Tagalog English Dictionary" MyDictionary.xml MyDictionary.css MyInfo.plist
- Building My Tagalog English Dictionary.dictionary.
- Cleaning objects directory.
- Preparing dictionary template.
- Preprocessing dictionary sources.
- Extracting index data.
- Preparing dictionary bundle.
- Adding body data.
- Preparing index data.
- Building key_text index.
- Building reference index.
- Fixing dictionary property.
- Copying CSS.
- Copying other resources.
- Finished building ./objects/My Tagalog English Dictionary.dictionary.
echo "Done."
Done.

$ make install
echo "Installing into ~/Library/Dictionaries".
Installing into ~/Library/Dictionaries.
mkdir -p ~/Library/Dictionaries
ditto --noextattr --norsrc ./objects/"My Tagalog English Dictionary".dictionary  ~/Library/Dictionaries/"My Tagalog English Dictionary".dictionary
touch ~/Library/Dictionaries
echo "Done."
Done.
echo "To test the new dictionary, try Dictionary.app."
To test the new dictionary, try Dictionary.app.

辞書.appを開き、環境設定から自分の辞書を選べるようになっているので、探してチェックを入れます。これで三本指タップで自分の辞書が表示されるようになりました。

後は辞書の内容をちゃんと書いていくだけです。

MyDictionary.xmlの編集

今回は簡単な辞書を作成していきます。タガログ語から英語の翻訳はwebからスクレイピングしました。

単語の定義ファイルは以下のようになりました

<?xml version="1.0" encoding="UTF-8"?>
<!--
   This is a sample dictionary source file.
   It can be built using Dictionary Development Kit.
-->
<d:dictionary xmlns="http://www.w3.org/1999/xhtml" xmlns:d="http://www.apple.com/DTDs/DictionaryService-1.0.rng">
<d:entry id="dictionary_application" d:title="Dictionary application">
    <d:index d:value="Dictionary application"/>
    <h1>Dictionary application </h1>
    <p>
        An application to look up dictionary on Mac OS X.<br/>
    </p>
    <span class="column">
        The Dictionary application first appeared in Tiger.
    </span>
    <span class="picture">
        It's application icon looks like below.<br/>
        <img src="Images/dictionary.png" alt="Dictionary.app Icon"/>
    </span>
</d:entry>
...
<!-- 単語ごとにd:entryタグを作っていきます -->
<d:entry id="一意な値" d:title="単語名">
  <d:index d:value="検索キー"/>
  <h1>Hoge</h1> <!-- 単語のの内容、htmlかけます -->
</d:entry>
...
</d:entry>
</d:dictionary>
  • htmlMyDictionary.cssマークアップ可能です
  • OtherResourcesディレクトリにアセット、例えばOtherResources/hoge/image1.pngを入れておけばhoge/image1.pngで参照できます

以上です。

感想

ものすごい必要最小限のものができました。 単語間のリンクとかうまくいっていない…今度調べないと…

何よりも辞書を作る作業がとても面倒でした。著作権フリーのものを使って、.dictionary形式で配布したほうがいいかもしれません。というか絶対どこかに「タガログ -> 英語」とか「タガログ -> 日本語」あると思うのですが、なんか見つからないです…

あとhtmlから抜いてきたのも失敗でした、他の辞書フォーマットから変換する処理を書いた方が楽だったかと思います。

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段にするとか?

追記 2018/05/07: SwitchNavigator 使うといいかもしれないです

準備

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

$ 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」ディレクトリ以下にコンポーネントが大量に平置きされてしまうのを防げました。

感想

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