こんにちは。西日本テクノロジー&イノベーション室の高谷です。 TIS入社4年目です。
3月末まで、とあるサービスのPoCに用いるMVP(Minimum Viable Product)として、スマホ向けWebアプリを開発していました。
今回はその開発中に発生したバーチャルキーボードの挙動によって意図しない画面表示になる問題と、その解決方法についてお話をします。

背景

開発したアプリはReactを利用したSPAで、iPhoneとAndroid端末を対象にしたWebアプリでした。
フロントエンドを開発するエンジニアは3名、開発期間は1ヶ月半の少人数短納期開発でした。
今回の問題を見つけたのは開発終盤で、発生した問題を短時間で解決することが求められる状況でした。

要件

このアプリには以下のような要件がありました。

  • ヘッダーは画面上部に表示していること
  • 入力欄は画面下部に表示していること
  • ヘッダーと入力欄は文字入力時も画面外に消えることなく常に表示していること

iPhoneで文字を入力するときの挙動が原因でこの要件を満たせないという問題が起きました。

【要件イメージ】

何が問題だったか

当初はヘッダーと入力欄にposition:fixedのスタイルを適用することで要件を満たそうとしていました。

しかしiPhoneで文字を入力しようとバーチャルキーボードを表示させると、ヘッダーが上にスライドして画面外に消えてしまい、要件通りにならない事象が起こりました。 この事象はiPhoneにて発生するもので、Android端末ではこのような事象は発生しませんでした。

【ヘッダーが上にスライドして画面外に消える】

【当初想定していた実装イメージ】

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="Header">
        ヘッダー
      </header>
      <div className="Content">
        {/*ヘッダーと入力欄の間要素(省略)*/}
      </div>
      <input className="Input" placeholder="入力欄" />
    </div>
  )
}
.Header {
  background-color: navy;
  height: 7vh;
  font-size: 18pt;
  color: white;
  /* fixedで上部に固定する */
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
}
.Input {
  background-color: navy;
  color: white;
  height: 7vh;
  font-size: 18pt;
  /* fixedで下部に固定する */
  position: fixed;
  bottom: 0;
  right: 0;
  left: 0
}
.Content {
  color: black;
  font-size: 8pt;
  padding-top: 7vh;
  padding-bottom: 7vh;
}

どうやって解決したのか

position: absolute を利用して画面を固定する

「iPhone virtual keyboard fixed element」というキーワードでインターネットを検索するといくつか解決策が示されており、その中でサンプル実装があったため、動作がすぐに確認できたMediumに投稿されていた記事を参考にしました。
position:absoluteを利用して画面を固定し、バーチャルキーボード表示時はbottomの位置をキーボードの高さだけ上げてヘッダーと入力欄を画面内に収める方法でiPhoneとAndroid共に要件を満たせました。

今回キーボードの高さとして270pxという値を利用しました。
この値は参考にした記事にもあるようにAppleが公式で出している数字ではなく、調整を繰り返した結果出てきた値です。
今回、短い時間で課題を解決する必要があったため、より正確に表示領域のサイズを計測して合わせる術を調査することはできませんでした。
そこで270pxという数値に頼ってiPhoneで動作を確認してみたところうまくいくことがわかったため採用しました。

position:absoluteを利用して画面を固定する実装イメージ】

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      {/*バーチャルキーボード表示時はこの要素のbottom位置を上げる*/}
      <div className="VariableArea">
        <header className="Header">
          ヘッダー
        </header>
        <div className="Content">
          {/*ヘッダーと入力欄の間要素(省略)*/}
        </div>
        <input className="Input" placeholder="入力欄" />
      </div>
    </div>
  )
}
.VariableArea {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  /* バーチャルキーボード表示時は270px(バーチャルキーボードの高さ)を指定する */
  bottom: 0;
}

/*variableAreaの内側の要素はvariableAreaを祖先にして相対配置する */
.Header {
  background-color: navy;
  height: 7vh;
  font-size: 18pt;
  color: white;
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
}

.Input {
  background-color: navy;
  color: white;
  height: 7vh;
  font-size: 18pt;
  position: absolute;
  bottom: 0px;
  right: 0;
  left: 0;
}

