推送通知
了解如何在 EasyAppSwiftUI 中实现推送通知
EasyAppSwiftUI 底层使用 OneSignal 实现推送通知。即使是免费版本,OneSignal也慷慨的提供了无限次数的推送功能。
推荐使用真机进行测试,模拟器会存在接收不到通知的情况。

接入 OneSignal
1. 创建 OneSignal 项目
要接入 OneSignal,您需要先创建一个 OneSignal 项目。您可以访问 OneSignal 官网 注册账号,并创建一个项目。OneSignal的文档写的非常详细。您可以参考官方文档来创建项目。

2. 上传p8证书

上传p8证书是最关键的一步,请按照官方文档来上传p8证书。
3. 配置Xcode 推送功能
EasyAppSwiftUI客户端模板已经按照iOS setup配置好了推送,这一步您无需操作。但是比较关键的一步就是,您的设置您自己的App Groups,不要使用模板自带的App Groups 标识符。

如何修改:
- 首先取消勾选模板设置好的App Groups 标识符。
- 然后点击+号,添加您自己的App Groups 标识符。这一步请参考OneSignal文档
其他的就无需更改了。
4. EasyAppNotifyKit 集成
EasyAppNotifyKit 是 EasyAppSwiftUI 的推送集成库,内部基于 OneSignal 的 SDK 实现了推送功能。由于该库属于 EasyApp 模板的私有库,如果您不是 EasyApp 的客户,您是看不到该库的。
推荐方案
EasyApp 已经通过本地 SPM 依赖的方式集成了 EasyAppNotifyKit,无需手动配置 Xcode 的 Github 账户。所有代码已包含在项目的 Packages 文件夹中。
使用本地 SPM 依赖
由于 Xcode 自带的 git 工具往往不能顺利拉取私有库,我们采用了本地 SPM 依赖的方式来解决这个问题。
项目结构:
如何更新代码:
如果您需要修改 EasyAppNotifyKit 代码以符合您的业务需求,可以直接在 EasyAppSwiftUI/Packages/EasyAppNotifyKit 文件夹中编辑源代码。
旧方法:通过 Github 账户拉取(已废弃)
已废弃
此方法已不再推荐使用。由于 Xcode 拉取私有库经常遇到问题,我们已改用本地 SPM 依赖方式。以下内容仅供参考。
步骤 1:加入 FastEasyApp 组织
首先确保您的 Github 账户已经属于 FastEasyApp 组织。如果您已经购买了 EasyApp,但还未加入到该组织,请联系我们:
步骤 2:Xcode 登录 Github 账户
打开 Xcode 设置:

新增 Github 账户:

输入您的 Github 账户和 Token,如何创建 Token,请点击 Create a Token on Github 按钮,按照文档创建即可:

关联 Github 账号成功之后,您就可以拉取 EasyAppNotifyKit 了。
5. 配置 EasyAppNotifyKit
配置您的 OneSignal
EasyApp 默认开启推送功能,您可以在这里配置是否开启和 OneSignal 的 app id。
enum Constants {
enum OneSignal {
/// is enabledNotification
static let isEnabledNotification = true
/// The app id for the OneSignal service
static let appId = " your one signal app id"
}
}如何获取您的 OneSignal app id,步骤如下:


