前書き
ReactNativeが流行ってるし触ってみたいなと思ったけど、React分かんないしどうしようかな、Vue.jsで実装できないかなと探していたら、Vue.js公式サイトでこんな文言を発見
Vueの開発者がもうすぐ利用できるもう1つの選択肢は、NativeScriptです。コミュニティドリブンのプラグインを介して使用することができます。
出典: https://jp.vuejs.org/v2/guide/comparison.html#ネイティブレンダリング
そして探してみたらnativescript-vueなるものも発見。Vue.jsでもネイティブアプリを開発できそうな匂いがしたので、実装をしてみることにしました。
開発環境
- MacOS High Sierra
 - Xcode v9.0
 - npm
 
準備
npmはHomebrewなどであらかじめ最新版を導入しておいてください。(当環境ではv5.5.1)
- nativescript
 
$ npm install -g nativescript
インストールの最中に2回ほど質問をされる場合がありますが、前者はNo, 後者はYesを選択して下さい。
(前者はインストール後に公式サイトを訪れるか、後者は環境の構築もするか、みたいな質問だったと記憶しています)
また、実行時にEACCESエラーがでたらsudo付けて再実行して下さい。
$ tns
第1段階として、まずはこのコマンドが使えるようになっていればOKです。
次にVue.jsとNativeScriptのプロジェクトを作成します。
適当なディレクトリに移動し、以下のコマンドを打ちます。
$ tns create todo-app --template nativescript-vue-rollup-template
$ cd todo-app
デフォルトのnativescript-vueでは.vueファイルやES2015をサポートしていないので、今回は有志の方が作成しているテンプレートを用いています。
これで環境構築はできたので、試しに動かしてみましょう。
(今回はiosで動かしますが、Androidの方が良い!という方は適宜変えて貰って大丈夫です)
作成したアプリケーションのルートディレクトリにおいて、
$ rollup -c -w & cd tns;tns run ios --clean --emulator
これを打ち込むとエミュレータが起動します。
もし『エミュレータが見つからないよ!』みたいなエラーが出た場合には、エミュレータの起動を待った後にアプリケーションのルートディレクトリに移動し、再度コマンドを実行してみて下さい。
この画面がでてきたら準備はOKです
実装
今回はお試しとして、簡素なTodoアプリを実装してみます。
app/componentsディレクトリにTodo.vueという名前のファイルを作成し、
そのファイルと同ディレクトリ内のApp.vueを以下のように書き換えます。
<template>
</template>
<script>
export default {}
</script>
<style lang="scss">
</style>
<template>
<page>
  <action-bar title="Todoリスト"></action-bar>
  <todo></todo>
</page>
</template>
<script>
import Todo from './Todo.vue'
export default {
  components: {
    Todo,
  }
}
</script>
1. ローカルストレージの準備
最初にデータの保存場所を作っていきましょう。
Webであればローカルストレージが使えますが、スマホであるのかな?と探してみると、以下の物を発見。
GitHub - NathanaelA/nativescript-localstorage: NativeScript
今回はコレを使います。
$ tns plugin add nativescript-localstorage
<script>
import localStorage from 'nativescript-localstorage'
const LOCAL_STORAGE_KEY = 'todos-localstorage'
const todoLocalStorage = {
  fetch: () => {
    const todos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '[]')
    todos.forEach((todo, index) => {
      todo.id = index
    })
    return todos
  },
  store: (todos) => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
  },
}
</script>
これでTodoの一覧取得と保存ができるようになりました。
2. Todoの追加、削除、完了処理の実装
次いでTodoの新規追加と削除、完了の処理を実装していきましょう!
Todo.vueのscriptタグ内に以下を追加します。
export default {
  data() {
    return {
      todos: [],
      newTodoTitle: '',
    }
  },
  created: function() {
    this.todos = todoLocalStorage.fetch()
  },
  watch: {
    todos: {
      handler: function(todos) {
        todoLocalStorage.store(todos)
      },
      deep: true
    }
  },
  methods: {
    addTodo: function() {
      if (!this.newTodoTitle) return
      this.todos.push({
        id: this.todos.length + 1,
        title: this.newTodoTitle,
        done: false
      })
      this.newTodoTitle = ''
    },
    deleteTodo: function(targetTodo) {
      this.todos.splice(this.todos.findIndex(function(todo) {
        return todo.id === targetTodo.id
      }), 1)
    },
    toggleDoneState: function(targetTodo) {
      const todoIndex = this.todos.findIndex(function(todo) {
        return todo.id === targetTodo.id
      })
      this.todos[todoIndex].done = !targetTodo.done
    }
  }
}
Vue.jsは公式ドキュメントが日本語でサクサク読めることと、コンポーネント内にcssやhtmlも全て纏めて記述できるところが良いですね!
ちなみにtodoの削除と完了状態を変更させる関数内でArrayのindexOf()に似た処理を自前で用意しているのは、this.todos.indexOf(todo)だと正常なインデックスを返さないことが理由です。
実はこれの原因がいまいち分かっていなくて、もしご存知の方がおられましたらご教示頂けますと幸甚です。
うーん、実は参照渡しでなく値渡しをしていて、それが厳格な比較に引っかかっているとかなのだろうか。
3. ビューの実装
バックの実装は完了したので、最後に見える部分の実装をしましょう!
<template>
<grid-layout rows="*,80">
  <scroll-view row="0">
    <list-view :items="todos" class="list-group">
      <template scope="todo">
          <grid-layout row="1" columns="*, 60">
            <label :text="todo.title" textWrap="true" :class="{done: todo.done}" @tap="toggleDoneState(todo)"></label>
            <button col="1" text="削除" @tap="deleteTodo(todo)"></button>
          </grid-layout>
        </template>
    </list-view>
  </scroll-view>
  <grid-layout row="1" columns="*, 60" class="footer">
    <text-field col="0" hint="今日は何をしますか?" v-model="newTodoTitle"></text-field>
    <button col="1" text="追加" @tap="addTodo()"></button>
  </grid-layout>