.Content {
  color: black;
  font-size: 8pt;
  position: absolute;
  top: 7vh;
  bottom: 7vh;
  right: 0;
  left: 0;
  /* 高さを固定しているので、表示しきれなかった分をスクロールさせる */
  overflow-y: auto;
}

バーチャルキーボードの表示/非表示に合わせて動的にbottomの値を変更する

バーチャルキーボードの表示/非表示は入力欄のonfocusイベント/onblurイベントを利用して判定し、bottomの値を動的に変更します。
bottomの値を変更する処理を加えると、入力欄が画面上端に寄ってヘッダーが画面外に消えてしまいます。
そこで、バーチャルキーボード表示後に画面上端までスクロールする処理も追加し、ヘッダーを画面に表示させます。

【入力欄が画面上端に寄ってヘッダーが画面外に消える】

【動的にbottomの値を変更する実装イメージ】

import React, { useState } from 'react';
import './App.css';

function App() {
  const [isFocused, setFocused] = useState(false);

  return (
    <div className="App">
      {/* 入力欄にフォーカスが当たっている状態(isFocused)で動的にbottomの値を変更する */}
      <div className="VariableArea" style={isFocused ? {bottom: '270px'} : {bottom: '0'}}>
        <header className="Header">
          ヘッダー
        </header>
        <div className="Content">
          {/* 略 */}
        </div>
        <input className="Input" placeholder="入力欄"
          onFocus={() => {
            setFocused(true)
            //バーチャルキーボード表示後に画面上端までスクロールする(バーチャルキーボード表示にかかる時 間として200ミリ秒待たせる)
            setTimeout(() => {window.scrollTo(0, 0);}, 200);
          }} 
          onBlur={() => setFocused(false)}/>
      </div>
    </div>
  );
}

【バーチャルキーボード表示でヘッダーが画面外に消えない】

iPhoneに限定して適用する

今回の事象はAndroid端末では発生しないため、上で挙げたbottomの値の変更をiPhoneに限定して適用します。
以下に記載した実装イメージのように、ユーザーエージェント名からiPhoneを特定しbottomの値の変更可否と変更時の値を返却する関数を作成しました。

【ユーザーエージェント名からiPhoneを特定する実装例】

const getViewPatch = () => {
    if (/iPhone/.test(navigator.userAgent) && !window['MSStream']) {
        //bottomの値変更可否と変更時の値を返す。
        return {
            isViewPatch: true,
            bottom: '270px'
        };
    }

    return {
        isViewPatch: false,
        bottom: '0px'
    };
}

iPhoneXでは調整が必要

iPhoneXの場合バーチャルキーボードの高さが270pxより少し大きくなるため、bottomの位置を更に上げる必要がありました。
こちらも270px同様、微調整の結果生まれた数値になってしまうのですが、270pxに追加で34pt上げることで丁度合いました。
ユーザーエージェント名からはiPhoneであることまでしか特定できないため、画面サイズからiPhoneXを特定しました。

【画面サイズからiPhoneXを特定する実装例】

const getViewPatch = () => {
    if (/iPhone/.test(navigator.userAgent) && !window['MSStream']) {
        //利用者端末の画面サイズを取得する
        const platformSize = (window.screen.width * window.screen.height * window.devicePixelRatio);
        //cf. https://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutions
        const iPhoneXSizeList =
        [
            (375 * 812 * 3), // iPhoneX or Xs
            (414 * 896 * 3), // iPhoneXsMax
            (414 * 896 * 2)  // iPhoneXR
        ];
        if (iPhoneXSizeList.includes(platformSize)) {
            //iPhoneXの場合は追加で34pt上げる
            return {
                isViewPatch: true,
                bottom: 'calc(270px + 34pt)'
            }
        }
        return {
            isViewPatch: true,
            bottom: '270px'
        };
    }
    return {
        isViewPatch: false,
        bottom: '0px'
    };
}

さいごに

今回は問題解決に時間を大きく割けないという制約があったため、このような回避策をとりました。
短い時間の中で原因を調査し対応することの難しさを改めて感じる経験でした。
今回出会った問題を次からの開発に活かすために、更に調査をおこない、より良い解決策を見つけようと考えています。


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。