粘贴到您的项目中。
到这里,您已经配置好了 OneSignal 的推送功能。
接下来,您需要熟悉 EasyAppNotifyKit 的用法。
使用 EasyAppNotifyKit
EasyAppNotifyKit 抽象了推送通知的接口,您可以随意更换底层推送商的选择。只需要实现 PushServiceProtocol 协议即可。
public struct PushServiceConfiguration {
let appId: String
let launchOptions: [UIApplication.LaunchOptionsKey: Any]?
public init(appId: String, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
self.appId = appId
self.launchOptions = launchOptions
}
}
public protocol PushServiceProtocol {
/// Initialize the push service
/// - Parameter configuration: The configuration to initialize the push service
func initialize(with configuration: PushServiceConfiguration)
/// Request permissions
func requestPermissions()
/// Get permissions status
/// - Returns: The permissions status
func getPermissionsStatus() -> UNAuthorizationStatus
/// Login user
/// - Parameter userId: The user id to login
func loginUser(with userId: String)
/// Logout user
func logoutUser()
}如果协议满足不了您当前的需求,您可以随意扩展。
PushManager 是一个单例,管理了当前的推送服务。您可以通过 PushManager 来配置和初始化当前的推送服务。
let oneSignalService = OneSignalService()
PushManager.shared.configure(with: oneSignalService)
let configuration: PushServiceConfiguration = PushServiceConfiguration(
appId: Constants.OneSignal.appId,
launchOptions: launchOptions
)
PushManager.shared.initialize(with: configuration)发起权限请求
Apple 鼓励用户在用到了推送通知时,主动发起权限请求。您可以调用 requestPermissions 方法来发起权限请求。
PushManager.shared.requestPermissions()为了更好的用户体验,EasyAppNotifyKit 提供了 三个视图来帮助您管理推送通知的权限。
NotificationsPermissions()
NotificationGrantedView()
NotificationDeniedView()NotificationsPermissions 视图是用于请求权限的视图。
NotificationGrantedView 视图是用于显示推送通知已授权的视图。
NotificationDeniedView 视图是用于显示推送通知未授权的视图。
您可以随意使用这些视图,或者自定义视图。
EasyAppSwift客户端提供了一个 Demo页面,专门用来测试通知权限,文件地址位于
import EasyAppNotifyKit
import SwiftUI
struct NotificationsDemo: View {
@State private var showNotificationStatus = false
@State private var showDeepLinkTest = false
var body: some View {
ScrollView {
VStack(spacing: 20) {
Button {
showNotificationStatus = true
} label: {
Label("dev_show_notification_status", systemImage: "bell")
}
.buttonStyle(.borderedProminent)
Text("dev_notifications_status_hint")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Divider()
// OneSignal test suite from EasyAppNotifyKit
OSNotificationTestsView()
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
// Backend OneSignal payload test harness
NavigationLink {
BackendSentNotificationTestView()
} label: {
Label(
"dev_backend_push_test", systemImage: "paperplane.fill")
}
.buttonStyle(.bordered)
// Manual open the deep link target page for quick test
Button {
showDeepLinkTest = true
} label: {
Label(
"dev_open_deep_link_test_page",
systemImage: "arrow.right.circle")
}
.buttonStyle(.bordered)
}
.padding(.bottom)
}
.navigationTitle("dev_notifications_demo_title")
.padding(.top, 40)
.sheet(isPresented: $showNotificationStatus) {
// Use the new NotificationStatusContainer that automatically determines which view to show
NotificationStatusContainer()
}
.sheet(isPresented: $showDeepLinkTest) {
OSDeepLinkTargetTestView()
}
}
}发送通知
使用 OneSignal Dashboard 发送通知

使用Supabase 云函数发送通知 (推荐)
EasyApp 提供了服务端能力发送通知,这样用代码控制发送通知,会更加灵活。配合您自己的业务需求,达到颗粒度推送的目的。OneSignal 支持非常丰富的自定义字段,您可以实现针对某一个用户/批量用户、某个地区、某个时间段等等事件来触发通知。详细请参考OneSignal 文档。
Supabase 提供了send-onesignal-notification 云函数,该云函数会调用 OneSignal 的 REST API 发送通知。因此您需要在 EasyAppSupabase 项目中配置OneSignal 的 REST API Key和App ID。
下面介绍如何获取OneSignal 的 REST API Key和App ID。
登录您的OneSignal 账号,进入到 Dashboard,然后点击Settings。

复制App ID 和 新建 REST API key。
接下来,我们把App ID 和 REST API Key 配置到EasyAppSupabase 项目中。在项目根目录下,.env.local文件,然后配置如下内容:
# 配置App ID
export ONESIGNAL_APP_ID=your_onesignal_app_id
# 配置REST API Key
export ONESIGNAL_REST_API_KEY=your_onesignal_rest_api_key启动本地开发环境后,您就可以使用云函数来发送通知了。
npm run dev:functions.env.local注意:部署云函数时,一定要把ONESIGNAL_APP_ID和ONESIGNAL_REST_API_KEY配置到环境变量中。

