狛ログ

2021年12月28日火曜日

C# List内の要素同士で一致するものがあるか確認方法。

オフィス狛 技術部のHammarです。

プログラム実装時に、配列やList同士で一致するものがあるかどうかチェックする、というような場合が多々あると思います。
今回はC#でこのチェックをさっときれいに書く方法がないか調べてみて、良さそうな書き方を見つけたので、ご参考程度にちょっと共有したいと思います。

まず単純に、2つのList内に同一のものがあるかどうかチェックするする場合、下記のようなFindAllメソッド等を使えば簡単に確認ができます。
string[] fruitsAry1 = new string[] { "Apple", "Banana", "Orange", "Grape" };
string[] fruitsAry2 = new string[] { "Apple", "Grape", "Grapefruit", "Strawberry" };
List<string> list1 = new List<string>(fruitsAry1);
List<string> list2 = new List<string>(fruitsAry2);
List<string> fruitsList = list1.FindAll(list2.Contains);
結果上記のfruitsListは{ "Apple", "Grape" }になります。

単純なList同士であれば上記のような方法でチェックできますが、例えばListに複数要素が含まれている場合で、ある要素だけ、一致するかどうかをチェックしたい場合は、ちょっと工夫が必要になります。

例えば

■Fruits.cs

namespace Fruits
{
	public class FruitsData
	{
	    public string id { get; set; }
	    public string name { get; set; }
	}
}

■CheckFruits.cs

List<FruitsData> fruitsList1 = new List<FruitsData>()
{
    new FruitsData {Id = "1", Name = "Apple" },
    new FruitsData {Id = "2", Name = "Banana" }
    new FruitsData {Id = "3", Name = "Orange" }
    new FruitsData {Id = "4", Name = "Grape" }
};

List<FruitsData> fruitsList2 = new List<FruitsData>()
{
    new FruitsData {Id = "1", Name = "Apple" },
    new FruitsData {Id = "2", Name = "Grape" }
    new FruitsData {Id = "3", Name = "Grapefruit" }
    new FruitsData {Id = "4", Name = "Strawberry" }
};
上記のような場合でNameが一致するものがあるか、チェックする場合、素直にやると以下のような感じでしょうか
bool isSame = false;
foreach(var fruits1 in fruitsList1)
{
    if(isSame)
    {
        break;
    }
    foreach (var fruits2 in fruitsList2)
    {
        if(fruits2.Name.Equals(fruits1.Name))
        {
            isSame = true;
            break;
        }
    }
}
2つのリストを回して一致するものがあったら抜けるという、すごい単純なものですが、一致するものがあるかどうかのチェックだけのためにこれを書くのはちょっと長いですよね。

もうちょっとだけ簡略化すると、Existsメソッドを使えばこのようにも書けると思います。
bool isSame = false;
foreach(var fruits1 in fruitsList1)
{
    if(fruitsList2.Exists(fruitsItem2 => fruitsItem2.Name.Equals(fruits1.Name)))
    {
        isSame = true;
        break;
    }
}
結構すっきりしましたが、fruitsList1をとりあえず回さないといけないので、ここまできたらループさせずにいけそうな気がします。
で、上記を踏まえて、さらにFindメソッドを使って、最終的にはこんな感じに簡略化できました。
var machedFruits = fruitsList1.Find(fruitsItem1 => 
    fruitsList2.Exists(fruitsItem2 => fruitsItem2.Name.Equals(fruitsItem1.Name))
);
FindメソッドとExistsメソッドを使ってループを使わずに一致する要素の確認ができました。
ちなみにこのFindメソッドの返り値は、全体の中で最もインデックス番号の小さい要素が返ってきます。
一致するものが無い場合はnullが返ってきますので、単純に一致するかどうかのチェックとしては、nullが返ってこなかったら一致しているということになります。

この他にも、Linqを使ったり等いろいろと方法があると思いますが(もっと簡略化もできそう)、方法の1つとしてご参考いただければと思います。

【Laravel】条件によって、RateLimitterの回数制限のカウント処理を分ける方法。


オフィス狛 技術部のmmm(むー)です。

LaravelでAPIのリクエストをある回数以上行うと、「Too Many Attempts」のエラーが発生して一定時間経過するまでAPIを実行できなくなります。
このエラー文言の表示条件は、RateLimiterにて設定されています。
条件によって、この制限回数のカウント方法を変更する修正がしたかったのですが、対応方法がなかなか見つけられず時間がかかったため記事に残します。

