Skip to content

Commit 37683db

Browse files
notif ios: Navigate when app launched from notification
Introduces a new Pigeon API file, and adds the corresponding bindings in Swift. Unlike the `pigeon/android_notifications.dart` API this doesn't use the ZulipPlugin hack, as that is only needed when we want the Pigeon functions to be available inside a background isolate (see doc in `zulip_plugin/pubspec.yaml`). Since the notification tap will trigger an app launch first (if not running already) anyway, we can be sure that these new functions won't be running on a Dart background isolate, thus not needing the ZulipPlugin hack.
1 parent 9d3e500 commit 37683db

15 files changed

+862
-3
lines changed

ios/Runner.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
1414
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
1515
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
16+
B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34E9F082D776BEB0009AED2 /* Notifications.g.swift */; };
1617
F311C174AF9C005CE4AADD72 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EAE3F3F518B95B7BFEB4FE7 /* Pods_Runner.framework */; };
1718
/* End PBXBuildFile section */
1819

@@ -48,6 +49,7 @@
4849
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
4950
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
5051
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
52+
B34E9F082D776BEB0009AED2 /* Notifications.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.g.swift; sourceTree = "<group>"; };
5153
B3AF53A72CA20BD10039801D /* Zulip.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Zulip.xcconfig; path = Flutter/Zulip.xcconfig; sourceTree = "<group>"; };
5254
/* End PBXFileReference section */
5355

@@ -115,6 +117,7 @@
115117
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
116118
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
117119
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
120+
B34E9F082D776BEB0009AED2 /* Notifications.g.swift */,
118121
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
119122
);
120123
path = Runner;
@@ -297,6 +300,7 @@
297300
buildActionMask = 2147483647;
298301
files = (
299302
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
303+
B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */,
300304
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
301305
);
302306
runOnlyForDeploymentPostprocessing = 0;

ios/Runner/AppDelegate.swift

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ import Flutter
88
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
99
) -> Bool {
1010
GeneratedPluginRegistrant.register(with: self)
11+
let controller = window?.rootViewController as! FlutterViewController
12+
13+
// Retrieve the remote notification payload from launch options;
14+
// this will be null if the launch wasn't triggered by a notification.
15+
let notificationPayload = launchOptions?[.remoteNotification] as? [AnyHashable : Any]
16+
let api = NotificationHostApiImpl(notificationPayload.map { NotificationDataFromLaunch(payload: $0) })
17+
NotificationHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
18+
1119
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1220
}
1321
}
22+
23+
private class NotificationHostApiImpl: NotificationHostApi {
24+
private let maybeDataFromLaunch: NotificationDataFromLaunch?
25+
26+
init(_ maybeDataFromLaunch: NotificationDataFromLaunch?) {
27+
self.maybeDataFromLaunch = maybeDataFromLaunch
28+
}
29+
30+
func getNotificationDataFromLaunch() -> NotificationDataFromLaunch? {
31+
maybeDataFromLaunch
32+
}
33+
}

ios/Runner/Notifications.g.swift