在 EasyAppSwift客户端中,提供了一个专门用来测试通知的页面。您可以在这里测试通知的接收情况。文件位于
import SwiftUI
import UIKit
struct BackendSentNotificationTestView: View {
@Environment(\.colorScheme) private var colorScheme
@StateObject private var viewModel = BackendSentNotificationViewModel()
@State private var titleEn = ""
@State private var messageEn = ""
@State private var includeSubscriptionIdsText = ""
@State private var externalIdsText = ""
@State private var url = ""
@State private var additionalDataItems: [AdditionalDataItem] = [
AdditionalDataItem(key: "deeplink", value: "EasyApp://notifykit?screen=osTests")
]
@State private var showRawResponse = false
@State private var showPayloadDetails = false
@FocusState private var focusedField: Field?
var body: some View {
ScrollView {
VStack(spacing: 24) {
overviewCard
contentCard
audienceCard
additionalDataCard
optionalSettingsCard
sendButton
responseSection
}
.padding(.horizontal, 20)
.padding(.vertical, 24)
}
.background(Color(.systemGroupedBackground).ignoresSafeArea())
.navigationTitle("dev_backend_push_test")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("done") {
dismissKeyboard()
}
}
}
.showToast(
$viewModel.showToast,
message: viewModel.toastMessage,
isSuccess: viewModel.isSuccess
)
}
private var overviewCard: some View {
cardContainer {
VStack(alignment: .leading, spacing: 12) {
Label("dev_supabase_edge_function", systemImage: "cloud.fill")
.font(.headline)
Text("dev_supabase_explain")
.font(.footnote)
.foregroundStyle(.secondary)
Button {
prefillExample()
} label: {
Label("dev_fill_example_payload", systemImage: "sparkles")
.font(.subheadline)
}
.buttonStyle(.bordered)
}
}
}
private var contentCard: some View {
cardContainer {
VStack(alignment: .leading, spacing: 16) {
Text("dev_notification_content")
.font(.title3)
.fontWeight(.semibold)
VStack(spacing: 12) {
TextField("dev_title_optional", text: $titleEn)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .title)
VStack(alignment: .leading, spacing: 8) {
Text("dev_message_label")
.font(.footnote)
.foregroundStyle(.secondary)
ZStack(alignment: .topLeading) {
if messageEn.isEmpty {
Text("dev_enter_push_body")
.foregroundStyle(Color.secondary)
.padding(.top, 12)
.padding(.leading, 6)
}
TextEditor(text: $messageEn)
.frame(minHeight: 120, alignment: .topLeading)
.padding(8)
.background(Color(.secondarySystemBackground))
.roundedCorners(radius: 12)
}
}
}
}
}
}
private var audienceCard: some View {
cardContainer {
VStack(alignment: .leading, spacing: 12) {
Text("dev_audience_filters")
.font(.title3)
.fontWeight(.semibold)
Text("dev_audience_filters_hint")
.font(.footnote)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text("dev_external_user_ids")
.font(.caption)
.foregroundStyle(.secondary)
TextField(
"dev_external_user_ids_placeholder", text: $externalIdsText
)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .externalIds)
}
VStack(alignment: .leading, spacing: 6) {
Text("dev_subscription_ids")
.font(.caption)
.foregroundStyle(.secondary)
TextField(
"dev_subscription_ids_placeholder",
text: $includeSubscriptionIdsText
)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .includeSubscriptionIds)
}
}
}
}
}
private var additionalDataCard: some View {
cardContainer {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("dev_additional_data")
.font(.title3)
.fontWeight(.semibold)
Spacer()
Button {
withAnimation {
additionalDataItems.append(AdditionalDataItem())
}
} label: {
Label("dev_add", systemImage: "plus.circle.fill")
.labelStyle(.iconOnly)
}
.buttonStyle(.borderless)
.accessibilityLabel("dev_add_additional_data_accessibility")
}
if additionalDataItems.isEmpty {
Text("dev_additional_data_empty")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
VStack(spacing: 12) {
ForEach($additionalDataItems) { item in
AdditionalDataRow(item: item) {
removeAdditionalItem(id: item.id)
}
}
}
}
}
}
}
private var optionalSettingsCard: some View {
cardContainer {
VStack(alignment: .leading, spacing: 12) {
Text("dev_optional_settings")
.font(.title3)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 8) {
Text("dev_launch_url_label")
.font(.caption)
.foregroundStyle(.secondary)
TextField("dev_launch_url_placeholder", text: $url)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
}
}
}
}
private var sendButton: some View {
Button {
dismissKeyboard()
let payload = buildPayload()
Task {
await viewModel.sendNotification(with: payload)
}
} label: {
HStack {
Image(systemName: "paperplane.fill")
Text(
viewModel.showLoading
? "dev_sending" : "dev_send_notification")
}
.font(.headline)
.frame(maxWidth: .infinity)
.padding(10)
}
.buttonStyle(.borderedProminent)
.disabled(!canSend)
}
@ViewBuilder
private var responseSection: some View {
if let response = viewModel.lastResponse {
cardContainer {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 8) {
Image(
systemName: response.success
? "checkmark.circle.fill" : "exclamationmark.triangle.fill"
)
.foregroundStyle(response.success ? Color.green : Color.orange)
Text(
response.success
? "dev_result_notification_queued"
: "dev_result_notification_failed"
)
.font(.headline)
}
if let id = response.result?.id, !id.isEmpty {
detailRow(title: "dev_result_notification_id", value: id)
}
if let recipients = response.result?.recipients {
detailRow(title: "dev_result_recipients", value: "\(recipients)")
}
if let remaining = response.result?.remaining {
detailRow(title: "dev_result_remaining", value: "\(remaining)")
}
if let externalId = response.result?.externalId, !externalId.isEmpty {
detailRow(title: "dev_result_external_id", value: externalId)
}
if let errors = response.result?.errors, !errors.isEmpty {
detailRow(
title: "dev_result_errors", value: errors.readableText)
}
if response.success == false, let message = response.message, !message.isEmpty {
detailRow(title: "dev_result_message", value: message)
}
DisclosureGroup(isExpanded: $showPayloadDetails) {
VStack(alignment: .leading, spacing: 8) {
if let payload = viewModel.lastPayload {
detailRow(
title: "dev_detail_contents",
value: formatDictionary(payload.contents))
if let headings = payload.headings {
detailRow(
title: "dev_detail_headings",
value: formatDictionary(headings))
}
if let data = payload.data {
detailRow(
title: "dev_detail_data", value: formatDictionary(data))
}
if let externalIds = payload.externalIds {
detailRow(
title: "dev_detail_external_ids",
value: externalIds.joined(separator: ", "))
}
if let subscriptionIds = payload.includeSubscriptionIds {
detailRow(
title: "dev_detail_subscription_ids",
value: subscriptionIds.joined(separator: ", "))
}
if let targetUrl = payload.url, !targetUrl.isEmpty {
detailRow(title: "dev_detail_url", value: targetUrl)
}
}
if let jsonString = prettyPrintedResponse(response: response) {
DisclosureGroup(isExpanded: $showRawResponse) {
ScrollView {
Text(jsonString)
.font(.system(.caption, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxHeight: 160)
} label: {
Text("dev_raw_response_json")
.font(.subheadline)
.fontWeight(.semibold)
}
}
}
.padding(.top, 8)
} label: {
Text("dev_show_payload_details")
.font(.subheadline)
.fontWeight(.semibold)
}
}
}
} else if let error = viewModel.lastError {
cardContainer {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(Color.orange)
Text("dev_last_error")
.font(.headline)
}
Text(error.localizedDescription)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
private var canSend: Bool {
!messageEn.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !viewModel.showLoading
}
private func buildPayload() -> BackendNotificationPayload {
let content = messageEn.trimmingCharacters(in: .whitespacesAndNewlines)
let contents = ["en": content]
let heading = titleEn.trimmingCharacters(in: .whitespacesAndNewlines)
let headings = heading.isEmpty ? nil : ["en": heading]
let additionalData = Dictionary(
uniqueKeysWithValues: additionalDataItems.compactMap { item in
let key = item.key.trimmingCharacters(in: .whitespacesAndNewlines)
let value = item.value.trimmingCharacters(in: .whitespacesAndNewlines)
return key.isEmpty || value.isEmpty ? nil : (key, value)
}
)
let externalIds = parseList(from: externalIdsText)
let subscriptionIds = parseList(from: includeSubscriptionIdsText)
let targetUrl = url.trimmingCharacters(in: .whitespacesAndNewlines)
return BackendNotificationPayload(
contents: contents,
headings: headings,
data: additionalData.isEmpty ? nil : additionalData,
externalIds: externalIds.isEmpty ? nil : externalIds,
includeSubscriptionIds: subscriptionIds.isEmpty ? nil : subscriptionIds,
url: targetUrl.isEmpty ? nil : targetUrl
)
}
private func parseList(from text: String) -> [String] {
text
.replacingOccurrences(of: "\n", with: ",")
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}
private func removeAdditionalItem(id: UUID) {
withAnimation {
additionalDataItems.removeAll { $0.id == id }
}
}
private func prefillExample() {
titleEn = "Backend Push Test"
messageEn =
"Triggered from Supabase Edge Function. This verifies EasyAppNotifyKit deep link handling."
url = "EasyApp://notifykit?screen=osTests"
externalIdsText = "demo_user"
additionalDataItems = [
AdditionalDataItem(key: "deeplink", value: "EasyApp://notifykit?screen=osTests"),
AdditionalDataItem(key: "symbolImage", value: "square.and.arrow.up"),
AdditionalDataItem(key: "status", value: "success"),
]
}
private func dismissKeyboard() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
private func prettyPrintedResponse(response: BackendNotificationResponse) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let data = try? encoder.encode(response) else { return nil }
return String(data: data, encoding: .utf8)
}
private func formatDictionary(_ dictionary: [String: String]) -> String {
dictionary
.sorted { $0.key < $1.key }
.map { "\($0.key): \($0.value)" }
.joined(separator: "\n")
}
private func detailRow(title: String, value: String) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
Text(value)
.font(.subheadline)
.fontWeight(.medium)
.textSelection(.enabled)
}
}
private func cardContainer<Content: View>(@ViewBuilder content: () -> Content) -> some View {
content()
.frame(maxWidth: .infinity, alignment: .leading)
.padding(20)
.background(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(colorScheme == .dark ? Color(.secondarySystemBackground) : Color(.systemBackground))
)
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(Color(.separator).opacity(0.15))
)
}
}
private enum Field: Hashable {
case title
case includeSubscriptionIds
case externalIds
}
private struct AdditionalDataItem: Identifiable, Equatable {
let id: UUID
var key: String
var value: String
init(id: UUID = UUID(), key: String = "", value: String = "") {
self.id = id
self.key = key
self.value = value
}
}
private struct AdditionalDataRow: View {
@Binding var item: AdditionalDataItem
let onRemove: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 6) {
Text("dev_key")
.font(.caption)
.foregroundStyle(.secondary)
TextField("dev_additional_data_key_placeholder", text: $item.key)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
}
.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .leading, spacing: 6) {
Text("dev_value")
.font(.caption)
.foregroundStyle(.secondary)
TextField("dev_additional_data_value_placeholder", text: $item.value)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
}
.frame(maxWidth: .infinity, alignment: .leading)
Button(role: .destructive) {
onRemove()
} label: {
Image(systemName: "minus.circle.fill")
.foregroundStyle(.red)
}
.buttonStyle(.plain)
.padding(.top, 22)
}
}
}
}
#Preview {
NavigationStack {
BackendSentNotificationTestView()
}
}应用内显示通知消息
EasyApp 支持应用内显示消息。就是说,当您的 App 正在使用/在前台时,我们可以自定义消息显示的效果。在 EasyAppSwift客户端中,已经内置好了该功能。
在AppDelegate中,监听了addForegroundLifecycleListener事件,当通知到来时,就会触发onWillDisplay事件,我们可以在这里自定义消息显示的效果。但是,我们需要配置一些扩展参数来区分系统通知。
除了必传参数title和body之外,我们需要新增参数additionalData,该参数是一个字典,用于存储通知的扩展参数。我们定义了symbolImage和status两个参数来区分系统通知。
symbolImage: 系统通知图标status: 系统通知状态
如果没有配置symbolImage和status,则系统会按照默认效果显示通知。
如何来配置呢?
-
在 OneSignal Dashboard中,配置通知的扩展参数。

