_ NativeScriptのiOS対応時に気をつけること
基本的にはAndroidで開発を行っているのですが、ある程度出来てからiOSで動かしたときに期待通り動かなかった点を中心に、双方のプラットフォームで動かすために気をつけるべき点を五月雨的に書いていきます(随時追記する予定)。
基本的には深く根本原因まで探って対処するというより、「こうしてみたら動いた」的な内容になっていますので、理屈から理解したい方には不向きかも知れません。
_ WebView, HtmlViewの初期高さが1行
まずAndroidとiOSの大きな違いはViewの高さのデフォルト状態です。Androidは基本的に領域一杯に広がって配置されますが、iOSは最小範囲に留まって配置されるようです。このため、WebViewやHtmlViewを画面一杯に表示してスクロールさせたいような場合は、高さを指定する必要があります。
1
2
3
4
5
| <GridLayout>
<ScrollView>
<WebView height="100%" class="m-10" src="{{ description }}" verticalAlignment="top" />
</ScrollView>
</GridLayout>
|
この方法は画面内に配置されているViewがWebViewやHtmlView1つだけの場合は有効ですが、上下に他のViewが配置されていると都合が良くありません。何故ならWebView、HtmlViewの部分のみスクロールされてしまい、画面全体のスクロールにならないからです。
対応策は次々項に書きます(無駄に引っ張る(笑))。
_ ページが表示されない
どうやら、<StackLayout>は中にListViewなどの高さが変化するViewを入れるのには不向きで、そういう場合は<GridLayout>を使うと良いようです。
特にRadListViewの時は動きが不可解で、最初上部20%程度しか表示されず、その狭い範囲をスクロールすると徐々に見えるエリアが広がっていきます。こういう場合、外側にGridLayoutを配置すると最初から全体が表示されるようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <Page class="page"
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:lv="nativescript-ui-listview"
navigatingTo="onNavigatingTo">
<StackLayout>
:
<lv:RadListView
:
<lv:RadListView.itemTemplate>
:
</lv:RadListView.itemTemplate>
</lv:RadListView>
</StackLayout>
</Page>
|
↓
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <Page class="page"
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:lv="nativescript-ui-listview"
navigatingTo="onNavigatingTo">
<GridLayout>
:
<lv:RadListView
:
<lv:RadListView.itemTemplate>
:
</lv:RadListView.itemTemplate>
</lv:RadListView>
</GridLayout>
</Page>
|
※ Androidではどちらでも同じように表示されるがiOSでは下でないと期待通り表示されない。
_ ページがスクロールしない
WebViewやHtmlViewは初期配置時に高さが1行分しか確保されません。ところが画面を回転させるとちゃんと全部表示されるようになります[*1]。つまり、描画後に再描画してあげれば良いわけです。
HtmlViewにidを振っておきます。
1
| <HtmlView id="htmlview" class="m-l-2 m-r-2" html="{{ description }}" />
|
ページ表示後、htmlviewを再描画します。
1
2
3
| const page = args.object.page;
const htmlview = page.getViewById("htmlview");
htmlview.requestLayout();
|
_ ボタンの高さが低い(ボタンが細くなる)
Androidでは以下のように誤った配置をしてもボタンのCSSを指定してあればそちらが優先されて高さが確保されますが、iOSでは与えられた領域内でのみ高さを確保しようとするようです。
1
2
3
4
5
| <GridLayout rows="40,5,40,5,40,5,40,5,40" columns="80,*,40" >
:
<Button row="8" colSpan="3" class="btn_search btn btn-primary btn-red btn-rounded-lg btn-active"
text="検索" tap="onSearchButtonTap"/>
</GridLayout>
|
1
|
| .btn_search { margin-top: 24px; }
|
(GridLayoutで高さ40が指定されているがCSSでmarginが24なのでボタン高さが16しか無い)
この場合、GridLayoutの高さを適切にするか、おまかせにしたい場合はautoにします。
1
| <GridLayout rows="40,5,40,5,40,5,40,5,auto" columns="80,*,40" >
|
_ Toastが画面下部に固定されているタブに隠れて見えない
Toastを表示させようとして最初に見つけたnativescript-toastを使っていたのですが、表示位置が画面下部に固定されてしまい、iOSの場合はタブ位置がbottom固定なのでかぶって見えなくなってしまいます。また、このnativescript-toastは表示位置の指定ができません。
というわけで私はnativescript-toastyに乗り換えました。(公式的にはこれ?(nativescript-toasts[*2]))
以下のように、使い勝手はnativescript-toastと同等にし、platformによって位置を自動で変えてくれるようにしています。(ちょっとお行儀悪いかも)
[toast.js]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| const application = require("tns-core-modules/application");
const Toasty = require("nativescript-toasty");
function toast() {
if (!(this instanceof toast)) {
return new toast();
}
}
toast.prototype = {
text: "",
duration: Toasty.ToastDuration.SHORT,
position: application.android ? Toasty.ToastPosition.BOTTOM : Toasty.ToastPosition.CENTER,
makeText: function makeText(text) {
this.text = text;
return this;
},
show: function show() {
new Toasty.Toasty({text:this.text, duration:this.duration, position:this.position}).show();
},
setDuration: function setDuration(duration) {
this.duration = "long"==duration.toLowerCase() ? Toasty.ToastDuration.LONG : Toasty.ToastDuration.SHORT;
},
setPosition: function setPosition(position) {
if("top"==position.toLowerCase()) this.position = Toasty.ToastPosition.TOP;
if("center"==position.toLowerCase()) this.position = Toasty.ToastPosition.CENTER;
if("bottom"==position.toLowerCase()) this.position = Toasty.ToastPosition.BOTTOM;
},
};
global.Toast = new toast();
|
使うとき。
1
2
| require("toast");
Toast.makeText("メッセージ").show();
|
_ HtmlView, WebViewの見え方が違う(字が小さい、フォントが違う)
未稿。基本的にはHTML内のCSSで制御。
_ ActionBarのBackButtonが表示されない
iOSではBackButtonはアイコンでは無く文字で表示されます。このためテキストを指定する必要があります。
1
2
3
4
5
| <ActionBar>
<NavigationButton tap="onBackButtonTap" text="戻る"
icon="res://baseline_arrow_back_white_24" width="10"/>
<Label class="action-bar-title" text="ページタイトル"/>
</ActionBar>
|
このtextはandroidでは無視されます。一方でiconはiOSではWarningが(consoleに)出ます。アプリの動作には影響ありませんが、気持ち悪い方は次項のようにandroidとiOSで記述を分けた方が良いかも知れません。
_ モーダルダイアログ(showModal)のNavigationButtonが表示されない
こちらの記事に書いたように、制作中のアプリはモーダルダイアログ表示時に一旦カラのページを出した後遷移して表示したいページを表示しています。
そのためか、iOSではNavigateionButtonが表示されません(「<」も出ない)。
これを回避するためには、公式ページにも書かれている、ボタンを左側に出す方法を使います。
1
2
3
4
5
6
7
8
9
10
11
12
| <ActionBar>
<android>
<NavigationButton tap="onBackButtonTap"
icon="res://baseline_arrow_back_white_24" width="10"/>
</android>
<ios>
<ActionBar.actionItems>
<ActionItem icon="" text="戻る" tap="onBackButtonTap" ios.position="left" />
</ActionBar.actionItems>
</ios>
<Label class="action-bar-title" text="ページタイトル"/>
</ActionBar>
|
_ GridLayout内のStackLayoutは100%の大きさで配置される
画面の更新中はプログレスバーを出すため、Androidでは以下のようなXMLを組んでいました。
1
2
3
4
5
6
7
8
| <GridLayout>
<ScrollView class="{{ progress_next_busy ? 'main_view_disable':'main_view_enable'}}">
:
</ScrollView>
<StackLayout verticalAlignment="middle">
<ActivityIndicator busy="{{progress_next_busy}}" height="50"/>
</StackLayout>
</GridLayout>
|
progress_next_busyを true にすると、プログレスバーが表示されると共に、ScrollViewの方は disable になります。
ところが、このままではiOSではprogress_next_busyが false の時に ScrollView の中身が触れなくなってしまいます(TextViewの入力もできず Button も押せない)。どうやら、StackLayoutの高さが100%に固定され、ScrollViewにかぶさってしまうようです。
これを回避するには、StackLayout のvisibilityも制御してあげる必要があります。
1
2
3
4
5
6
7
8
9
| <GridLayout>
<ScrollView class="{{ progress_next_busy ? 'main_view_disable':'main_view_enable'}}">
:
</ScrollView>
<StackLayout verticalAlignment="middle"
visibility="{{progress_next_busy ? 'visible':'collapse'}}">
<ActivityIndicator busy="{{progress_next_busy}}" height="50"/>
</StackLayout>
</GridLayout>
|
_ nativescript-checkboxが落ちる
NativeScriptでチェックボックスを表示するにはプラグインを導入する必要があります。あまり種類も無いので、私が入れたのはこれでした。nativescript-checkbox
readme通りの記述でandroidではあっさり動いてくれました。
1
2
3
4
5
6
7
| <Page
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:CheckBox="nativescript-checkbox">
:
<CheckBox:CheckBox checked="{{ isChecked }}" text="チェックしてください。" fillColor="" />
:
</Page>
|
しかし、iOSでは落ちます。demoのプロジェクトをそのままビルドしても実行時に落ちます。
bundleでうまくpackされてないのかな? などと半日程度試行錯誤しましたが、エラーメッセージにちゃんと書いてありました(読め(笑))。
JavaScript error:
file:///app/vendor.js:21539:56: JS ERROR Error: Parsing XML at 30:6
> Invalid color:
最後のInvalid colorってやつです。fillColorに何も指定しないと落ちるようです。
最終的にはこうなりました。
1
2
3
4
5
6
| <Checkbox:CheckBox
checked="{{ isChecked }}"
text="チェックしてください。"
fillColor="blue"
onCheckColor="white"
onTap="onCheckBoxTap" />
|
ここで、公式サイトに書かれていないものも含め、プロパティは以下の意味になります。
- fillColor=チェック時の塗りつぶし色
- onCheckColor=チェック時のチェックの色
- tintColor=チェックボックスの枠の色
_ Date(string)の変換が厳密
よく言えば厳密、悪く言えば融通が利きません。
Androidでは、new Date('2019-01-01 10:00:00');から日付型を作ってくれますが、この形式はISOに準拠していないらしく、iOSではNaNになります。
参考URL*3によると、「yyyy/mm/dd hh:ii:ss」か「yyyy-mm-ddThh:ii:ss」にすれば良いので、以下のように記述します。まだ試していませんが、最初の方法の方がAndroidへの影響が少ないように思います。
1
| const dat = new Date(datestr.replace(/-/g, '/'));
|
1
| const dat = new Date(datestr.replace(' ', 'T'));
|
iOS: HtmlView content is cropped when using it inside ScrollView
https://github.com/NativeScript/NativeScript/issues/5926
ただし、さらに他のプラグインが必要なのと、ライセンスがMITじゃないので新たなライセンス表記が必要でちょっと面倒・・・(笑
https://stackoverflow.com/questions/13363673/javascript-date-is-invalid-on-ios