やりたいこと
・ユーザー情報を取得できた場合:1分間に5回連続でAPIをリクエストすると、「Too Many Attempts」のエラーを発生させる
・ユーザー情報を取得できない場合:1時間に10回連続でAPIをリクエストすると、「Too Many Attempts」のエラーを発生させる

Laravelバージョン
$ php artisan --version
Laravel Framework 8.27.0

対処方法

早速ですが、下記のように記載すると実現できました。
RateLimiter::for('login', function (Request $request) {
	return $request->user()
	  ? Limit::perMinute(5)->by($request->user()->id) // ユーザー情報を取得できた場合のリクエスト回数制限
	  : Limit::perHour(10)->by($request->email.$request->ip()); // ユーザー情報を取得できない場合のリクエスト回数制限
}

・制限回数のチェックをする際は、by()でどの単位でカウントするかを指定します。
   ・ユーザー情報を取得できた場合:ユーザーID単位($request->user()->id)でカウント
   ・ユーザー情報を取得できない場合:リクエストのメールアドレス+IPアドレス($request->email.$request->ip())でカウント
・1分間での回数制限を設定する場合は、perMinute()を使用します。
・1時間での回数制限を設定する場合は、perHour()を使用します。

参考:https://laravel.com/docs/8.x/routing#segmenting-rate-limits (公式)

その他
確認する際は、キャッシュをクリアしてください。
$ php artisan cache:clear

またRateLimiterの修正が反映されているか、レスポンスヘッダーから確認することができます。
・X-RateLimit-Limit:全何回リクエストできるか
・X-RateLimit-Remaining:あと何回リクエストできるか
・Retry-After:何秒後にロックが解除されるか


以上となります、参考になりましたら幸いです。

,

2021年12月27日月曜日

【Python】loggingでログをカスタマイズする【Flask】


狛ログをご覧のみなさん、はじめまして。
2021年にオフィス狛に入社いたしました、nago(なご)です。
技術に関するブログ記事を書くのは初めてですが、有益な情報を投稿できるように頑張ります。
よろしくおねがいします☺

さて、今回はPython(Flask)で、お試し開発をしていた時に苦戦したログ出力についてまとめたいと思います。

作ろうとしていたのは、外部のREST APIにリクエストを投げて、何かしらの結果を受け取り、画面に表示させる機能です。
まず、先輩社員が投稿してくださっていたこちらの記事を参考にして、AWS API GatewayでAPIのMockを作成しました。

正しい合言葉をidに設定してリクエストすると、ステータスコードが200となり、とってもタメになる情報を受け取れるAPIにしました。
合言葉を間違えていたらステータスコードが401となりエラーが返ってきます。

  

  
↓ ↓ ↓
合言葉を間違えると401エラーとなり、以下のメッセージが返ってきます。
  
アプリケーションは以下のような構成で作成しました。
 	 
     api-test/
         ├ static/
         │ └ styles.css
         ├ templates/
         │ └ api_test.html
         ├ views/
         │ ├ __init__.py
         │ └ api_test.py
         ├ __init__.py
         ├ app.py
         ├ get-pip.py
         ├ Pipfile
         └ Pipfile.lock
   	 
  	

コードはこちら。
・api_test.html
    
      <!doctype html>
      <html lang="ja">

        <head>
            <meta charset="utf-8">
            <title>APIテスト</title>
            <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
        </head>

        <body>
            <form action="{{ url_for('api_test.call') }}" method="POST">
                <input type="text" name="id">
                <input type="submit" value="API CALL">
            </form>
            <input type="button" onclick="location.href=&#39;{{ url_for('api_test.show_api_test') }}&#39;" value="RESET">
            <p>{{ message }}</p>
        </body>
     
  

・views/app.py
    
        from flask import Flask, redirect, url_for
        from views.api_test import api_test

        app = Flask(__name__)

        app.register_blueprint(api_test)
			
        # 最初の画面へリダイレクト
        @app.route("/")
        def redirect_to_api_test():
            return redirect(url_for("api_test.show_api_test"))

        if __name__ == "__main__":
            app.run(host="0.0.0.0", port=8080)
    
  

・api_test.py
 	 
      import requests, json

      from flask import Blueprint
      from flask import render_template, request

      api_test = Blueprint("api_test", __name__)

      # 画面表示(初期 & リセット時)
      @api_test.route("/")
      def show_api_test():
          message = ""
          return render_template(
              "api_test.html",
              message=message
          )

      # APIリクエスト & レスポンス受け取り & 画面表示
      @api_test.route("/call", methods=["POST"])
      def call():

          # APIGatewayでMock作成時に設定されたURL
          url = "https://xxxx.amazonaws.com/api-test/my/info"
          # クエリ文字列を作成
          payload = { "id": request.form.get("id") }
          # リクエストヘッダを作成
          headers = { "Content-Type": "application/json" }
          
        # リクエスト & レスポンス受け取り
        response = requests.post(url, data=json.dumps(payload), headers=headers)
        # レスポンスからメッセージを取り出す
         message = response.json()["message"]

          # 画面遷移 & メッセージ表示
          return render_template(
              "api_test.html",
              message=message
          )
	 
  

これで、APIから返ってきたメッセージをそのまま画面に表示することができました。
次に、APIがコールされるたびに、そのリクエスト内容と結果のログを出力する仕組みを作ろうと思います。

Pythonでログを出力するには、Pythonで用意されているloggingというモジュールを使います。
今回は設定ファイルを既存のファイルとは別に作成して、ログの設定をしてみます。
私はプロジェクトルートに「config」という名前のディレクトリを作り、その中にログの設定ファイルを作りました。
また、ログファイルを出力したかったので、ログファイルを置くための「log」ディレクトリも作成しました。

以下が作成したディレクトリと設定ファイルです。

 	 
     api-test/
         ├ config/
         │ ├ __init__.py
         │ └ logging_config.py
         ├ log/
         ├ static/
         │ └ styles.css
         ├ templates/
         │ └ api_test.html
         ├ views/
         │ ├ __init__.py
         │ └ api_test.py
         ├ __init__.py
         ├ app.py
         ├ get-pip.py
         ├ Pipfile
         └ Pipfile.lock
   	 
  	
※赤字が追加分

・config/logging_config.py
    
    from logging import DEBUG, INFO, config
    import datetime

    # ファイル名に入れる日付フォーマット
    today = datetime.date.today().strftime("%Y-%m-%d");

    config.dictConfig(
        {
            "version": 1,
            # ログフォーマット設定
            "formatters": {
                "formatter": {
                    "format": "%(levelname)s  %(asctime)s [%(module)s] %(message)s"
                }
            },
            # ハンドラー設定
            "handlers": {
                # 標準出力ハンドラー
                "streamHandler": {
                    "class": "logging.StreamHandler",
                    "formatter": "formatter",
                    "level": INFO
                },
                # ファイル出力ハンドラー
                "fileHandler": {
                    "class": "logging.FileHandler",
                    "formatter": "formatter",
                    "level": INFO,
                    # ログファイルを出力したい場所のパスとファイル名を指定
                    "filename":f"xxxx/api-test/log/{today}_api-test.log",
                    "encoding": "utf-8"
                }
            },
            # ロガー設定
            "loggers": {
                "basicLogger": {
                    "handlers": [
                        "streamHandler",
                        "fileHandler"
                    ],
                    "level": INFO,
                    "propagate": 0
                }
            }
        }
    )
    
  

ログの設定は、logging.config.dictConfigに辞書(Dictionary型の設定値)を渡すか、
logging.config.fileConfigにconfigparser形式ファイルを渡すかで設定できますが、
私は今回はdictConfigで設定してみました。

ログを出力するには、まずLogger(ロガー)の取得が必要なのですが、そこにHandler(ハンドラー)を指定することで出力形式の指定をプラスできるようになっています。
また、出力フォーマットも設定したい場合は、ハンドラーに対してFormatter(フォーマッター)を指定します。

ロガー、ハンドラー、フォーマッターの名前は自由につけることが出来ます。
私はロガーは「basicLogger」を作り、そこに標準出力ハンドラーの「streamHandler」と、ファイル出力の「fileHandler」をくっつけました。フォーマッターは「formatter」にしました(そのまんま)

"varsion" : 1 ってなんぞやと思い調べてみたのですが、辞書スキーマのバージョンのことでした。
現在は1しかないので、1を入れておけばいいそうです。こちらはdictConfigに辞書を渡す場合は必須のため、入れないと怒られます。

さて、ようやくapi_test.pyで設定ファイルを読み込んで、ログ出力してみます。

・views/api_test.py
    
    import requests, json
    from logging import getLogger

    from flask import Blueprint
    from flask import render_template, request

    from config import logging_config

    api_test = Blueprint("api_test", __name__)

    # ログ設定ファイル読込
    logging_config
    # ロガー取得
    logger = getLogger("basicLogger")

    # 画面初期表示
    @api_test.route("/")
    def show_api_test():
        message = ""
        return render_template(
            "api_test.html",
            message=message
        )

    # APIリクエスト & レスポンス受け取り & 画面表示
    @api_test.route("/call", methods=["POST"])
    def call():

        # APIGatewayでMock作成時に設定されたURL
        url = "https://xxxx.amazonaws.com/api-test/my/info"
        # クエリ文字列を作成
        payload = { "id": request.form.get("id") }
        # リクエストヘッダを作成
        headers = { "Content-Type": "application/json" }

        # リクエストログ出力
        logger.info("request : url = " + url + ", payload = " + str(payload))

        # リクエスト & レスポンス受け取り
         response = requests.post(url, data=json.dumps(payload), headers=headers)
        # レスポンスからステータスコードとメッセージを取り出す
         status_code = response.status_code
         message = response.json()["message"]

        # レスポンスログ出力
        if status_code == 200:
            logger.info("response : status_code = " + str(status_code) + ", message = " + message)
        else:
            logger.error("response : status_code = " + str(status_code) + ", message = " + message)

        # 画面遷移 & メッセージ表示
        return render_template(
            "api_test.html",
            message=message
        )
    
  
※赤字は追加分

・log/2021-12-15_api-test.log (2021/12/15にログが作成された場合のファイル名)
    
      INFO  2021-12-15 14:23:53,701 [api_test] request : url = https://xxxx.amazonaws.com/api-test/my/info, payload = {'id': 'sushi'}
      INFO  2021-12-15 14:23:53,778 [api_test] response : status_code = 200, message = お寿司はおいしい。猫はかわいい。ポメラニアンもかわいい。
      INFO  2021-12-15 14:24:46,184 [api_test] request : url = https://xxxx.amazonaws.com/api-test/my/info, payload = {'id': ''}
      ERROR  2021-12-15 14:24:46,254 [api_test] response : status_code = 401, message = 合言葉が違うようです・・・
    
  
正常パターンとエラーパターンを実行してみました。
フォーマッターで設定した「%(levelname)s %(asctime)s [%(module)s] %(message)s」の通りに表示されています。
出力時にapi_test.pyで設定したメッセージが「%(message)s」に代入されている形です。

調べてみると、loggingは思ったより奥が深くびっくりしました。
覚えておくと出したいログを見やすく出すことができて便利だと思います。

今回はこれでおしまいです。
ここまでお読みいただきありがとうございました。

参考リファレンス:
Logging HOWTO
logging.config --- ロギングの環境設定

, , ,

Angular・ngx-scannerでインカメラが使われるのを制御する方法。


こんにちは、オフィス狛 技術部のpinoです。


Angularで、バーコードやQRコード読み取りに使用するライブラリに、『ngx-scanner』があります。

テンプレートに<zxing-scanner></zxing-scanner>を埋め込むだけで、読み取り用のカメラを自動で起動してくれる便利なライブラリです。


当社のプロジェクトでも採用しているのですが、先日、通常なら背面のカメラが起動するところで、iOS12でのみインカメラが起動する事象が発生しました。


今回は、その時のトラブルシューティングとして、インカメラが使われるのを制御する方法について紹介します。



バージョン情報

  • Angular:12.0.3
  • ngx-scanner:3.0.1


なぜインカメラが起動する?

まず、インカメラが起動してしまう理由について見ていきます。


カメラが起動するまでの流れは、node_modules/@zxing/ngx-scanner/__ivy_ngcc__/fesm2015/zxing-ngx-scanner.jsで確認できます。

ngAfterViewInit()

/**
 * Executed after the view initialization.
 */
ngAfterViewInit() {
    // makes torch availability information available to user
    this.getCodeReader().isTorchAvailable.subscribe(x => this.torchCompatible.emit(x));
    if (!this.autostart) {
        console.warn('New feature \'autostart\' disabled, be careful. Permissions and devices recovery has to be run manually.');
        // does the necessary configuration without autostarting
        this.initAutostartOff();
        return;
    }
    // configurates the component and starts the scanner
    this.initAutostartOn();
}

this.autostartはtrue固定なのでif (!this.autostart)の分岐に入りません。
this.initAutostartOn()に続きます。

initAutostartOn()

/**
 * Initializes the component and starts the scanner.
 * Permissions are asked to accomplish that.
 */
initAutostartOn() {
    return __awaiter(this, void 0, void 0, function* () {
        this.isAutostarting = true;
        let hasPermission;
        try {
            // Asks for permission before enumerating devices so it can get all the device's info
            hasPermission = yield this.askForPermission();
        }
        catch (e) {
            console.error('Exception occurred while asking for permission:', e);
            return;
        }
        // from this point, things gonna need permissions
        if (hasPermission) {
            const devices = yield this.updateVideoInputDevices();
            this.autostartScanner([...devices]);
        }
    });
}

カメラアクセスの権限周りをチェックしています。

許可された状態だと、最終的にthis.autostartScanner([...devices])が実行されます。

autostartScanner()

/**
 * Starts the scanner with the back camera otherwise take the last
 * available device.
 */
autostartScanner(devices) {
    const matcher = ({ label }) => /back|trás|rear|traseira|environment|ambiente/gi.test(label);
    // select the rear camera by default, otherwise take the last camera.
    const device = devices.find(matcher) || devices.pop();
    if (!device) {
        throw new Error('Impossible to autostart, no input devices available.');
    }
    this.device = device;
    // @note when listening to this change, callback code will sometimes run before the previous line.
    this.deviceChange.emit(device);
    this.isAutostarting = false;
    this.autostarted.next();
}

まず、カメラのlabelプロパティにback|trás|rear|traseira|environment|ambienteが含まれるかを見ていますね。

(調べてみたところ、ポルトガル語やスペイン語などで「後方」「環境」といった意味の単語が含まれているようです。)

devicesの中で上記に当てはまる最初のカメラか、当てはまるものがない場合はdevicesの最後尾のカメラが起動される仕組みになっています。


ここまでで、カメラが起動するまでの流れを追うことができました。


次は、iOS12と、比較用に背面カメラが起動するiOS15のカメラ情報を、デモページを使って確認してみます。


デモページでカメラアクセスを許可すると、使用できるカメラのリストが表示されます。

iOS12とiOS15で試した結果、以下のようになりました。

『〇〇カメラ』となっているのがlabelプロパティに当たり、並び順がそのままdevicesの要素の並びになっています。


どちらのOSも、labelback|trás|rear|traseira|environment|ambienteは含まれていません。

その場合、配列の最後尾のカメラが起動されるわけですが、iOS15は背面カメラなのに対し、iOS12は前面側カメラが最後尾になっています。

iOS12でのみインカメラが起動するのは、ここに原因があったんですね。

デモのコードは、公式のGithubか下記からも確認できます。

https://stackblitz.com/edit/zxing-ngx-scanner


ちなみに、iPhoneの設定から『iPhoneの使用言語』をEnglishに変更したところ、

  • 前面カメラ -> Front Camera

  • 背面カメラ -> Back Camera

という風に文言が変わりました。

labelは使用言語に基づくようなので、使用言語を変更することで回避できる、ということになりますが、あまり現実的ではなさそうです...。



インカメラが使われるのを避けるには?

自動起動されるカメラを変更するのは、先ほど確認したような処理の流れ自体を変更しないといけないので、難しいです。

ただ、デモページで確認できるように、カメラの切り替えは可能になっています。

だとしたら、自動起動が終わった後に、背面カメラを起動するよう設定し直すのはどうでしょうか?


ということで、今度は「自動起動が完了したことを検知できるか」を確認してみます。


ngx-scannerでは、いくつかのイベントが用意されています。

今回使用しているVer.3.0.1では、以下のイベントが定義されていました。

  • autostarted

  • autostarting

  • torchCompatible

  • scanSuccess

  • scanFailure

  • scanError

  • scanComplete

  • camerasFound

  • camerasNotFound

  • permissionResponse

  • hasDevices

  • deviceChange

名前からなんとなくどんなイベントか予想がつくかと思います。


例えば、「使用できるカメラが存在していない場合」と「読み取りに成功した場合」にイベントハンドラを設けたい時は、以下のようにします。

テンプレート側

<zxing-scanner
    (camerasNotFound)="onCamerasNotFound()"
    (scanSuccess)="onScanSuccess($event)">
</zxing-scanner>

ロジック側

onCamerasNotFound(): void {
    console.log('cameras not found.')
}

onScanSuccess(result: string): void {
    console.log('scan success:', result)
}

ここで、カメラが起動するまでの流れの最後の処理(autostartScanner())をもう一度見てみます。

/**
 * Starts the scanner with the back camera otherwise take the last
 * available device.
 */
autostartScanner(devices) {
    const matcher = ({ label }) => /back|trás|rear|traseira|environment|ambiente/gi.test(label);
    // select the rear camera by default, otherwise take the last camera.
    const device = devices.find(matcher) || devices.pop();
    if (!device) {
        throw new Error('Impossible to autostart, no input devices available.');
    }
    this.device = device;
    // @note when listening to this change, callback code will sometimes run before the previous line.
    this.deviceChange.emit(device);
    this.isAutostarting = false;
    this.autostarted.next();  // <- ここ!
}

最後に、autostartedイベントが発火していますね。

これを、コンポーネント側で検知するようにしてあげればよさそうです。

テンプレート側

<zxing-scanner
    [(device)]="currentDevice"
    (camerasFound)="onCamerasFound($event)"
    (autostarted)="onAutoStarted()">
</zxing-scanner>

ロジック側

availableDevices: MediaDeviceInfo[];
currentDevice: MediaDeviceInfo = null;

...

onCamerasFound(devices: MediaDeviceInfo[]): void {
    // 使用できるカメラを取得
    this.availableDevices = devices;
}

onAutoStarted(result: string): void {
    const userAgent = navigator.userAgent.toLowerCase();
    let iosVersionInfo = userAgent.match(/iphone os ([\d]+)_([\d]+)_([\d]+)/);
    if (!iosVersionInfo) {
        iosVersionInfo = userAgent.match(/iphone os ([\d]+)_([\d]+)/);
    }
    const iosVersion = iosVersionInfo?.[1];

    if (iosVersion === '12') {
        // iOS12の場合は、配列の最初のカメラを使用する
        this.currentDevice = this.availableDevices?.[0];
    }
}

実際に、iOS12の場合は背面カメラを起動するよう設定し直すサンプルにしてみました。


まず、camerasFoundで使用できるカメラの情報が渡ってきますので、変数に控えます。

テンプレート側にある[(device)]プロパティに使用したいカメラを設定してあげるとカメラが切り替えられるので、UserAgentからiOS12と判断できる場合は配列の最初のカメラを設定するようにしました。


iOS12ではない場合はライブラリ側で自動起動したカメラが設定されますので、ピンポイントな修正が可能です。



INFO

最新版(Ver.3.2.0)では、this.autostarted.next()initAutostartOn()内に処理が移っていました。

紹介した処理の流れは大きく変わっていないと思うので、お使いのバージョンと比較してみてください。



以上が、今回のトラブルシューティングの内容でした。

どなたかの参考になれば嬉しいです🦓


iOS15版のSafariから目立つようになったステータスバーの背景色を指定したい。

こんにちは、オフィス狛 デザイン部のSatoです。



iOS15版のSafariで以前私がHTMLなどを作成したWebシステムを確認したところ、ステータスバーの色が変わっていることに気づきました。

自動的に選ばれた色とヘッダーの色味が少し違う色でしたので、逆に浮いて見えて困っていました。

ステータスバーをWebシステムのメインカラーの色に合わせられたら違和感を感じないのにと考え、色を制御する方法を探していたところ、色を指定できるようなので忘れないよう備忘録も兼ねてブログに書いてみようと思います。


ステータスバーの背景色を指定する方法、それは「theme-color」というmetaタグを書くだけです。

headタグの中に下記を記述することで色を変えることができます。

<meta name="theme-color" content="任意の色">

任意の色の部分にカラーコードを指定するだけで、ステータスバーやアドレスバーの背景色が変わります。

上記は
<meta name="theme-color" content="#dcdcdc">
をheadタグの中に記述しました。

「theme-color」を指定した場合も、明度の高いペールカラーのような色を指定した場合にはダークモードでは色が変わらないこともあるようですが、それはダークモードの仕様上仕方ないかと思います。



余談ですが、iOS15版のSafariから画面下部にアドレスバーが表示される関係だと思うのですが、要素が少ないページの背景色がアドレスバーの下には回り込まないで下に「theme-color」で指定した色が表示されてしまったりします。

上記のような状態になった場合は、全体を囲んでいるタグにheight: 100vh;を指定すると背景が下まで表示されるようになりました。(余白が空いてしまった例のCSS指定は height: 100%; にしていました)

簡単なことなのですが、少しつまづいてしまったので、似たような現象に悩まれた方は試してみてはいかがでしょうか?


iOS15版のSafariはレイアウト含めさまざまな変更点があり、技術者はもちろん普通に使っているユーザーも覚えることが多くて大変ですね……。

今後もiOS版Safariの仕様変更には気をつけていきたいです。