-
在 Supabase 云函数 中, 直接按照 Rest API 的请求参数配置即可。
// Build OneSignal payload (target_channel enforced to 'push')
const onesignalPayload: Notification = {
app_id,
target_channel: "push",
// if you set include_aliases, you should delete included_segments parameter
// included_segments: ["Active Subscriptions"],
include_aliases: {
external_id: external_ids,
},
is_ios: true,
contents,
...(headings ? { headings } : {}),
...(data !== undefined ? { data } : {}),
...rest,
};
const result = await oneSignalClient.createNotification(onesignalPayload);这列有一点要注意:如果您指定了include_aliases参数,那么您需要删除included_segments参数。included_segments: ["Active Subscriptions"] 表示发送给所有已经同意通知的用户。include_aliases表示您给特定的用户发送通知。在EasyAppSwiftUI模板中,用户登录时,模板会把用户的 userId 和 OneSignal 的 externalId 关联起来,因此,这里的 externalId 就是用户的 userId。
详情查看云函数send-onesignal-notification的实现。
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
if Constants.OneSignal.isEnabledNotification {
OneSignal.Notifications.addForegroundLifecycleListener(self)
}
return true
}
}
extension AppDelegate: OSNotificationClickListener, OSNotificationLifecycleListener {
func onWillDisplay(event: OSNotificationWillDisplayEvent) {
devLogger.info("Foreground notification: \(event.notification.title ?? "No Title")")
event.preventDefault()
guard let notifTitle = event.notification.title,
let notifMessage = event.notification.body,
let additionalData = event.notification.additionalData,
let symbol = additionalData["symbolImage"] as? String
else {
event.notification.display() // Show notification as usual
return
}
var notifHaptics: UINotificationFeedbackGenerator.FeedbackType = .warning
if let size = additionalData["status"] as? String {
if size == "error" {
notifHaptics = .error
} else if size == "success" {
notifHaptics = .success
}
UINotificationFeedbackGenerator().notificationOccurred(notifHaptics)
}
ShowNotification.showInAppNotification(
title: notifTitle,
message: notifMessage,
isSuccess: notifHaptics == .success,
duration: .seconds(seconds: 5),
systemImage: symbol,
handleViewTap: {
self.handleViewTap(additionalData: event.notification.additionalData)
}
)
}
}如何指定给某个用户/批量用户发送通知
- 首先EasyAppNotifyKit暴漏了 OneSignal关联外部 ID的Api,我们只需要在用户登录/注册时,绑定外部 ID 即可。
PushManager.shared.loginUser(userId)- 退出登录/删除用户时,需要调用
logoutUser方法。
PushManager.shared.logoutUser()- 关联上外部 ID 之后,我们就可以通过
send-onesignal-notification云函数,传入external_ids参数,来指定给某个用户/批量用户发送通知。
external_ids: 是 字符串数组类型。
详细逻辑,请参考客户端EasyAppSwiftUI/App/Developer/SubPages/Notifications/View/BackendSentNotificationTest.swift, 和云函数EasyAppSupabase/supabase/functions/send-onesignal-notification/index.ts的实现。
点击通知消息,如何跳转页面
点击通知消息,跳转页面时最常见的场景,EasyApp 已经提供有 Demo。
同样的,我们依赖additionalData参数,我们规定传入deeplink参数,deeplink的 值为: EasyApp://notifykit?screen=osTests
这里的逻辑,请参考EasyAppSwiftUI/App/Developer/SubPages/Notifications/View/BackendSentNotificationTest.swift。
在AppDelegate中,监听了addClickListener事件,当通知消息被点击时,就会触发onClick事件,我们可以在这里处理点击通知消息后的逻辑。
EasyApp 通过NotificationCenter广播了一个osNotificationClicked事件,我们可以在其他地方监听这个事件,来处理点击通知消息后的逻辑。
private func handleViewTap(additionalData: [AnyHashable: Any]? = nil) {
let ad = JSON(additionalData ?? [:])
let deeplink = ad["deeplink"].stringValue
if !deeplink.isEmpty {
// Broadcast click to allow host views to react (e.g., navigate)
NotificationCenter.default.post(
name: EasyAppNotifyKit.Notifications.osNotificationClicked,
object: nil,
userInfo: [
"deeplink": deeplink
]
)
}
}比如在 Demo 中,我们监听了这个事件,当点击通知消息后,就会打开一个测试页面。
var body: some Scene {
WindowGroup {
ContentView()
#if DEBUG
.sheet(isPresented: $showDeepLinkTest) {
OSDeepLinkTargetTestView()
}
.onReceive(
NotificationCenter.default.publisher(
for: EasyAppNotifyKit.Notifications.osNotificationClicked)
) { notification in
let deeplink = notification.userInfo?["deeplink"] as? String
if let deeplink, deeplink == "EasyApp://notifykit?screen=osTests" {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showDeepLinkTest = true
}
}
}
#endif
}
}其他情况以此类推,如果您有其他业务逻辑,您可以把业务参数放到additionalData参数中去,客户端接收到参数,您就可以按照自己的业务逻辑来处理了。
关于上架:
如果您暂时用不到推送能力,建议您删除模板中通知相关代码。删掉EasyAppNotifyKit依赖。删掉EasyAppSwiftUI/App/Developer整个文件夹,以免被苹果审核人员误认为您使用了推送能力。
Last updated on