+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
2+
// See also: https://pub.dev/packages/pigeon
3+
4+
import Foundation
5+
6+
#if os(iOS)
7+
import Flutter
8+
#elseif os(macOS)
9+
import FlutterMacOS
10+
#else
11+
#error("Unsupported platform.")
12+
#endif
13+
14+
/// Error class for passing custom error details to Dart side.
15+
final class PigeonError: Error {
16+
let code: String
17+
let message: String?
18+
let details: Sendable?
19+
20+
init(code: String, message: String?, details: Sendable?) {
21+
self.code = code
22+
self.message = message
23+
self.details = details
24+
}
25+
26+
var localizedDescription: String {
27+
return
28+
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
29+
}
30+
}
31+
32+
private func wrapResult(_ result: Any?) -> [Any?] {
33+
return [result]
34+
}
35+
36+
private func wrapError(_ error: Any) -> [Any?] {
37+
if let pigeonError = error as? PigeonError {
38+
return [
39+
pigeonError.code,
40+
pigeonError.message,
41+
pigeonError.details,
42+
]
43+
}
44+
if let flutterError = error as? FlutterError {
45+
return [
46+
flutterError.code,
47+
flutterError.message,
48+
flutterError.details,
49+
]
50+
}
51+
return [
52+
"\(error)",
53+
"\(type(of: error))",
54+
"Stacktrace: \(Thread.callStackSymbols)",
55+
]
56+
}
57+
58+
private func isNullish(_ value: Any?) -> Bool {
59+
return value is NSNull || value == nil
60+
}
61+
62+
private func nilOrValue<T>(_ value: Any?) -> T? {
63+
if value is NSNull { return nil }
64+
return value as! T?
65+
}
66+
67+
func deepEqualsNotifications(_ lhs: Any?, _ rhs: Any?) -> Bool {
68+
let cleanLhs = nilOrValue(lhs) as Any?
69+
let cleanRhs = nilOrValue(rhs) as Any?
70+
switch (cleanLhs, cleanRhs) {
71+
case (nil, nil):
72+
return true
73+
74+
case (nil, _), (_, nil):
75+
return false
76+
77+
case is (Void, Void):
78+
return true
79+
80+
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
81+
return cleanLhsHashable == cleanRhsHashable
82+
83+
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
84+
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
85+
for (index, element) in cleanLhsArray.enumerated() {
86+
if !deepEqualsNotifications(element, cleanRhsArray[index]) {
87+
return false
88+
}
89+
}
90+
return true
91+
92+
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
93+
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
94+
for (key, cleanLhsValue) in cleanLhsDictionary {
95+
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
96+
if !deepEqualsNotifications(cleanLhsValue, cleanRhsDictionary[key]!) {
97+
return false
98+
}
99+
}
100+
return true
101+
102+
default:
103+
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
104+
return false
105+
}
106+
}
107+
108+
func deepHashNotifications(value: Any?, hasher: inout Hasher) {
109+
if let valueList = value as? [AnyHashable] {
110+
for item in valueList { deepHashNotifications(value: item, hasher: &hasher) }
111+
return
112+
}
113+
114+
if let valueDict = value as? [AnyHashable: AnyHashable] {
115+
for key in valueDict.keys {
116+
hasher.combine(key)
117+
deepHashNotifications(value: valueDict[key]!, hasher: &hasher)
118+
}
119+
return
120+
}
121+
122+
if let hashableValue = value as? AnyHashable {
123+
hasher.combine(hashableValue.hashValue)
124+
}
125+
126+
return hasher.combine(String(describing: value))
127+
}
128+
129+
130+
131+
/// Generated class from Pigeon that represents data sent in messages.
132+
struct NotificationDataFromLaunch: Hashable {
133+
/// The raw payload that is attached to the notification,
134+
/// holding the information required to carry out the navigation.
135+
///
136+
/// See [NotificationHostApi.getNotificationDataFromLaunch].
137+
var payload: [AnyHashable?: Any?]
138+
139+
140+
// swift-format-ignore: AlwaysUseLowerCamelCase
141+
static func fromList(_ pigeonVar_list: [Any?]) -> NotificationDataFromLaunch? {
142+
let payload = pigeonVar_list[0] as! [AnyHashable?: Any?]
143+
144+
return NotificationDataFromLaunch(
145+
payload: payload
146+
)
147+
}
148+
func toList() -> [Any?] {
149+
return [
150+
payload
151+
]
152+
}
153+
static func == (lhs: NotificationDataFromLaunch, rhs: NotificationDataFromLaunch) -> Bool {
154+
return deepEqualsNotifications(lhs.toList(), rhs.toList()) }
155+
func hash(into hasher: inout Hasher) {
156+
deepHashNotifications(value: toList(), hasher: &hasher)
157+
}
158+
}
159+
160+
private class NotificationsPigeonCodecReader: FlutterStandardReader {
161+
override func readValue(ofType type: UInt8) -> Any? {
162+
switch type {
163+
case 129:
164+
return NotificationDataFromLaunch.fromList(self.readValue() as! [Any?])
165+
default:
166+
return super.readValue(ofType: type)
167+
}
168+
}
169+
}
170+
171+
private class NotificationsPigeonCodecWriter: FlutterStandardWriter {
172+
override func writeValue(_ value: Any) {
173+
if let value = value as? NotificationDataFromLaunch {
174+
super.writeByte(129)
175+
super.writeValue(value.toList())
176+
} else {
177+
super.writeValue(value)
178+
}
179+
}
180+
}
181+
182+
private class NotificationsPigeonCodecReaderWriter: FlutterStandardReaderWriter {
183+
override func reader(with data: Data) -> FlutterStandardReader {
184+
return NotificationsPigeonCodecReader(data: data)
185+
}
186+
187+
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
188+
return NotificationsPigeonCodecWriter(data: data)
189+
}
190+
}
191+
192+
class NotificationsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
193+
static let shared = NotificationsPigeonCodec(readerWriter: NotificationsPigeonCodecReaderWriter())
194+
}
195+
196+
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
197+
protocol NotificationHostApi {
198+
/// Retrieves notification data if the app was launched by tapping on a notification.
199+
///
200+
/// Returns `launchOptions.remoteNotification`,
201+
/// which is the raw APNs data dictionary
202+
/// if the app launch was opened by a notification tap,
203+
/// else null. See Apple doc:
204+
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
205+
func getNotificationDataFromLaunch() throws -> NotificationDataFromLaunch?
206+
}
207+
208+
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
209+
class NotificationHostApiSetup {
210+
static var codec: FlutterStandardMessageCodec { NotificationsPigeonCodec.shared }
211+
/// Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`.
212+
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
213+
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
214+
/// Retrieves notification data if the app was launched by tapping on a notification.
215+
///
216+
/// Returns `launchOptions.remoteNotification`,
217+
/// which is the raw APNs data dictionary
218+
/// if the app launch was opened by a notification tap,
219+
/// else null. See Apple doc:
220+
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
221+
let getNotificationDataFromLaunchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
222+
if let api = api {
223+
getNotificationDataFromLaunchChannel.setMessageHandler { _, reply in
224+
do {
225+
let result = try api.getNotificationDataFromLaunch()
226+
reply(wrapResult(result))
227+
} catch {
228+
reply(wrapError(error))
229+
}
230+
}
231+
} else {
232+
getNotificationDataFromLaunchChannel.setMessageHandler(nil)
233+
}
234+
}
235+
}

lib/host/notifications.dart

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export './notifications.g.dart';

0 commit comments

Comments
 (0)