iOSシミュレーターでもpush通知を実現する
native開発している人だったらわかると思いますが、iOSではシミュレーターではpush通知は送れません。しかし、それだけのためにわざわざ端末を用意するのも一手間ですよね。もちろんテストはちゃんとした端末でやったほうがいいのですが、開発段階でいちいち線で繋いでデバッグはつらすぎるので、そこの開発体験を自分でワークアラウンドしたので、書き残しておきます。
実はpush通知はデバッグが可能
Simulator supports simulating remote push notifications, including background content fetch notifications. In Simulator, drag and drop an APNs file onto the target simulator. The file must be a JSON file with a valid Apple Push Notification Service payload, including the “aps” key. It must also contain a top-level “Simulator Target Bundle” with a string value that matches the target application’s bundle identifier. simctl also supports sending simulated push notifications. If the file contains “Simulator Target Bundle” the bundle identifier is not required, otherwise you must provide it as an argument (8164566):
xcrun simctl push <device> com.example.my-app ExamplePush.apns
Xcode 11.4 Release Notes | Apple Developer Documentation
https://developer.apple.com/documentation/xcode-release-notes/xcode-11_4-release-notes
Xcode 11からシミュレーターで.apnsファイルを作成して、JSON形式のデータを書き込んでシミュレーターにドラッグアンドドロップすることで、push通知を再現することが可能になっています。
そして、Xcodeのコマンド経由でもシミュレーターに.apnsファイルを送ることができます。
xcrun simctl push シミュレーターのデバイスUUID apnsファイルのパス
デバイスUUIDは 👇 で取得できます。
xcrun simctl list devices
.apnsファイルのフォーマットはだいたいこんな感じです。
{
"Simulator Target Bundle": "ここにはbundler idを入力com.exampleappみたいなやつです",
"aps":{
"alert": {
"title": "👋🏻こんちわー👋🏻",
"body": "ここにはテキストがはいるぞー",
"data": "{\"type\":\"test\"}"
},
"sound": "default",
"badge": 100
},
"duration": "Current time"
}
macを経由してpush通知と同じ体験を実現する
リアルのpush通知は無理ですが、同じような体験を実現することは可能です。
ざっくりいうと、ローカルのバックエンドサーバーから、上で挙げたコマンドを使いmacを経由でシミュレーターに.apnsファイルを投げて通知させます。
サーバー側
ここでは例としてFastifyを使っていますが、Expressでも同じです。
import { readFileSync } from "fs";
import { exec } from "node:child_process";
import fs from "node:fs/promises";
import path from "path";
app.post("/push-notification", async (req, reply) => {
const { sound, title, body, data } = req.body as {
to: string;
sound: "default";
title: string;
body: string;
data: { someData: string };
};
const apnsPath = path.resolve(
process.cwd(),
"./notification.apns", // <= 👈 ここは好きなパスで大丈夫
);
const apns = readFileSync(apnsPath, "utf8");
const apnsJson = JSON.parse(apns) as {
"Simulator Target Bundle": string;
aps: {
alert: {
title: string;
body: string;
data: string;
};
sound: string;
badge: number;
};
duration: string;
};
apnsJson.aps.alert.title = title;
apnsJson.aps.alert.body = body;
apnsJson.aps.alert.data = JSON.stringify(data);
apnsJson.aps.sound = sound;
apnsJson.aps.badge = count++;
apnsJson.duration = "Current time";
await fs.writeFile(apnsPath, JSON.stringify(apnsJson, null, " "), "utf8");
const uuids: string[] = [];
const simulators = exec(
`xcrun simctl list devices | grep Booted | sed -E 's/.*\\(([A-Z0-9-]+)\\).*/\\1/'`,
);
await new Promise<string[]>((resolve) => {
simulators.stdout?.on("data", (data) => {
uuids.push(...data.toString().trim().split("\n"));
});
simulators.on("close", () => {
resolve(uuids);
});
});
await new Promise((resolve) => {
for (const uuid of uuids) {
const push = exec(`xcrun simctl push ${uuid} ${apnsPath}`);
push.stdout
?.on("data", (data) => {
console.log(data.toString().trim());
})
.on("close", () => {
resolve(true);
});
}
});
reply.send({
push: "sent",
});
});
ざっくり説明すると、clientからリクエストを受け取り、それを元に.apnsファイルを適切に書き換えます。そして、Xcodeのコマンドラインを使って現在起動中のシミュレーターのUUID一覧を取得します。それぞれのシミュレーターにまたXcodeのコマンドを使い、apnsファイルを飛ばします。
clientのコードはAPIリクエストを投げるだけなので割愛します。
Discussion