</grid-layout>
</template>
<script>
...
</script>
<style lang="scss" scoped>
.done {
    text-decoration: line-through;
}
.footer {
    padding: 40;
    background-color: #fafafa;
}
</style>
コレに限らず甘い実装が多いですが、お許しを・・・
普段Webでhtmlを書いている方は気づいたかと思いますが、上のテンプレート内に見慣れないタグがたくさんありますね。
そう、Webで用いられるhtmlのタグは基本的に使うことが出来ません。
なのでNativeScriptが用意する、androidやiosの各コンポーネントに対応した独自のタグを使うことになります。
おそらく当たり前のことなんだと思いますが、僕はそれに気づかず何時間も悩んでいました・・
完了の切り替えに関しては、Webであればチェックボックスを用いるのが一般的かと思いますが、NativeScriptでもネイティブでもチェックボックスはサポートされていないようです。
なので今回は簡潔に、Todoをタップするとタスクの完了状態が切り替わるようにしました。
本来であればチェックボックスの画像(チェック済みのものと、されてないものの2種類)を用意して、タップするたびに画像を切り替えて擬似的に実装するのが好ましいかと思います。
4. ビューがキーボードに追従するように(オプション)
動かせるTodoアプリはこれで完成しましたが、今のままだとTodoリストに追加する際に、キーボードが入力欄を隠してしまうため不便ですね。
そんな時にとても便利なプラグインが!
GitHub - tjvantoll/nativescript-IQKeyboardManager:
$ tns plugin add nativescript-iqkeyboardmanager
入れるだけでviewがキーボードに追従するようにしてくれるのでとても便利です。
完成!
見た目はアレですが、一応動かせるアプリを作ることが出来ました
開発においては多少ネイティブの知見を必要とする部分もありましたが、Web開発の要領でスマホのアプリも作れるというのは凄く感動しますね。
最後になりますが、実装において甘い部分が多く申し訳ありません。
もし何かご意見や不備・修正点等ございましたら直ぐに対応いたしますので、お気軽にご連絡下さい。
また、今回開発したコードは以下のリポジトリに置いてありますので、もし宜しければご参照下さい。
https://github.com/sugoikondo/nativescript-vue-test-app
ここまでお読み頂き、ありがとうございました!
参考・出典など
- 他のフレームワークとの比較 — Vue.js
 - rigor789/nativescript-vue
 - Installation - NativeScript docs
 - tralves/nativescript-vue-rollup-template
 - GitHub - NathanaelA/nativescript-localstorage: NativeScript
 - はじめに — Vue.js
 - nativescript-vue/samples at master · rigor789/nativescript-vue
 - tralves/groceries-ns-vue
 - GitHub - tjvantoll/nativescript-IQKeyboardManager: