From de32d54b74f61c94f9f6ed3368616a94c4d6f9ed Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Sat, 14 Feb 2026 20:48:23 +0100
Subject: [PATCH 1/9] Added Notification Playground
---
Android/app/src/main/AndroidManifest.xml | 24 +++
.../src/main/kotlin/NotificationReceiver.kt | 34 ++++
.../src/main/res/drawable/ic_notification.png | Bin 0 -> 2598 bytes
Darwin/Entitlements.plist | 7 +-
Package.swift | 7 +-
Project.xcworkspace/contents.xcworkspacedata | 2 +-
Sources/Showcase/NotificationPlayground.swift | 165 ++++++++++++++++++
Sources/Showcase/PlaygroundListView.swift | 5 +
.../Showcase/Resources/Localizable.xcstrings | 42 +++--
9 files changed, 268 insertions(+), 18 deletions(-)
create mode 100644 Android/app/src/main/kotlin/NotificationReceiver.kt
create mode 100644 Android/app/src/main/res/drawable/ic_notification.png
create mode 100644 Sources/Showcase/NotificationPlayground.swift
diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
index e112ba8..64b817f 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -12,6 +12,10 @@
android:name="android.hardware.camera.autofocus"
android:required="false" />
+
+
+
+
@@ -30,7 +34,26 @@
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
pbnOmk4Zof#Ap(b2hG7wP_95pp^Ol?)101*%pR5TlvP*U6dr)jKQ
z(6Yu-XS7nM&Gv0vo2=Z+rd1^>3!8rbi#p{vb?$S{_ucco@3)-u-RHTvjL>fk^)2-Q
z05A*;poKva)J{EJXpPj-C?HYDiV9?hgaD3^t_SGCOaVBg!Jr3#Spvuj9RMO=Rv-2C
zFzggY2f`2lXa&rn$yRUy#1uXqlG+gfO~})vWN@V{zJMtaCZZXMLbNN+14n>tIwd+E
zd0+|rlMdN{=Ed?7NX!-mtP%sjoO#*_0}2nI007SChDS-FLV`)(2?aRj24O4n9&~aCjZOGO!g}I|;`tGp^15;a*cj&x5kg!Fie{K1&%-->(
z#+M#=seh68?6Ia+jPLe1x16o^@gcMo4_wJK^!5>%lInb1jeYaBPa-^gvxiuFW>c4O
z+Md3H_YS&p*vXt`;->d+-bas(&NjPYakjSh+YgHKs3X}#(89mT=k6@e(z(4dL2DS+
zYzJfXIH-xQHz~@FZrXEhg>-(EqF_ui8XSXIEVJ3Q^8S+=%fo&vtMPK*t3L2LzvYiW
z8;=WXQLViEn3@5VwRL9VBR$>kvyiRl9^}XDY7*JjQHzwdznJ8#l*}~{8M^r`EQgof
zj3IAOZ}Yu@I(V%k72fPt6lOK_oat$eXy~vI=|*8boEmU=XC2waOwxy=o$PD=c$BkL?;1b__@j(_P)u~X7~))a^S$B
z%(=))cXSz7y}&RstFxe$qBn>bOr_EuKd|TR=uI{V&U7wn8*Lj!V-JGJGmiU=qccZJ
zTdl9JzInu)X3y-G?%chcyXJZpFC_^zhN$Am)Ji{6
zK5u>Iln1VBC*_eY1(_#jDNlsW61YAJt_Ua&6H;+vB-ZG^rU!qU9r*sIDuN1IZdl@5
z(Wu+7_v-4Z95aplz57PnXX@~q{U-Iw=jzdOx=mY8WO83_Xng%m2j`q;foT%b4|y4@
z4jWbQe4Asvj>&&woVB{Vm3mq@r>8K=$9c>Svwzuu1j|!Kkh*Rm%-3}c2HE%j32tI^
z??3#aJF+-iqLDSD3V|@0%UT*%wjom~+Cr
zQ`W*658Y&Au*3OV?^zUGWEmqBK`&mm{dbNNAsyuBriT^zUDIMah@P`vp^
zd)*&GdXX7E*OD2^ilLHm1*Wtrn~aF4>Dtyz*m$lx$RfLoIZM9PXgs#nHkv-L`lX*{
zMd1@z2tN5*t&BPJ)F}oo-+JwbLJ*}$gS-5w{s;1B;v0GghJ)+x%)j@j#ABbmVM6h(q+_AkBkjjhdW2=^HWKs+U?b*$VbBw^(>wG7
zx;>405z*uZ;;Zr=BUI|23~Kfpg`6$N{-6Se7<{i;HddHtrsQ3y!Z4&3U0y
z?AEc=Du^NK7XX1+CCmpop!?TGdtDUcpYtD?rp{tqT}>X-lk
literal 0
HcmV?d00001
diff --git a/Darwin/Entitlements.plist b/Darwin/Entitlements.plist
index 0c67376..a28da91 100644
--- a/Darwin/Entitlements.plist
+++ b/Darwin/Entitlements.plist
@@ -1,5 +1,10 @@
-
+
+ aps-environment
+ development
+ com.apple.developer.aps-environment
+ development
+
diff --git a/Package.swift b/Package.swift
index b733170..5170fa5 100644
--- a/Package.swift
+++ b/Package.swift
@@ -10,7 +10,12 @@ let package = Package(
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
- .package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),
+ // TODO: Remove temporary reference to skip foundation after PR has been merged (see: https://github.com/skiptools/skip-foundation/pull/93)
+ .package(url: "https://github.com/fhasse95/skip-foundation.git", branch: "Added-Calendar-Enumeration-Support"),
+
+ // TODO: Update skip ui package URL and version after PR has been merged (see: TODO)
+ .package(url: "https://github.com/fhasse95/skip-ui.git", branch: "Triggered-Notification-Support"),
+
.package(url: "https://source.skip.tools/skip-av.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-kit.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-sql.git", "0.12.1"..<"2.0.0"),
diff --git a/Project.xcworkspace/contents.xcworkspacedata b/Project.xcworkspace/contents.xcworkspacedata
index 0f2d128..e1c9da1 100644
--- a/Project.xcworkspace/contents.xcworkspacedata
+++ b/Project.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,4 @@
-
\ No newline at end of file
+
diff --git a/Sources/Showcase/NotificationPlayground.swift b/Sources/Showcase/NotificationPlayground.swift
new file mode 100644
index 0000000..de11688
--- /dev/null
+++ b/Sources/Showcase/NotificationPlayground.swift
@@ -0,0 +1,165 @@
+// Copyright 2023–2026 Skip
+import SwiftUI
+
+struct NotificationPlayground: View {
+
+ // MARK: - Variables
+ @State private var isAuthorized: Bool = false
+
+ @State private var now = Date()
+ @State private var nextTriggerDate: Date?
+ private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
+ private var secondsUntilNextTrigger: Int? {
+ guard let nextTriggerDate = self.nextTriggerDate else { return nil }
+ let seconds = Int(nextTriggerDate.timeIntervalSince(self.now))
+ return seconds > 0 ? seconds : nil
+ }
+
+ private let notificationCenterDelegate = NotificationCenterDelegate()
+
+ // MARK: - Initialization
+
+ /// Initializes a new instance of the `NotificationPlayground` struct.
+ init() {
+ let notificationCenter = UNUserNotificationCenter.current()
+ notificationCenter.delegate = self.notificationCenterDelegate
+ }
+
+ // MARK: - View
+ var body: some View {
+ VStack(spacing: 20) {
+ Text("Is Authorized: \(self.isAuthorized ? "True" : "False")")
+
+ VStack(spacing: 10) {
+ Button("Request Notification Permission") {
+ Task {
+ await self.requestNotificationPermission()
+ }
+ }
+ .buttonStyle(.bordered)
+ .disabled(self.isAuthorized)
+
+ Button("Trigger Immediate Notification") {
+ Task {
+ await self.addNotificationRequest(
+ title: "Title",
+ body: "Body",
+ identifier: UUID().uuidString
+ )
+ }
+ }
+ .backgroundStyle(.blue)
+ .buttonStyle(.borderedProminent)
+ .disabled(!self.isAuthorized)
+
+ Button("Trigger Scheduled Notification") {
+ Task {
+ await self.addNotificationRequest(
+ title: "Title",
+ body: "Body",
+ identifier: UUID().uuidString,
+ trigger: {
+ let result = UNTimeIntervalNotificationTrigger(
+ timeInterval: 10,
+ repeats: false
+ )
+ self.nextTriggerDate = result.nextTriggerDate()
+ return result
+ }()
+ )
+ }
+ }
+ .backgroundStyle(.blue)
+ .buttonStyle(.borderedProminent)
+ .disabled(!self.isAuthorized || self.secondsUntilNextTrigger != nil)
+ }
+
+ if let seconds = self.secondsUntilNextTrigger {
+ Text("Next notification in \(seconds) s")
+ .foregroundStyle(.secondary)
+
+ Button("Remove all pending notifications") {
+ self.removeAllPendingNotifications()
+ self.nextTriggerDate = nil
+ }
+ .foregroundStyle(.red)
+ } else {
+ Text("No scheduled notification")
+ .foregroundStyle(.secondary)
+ }
+ }
+ .navigationTitle("Notifications")
+ .onAppear {
+ Task {
+ await self.requestNotificationPermission()
+ }
+ }
+ .onReceive(self.timer) { input in
+ self.now = input
+ }
+ }
+
+ /// Requests the permission to push notifications.
+ private func requestNotificationPermission() async {
+ let options: UNAuthorizationOptions = [.alert, .sound, .badge]
+ let notificationCenter = UNUserNotificationCenter.current()
+ self.isAuthorized = (try? await notificationCenter.requestAuthorization(options: options)) ?? false
+ }
+
+ /// Adds a new notification request.
+ ///
+ /// - Parameters:
+ /// - title: The notification title.
+ /// - body: The notification body.
+ /// - identifier: The notification identifier.
+ /// - trigger: The (optional) notification trigger. If nil, the notification will trigger immediately.
+ private func addNotificationRequest(
+ title: String,
+ body: String,
+ identifier: String,
+ trigger: UNNotificationTrigger? = nil
+ ) async {
+
+ // Create a new notificiation content.
+ let content = UNMutableNotificationContent()
+ content.title = title
+ content.body = body
+ content.userInfo = [
+ "identifier": UUID().uuidString
+ ]
+
+ // Create a new notification request.
+ let request = UNNotificationRequest(
+ identifier: identifier,
+ content: content,
+ trigger: trigger
+ )
+
+ // Schedule the delivery of the notification.
+ let notificationCenter = UNUserNotificationCenter.current()
+ try? await notificationCenter.add(request)
+ }
+
+ /// Removes all pending notification requests.
+ private func removeAllPendingNotifications() {
+ let notificationCenter = UNUserNotificationCenter.current()
+ notificationCenter.removeAllPendingNotificationRequests()
+ }
+}
+
+// MARK: - UNUserNotificationCenterDelegate
+private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
+
+ /// Asks the delegate how to handle a notification that arrived while the app was running in the foreground.
+ ///
+ /// - Parameters:
+ /// - center: The shared user notification center object that received the notification.
+ /// - notification: The notification that is about to be delivered.
+ /// - Returns: Constants indicating how to present a notification in a foreground app.
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification
+ ) async -> UNNotificationPresentationOptions {
+ return [.banner, .sound, .badge]
+ }
+}
diff --git a/Sources/Showcase/PlaygroundListView.swift b/Sources/Showcase/PlaygroundListView.swift
index 83e8d69..ff569fb 100644
--- a/Sources/Showcase/PlaygroundListView.swift
+++ b/Sources/Showcase/PlaygroundListView.swift
@@ -42,6 +42,7 @@ enum PlaygroundType: CaseIterable, View {
case menu
case modifier
case navigationStack
+ case notification
case observable
case offsetPosition
case onSubmit
@@ -164,6 +165,8 @@ enum PlaygroundType: CaseIterable, View {
return LocalizedStringResource("Modifiers")
case .navigationStack:
return LocalizedStringResource("NavigationStack")
+ case .notification:
+ return LocalizedStringResource("Notification")
case .observable:
return LocalizedStringResource("Observable")
case .offsetPosition:
@@ -329,6 +332,8 @@ enum PlaygroundType: CaseIterable, View {
ModifierPlayground()
case .navigationStack:
NavigationStackPlayground()
+ case .notification:
+ NotificationPlayground()
case .observable:
ObservablePlayground()
case .offsetPosition:
diff --git a/Sources/Showcase/Resources/Localizable.xcstrings b/Sources/Showcase/Resources/Localizable.xcstrings
index 5267d18..223f405 100644
--- a/Sources/Showcase/Resources/Localizable.xcstrings
+++ b/Sources/Showcase/Resources/Localizable.xcstrings
@@ -1372,12 +1372,6 @@
},
"Footer 3" : {
- },
- "Footer line 1" : {
-
- },
- "Footer line 2" : {
-
},
"ForEach index row: %lld" : {
@@ -1478,12 +1472,6 @@
},
"Header" : {
- },
- "Header line 1" : {
-
- },
- "Header line 2" : {
-
},
"height: 50" : {
@@ -1630,6 +1618,9 @@
},
"iOS 17 / macOS 14 required for Observation framework" : {
+ },
+ "Is Authorized: %@" : {
+
},
"isOn" : {
@@ -1941,6 +1932,9 @@
},
"New Key" : {
+ },
+ "Next notification in %lld s" : {
+
},
"Nil data" : {
@@ -1953,6 +1947,9 @@
},
"NO" : {
+ },
+ "No scheduled notification" : {
+
},
"No URL" : {
@@ -1971,6 +1968,12 @@
},
"Note: tint should not affect Label appearance" : {
+ },
+ "Notification" : {
+ "extractionState" : "manual"
+ },
+ "Notifications" : {
+
},
"Observable" : {
@@ -2288,9 +2291,15 @@
},
"Remapped URL" : {
+ },
+ "Remove all pending notifications" : {
+
},
"Repository item tap count: %lld" : {
+ },
+ "Request Notification Permission" : {
+
},
"Requires iOS 17+" : {
@@ -2330,9 +2339,6 @@
},
"Row 2.1" : {
- },
- "Row 3.1" : {
-
},
"Row 3a" : {
@@ -3136,6 +3142,12 @@
},
"Transition" : {
+ },
+ "Trigger Immediate Notification" : {
+
+ },
+ "Trigger Scheduled Notification" : {
+
},
"Two Buttons" : {
From feea69e3a5ca98006368cf31ab0bae66b84188c8 Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Sun, 15 Feb 2026 01:55:40 +0100
Subject: [PATCH 2/9] Added Skip Notify Example from Skip Showcase Fuse
---
.../src/main/res/drawable/ic_notification.png | Bin 2598 -> 2555 bytes
Package.swift | 9 +-
Sources/Showcase/NotificationPlayground.swift | 127 ++++++++++++------
.../Showcase/Resources/Localizable.xcstrings | 20 ++-
4 files changed, 110 insertions(+), 46 deletions(-)
diff --git a/Android/app/src/main/res/drawable/ic_notification.png b/Android/app/src/main/res/drawable/ic_notification.png
index a1af53e38621de3a3016e9f48060d015add2f27d..969d5032ae8fd72c0dbadda159fd8a50cf8d5084 100644
GIT binary patch
delta 1486
zcmV;<1u^=j6#Emf90Lk9R4_3*H8DCfII|`LDgl3tItqmV00oapL_t(&1?`yoi&jM#
z$2V)$BDK6^TIpH_Wt3&vh3KM*QlV5Dc7Y%*=V;rl$YZy)&?1jm=yC
zAQ*oN6JZjZ2!o*~Y=;f79A1HSumko&x2u^RoCovZYxtvtpD14nSHj`Ytr|${GhscH
zbv#?Xi#m(oOy~joT!zbh*bUjvRek@Rx~pI^902=F{b=b?_ywwVHf85N*bLKQe`vUR
z)54w50jW-PCEvZ#3VorWnu_jm0E%R%PlltR9kzmlu8Z7cOQ38PS7UP_41+#k#|Oa}cpR$in_sbe7cPKKtpxT-
zO`)W$@lUWh0aA^+`5%OxP&N^6?rmVZHOtFtG3Yea@FvPGg|cw$g^h=RCr`>(obP|W
zxCUy{anxP|vEw-Hr^4M}xKxoB*EDz!iY6tr@p75dvP)zX4h}7FvXZg|;H4I;KQHHk
z-$lQ{2kVlWwQ>4hS=necqhoeHeL*U<)`fPbR}`9vlhEl^6+6
zfcHSwukY?VBcLYTPwi9#HZnHj-PwPf3wa$f?;U7?AVY5sxJq#y-)oM4d^D8};B<{5
z4I#Y~;?88#UWQX4+`Ml7%V9BicPs$IFLk0(R9&r@7KPCD8CMf|&vrQPU7xez7^q7}
z@slf03}-B_6XtnJgqbASbPD_cDf>9zm3k4*0QW>gAB1bTFQlK^RQxX*fIyhFy?@^Dg-qidHfB
z`~thHpc(3_(RkYop`p~%ka>R%#zWYchQ$PU6&z%$uK5n~O*j_nDv!D=;72IxfMx#P
zT?dmPgq_b8XamEe$cKZx3qoCz^-(X}+zQ`Am2mJw&v5W$3bIr8K-p!t8k++{rFuSf
zdLyidqFD@Oe$UK;p%Cj5UN%9n%RZeT|8^-=XP3$F_y{(_2sjFgr2l`jVE`Cb-W#eN
z49jyM@94D5lWYYvL^j|ru&2R6WoIz^ZpUwcSYvqk5VE%6eMxMbN{5poRi4VX5s$_h
z@Cf`2MRLFr%NcehQyP{ki1YK7FD^F(jsS03m)qH14o|`7U?*mR8@3htL!SJeT9YZz
zmwP0oc=M-h3iF+@^C5q4nLQ=kh+cB48Eyh+G=xBSZX@pj&?eSMUY0?y%RX1e#$8GG
zrP6b-2eQt6w_^{(a8M3-5-b8Yx|`O^%cHj+#4>a*23Ii8w|VoSCXJ+u5!xfqV`%FD(X*#TeqT{g20G;_+kau5@-B@|rEQAiI(%IO?m|qLG
zfWI@kMaHW)uOE9GA?{#N+712;oCUq$Um-i<2i^OS3M)3hpW0wF)J2Wh@$L`rjuGIQ
o?-A@Li5JvoQ1;salmC$Z1W)e@m)^{gQUCw|07*qoM6N<$g4+DW%K!iX
delta 1529
zcmVjc7E6
zh$d=HL{Z{1JVYRk?}DO2Ev@n>_4nU&Z>G!LIkUZc+Yfq@-<{c=GiPRYXU;i$`}O-@
z`u2Yb6v`RCv$IqEhrk%9g*h-8M!`VX2Roq=-hyq=QYaLTQ1-1b`D(Zjo`qk*lD|=S
z7p{ks`gHb8U<53J9gtPp=&%~5LshRYgh9tZ1GGa=;@NdyMD{WBj>+nAT`@s?&?!7S2oSdbA5e2I?AfsvWBMn!9*OPzH
zJ_zIlOuOs7bnQg1>oa|I|4loEfPy9r7AnXWPLD5MRbL8rO~xt7OTu7(u(g!?sd
zAL{Z{%is{WqU-YZzXrjGwSZ~yWV-Z6(`UaslM;0WLj}bs@>2GPYeCLpJ*p
zp#O&=p{|8I1Xi%yjLtD}zuZb0h1+2VgoqCRV$dI7Md4cUL-|pf=mCFCFLubYf?@6H
zD_{eJh_=iMDDG?OdYgI^zC3G5p7ZWzi-C0}^ab`3XwO@sfOnf~glm0je(mYF&ecz=
z_b7yu*LT8JjNT8=z&~jNblq3JlgxQ`bFazMZ>*=S*yN3TJRNz0lR*po0$qflzWb6_=W0>uIaFS*{~?o7DW
z7un^YH<%_3ZlBC`tWJgR(yVvgZ=-x2bRe|=aUz@zO6U~4jtYNB+&d251YbP4IrF65
z4j9)H(uap7@8_oiN@o;
z&dPj9+m;j)O(>NPSK7`~v
zO(hKJRo)2qLJfadqW^(5!3Ur}c@-l@!}%}^ir);>lOnge9G2js<(l&m7NJ+K6!}B&
z9Xy(gqOGUI?>EpJwEC%C_9ue*U^A~wMqwPh0#;s+N@meQYa##1yQ^h@p|BM8KwhFZ
zq_m`Wgu$v{0c-@HJEYkZJ^96ORJ^G8Y8X`BW)-wTR?%Ue*IzbwLjE5wA**NLIP%s}
zZ-P+K0e=~0z@U(^Dz2l6fD*bdA#GbJf2xNmc}*zy6VK%IjiK0a8p!7>qPBoi>hJOI
f*0%#{{~`Go90LYC|D%kk00000NkvXXu0mjfY6j4>
diff --git a/Package.swift b/Package.swift
index 5170fa5..64b2ff2 100644
--- a/Package.swift
+++ b/Package.swift
@@ -10,11 +10,10 @@ let package = Package(
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
- // TODO: Remove temporary reference to skip foundation after PR has been merged (see: https://github.com/skiptools/skip-foundation/pull/93)
- .package(url: "https://github.com/fhasse95/skip-foundation.git", branch: "Added-Calendar-Enumeration-Support"),
- // TODO: Update skip ui package URL and version after PR has been merged (see: TODO)
- .package(url: "https://github.com/fhasse95/skip-ui.git", branch: "Triggered-Notification-Support"),
+ // TODO: Update package URLs and version after PRs have been merged (see: TODO)
+ .package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-foundation"),
+ .package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-ui"),
.package(url: "https://source.skip.tools/skip-av.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-kit.git", "0.0.0"..<"2.0.0"),
@@ -25,6 +24,7 @@ let package = Package(
.package(url: "https://source.skip.tools/skip-keychain.git", "0.3.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-marketplace.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-authentication-services.git", "0.0.0"..<"2.0.0"),
+ .package(url: "https://source.skip.tools/skip-notify.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "Showcase", dependencies: [
@@ -38,6 +38,7 @@ let package = Package(
.product(name: "SkipKeychain", package: "skip-keychain"),
.product(name: "SkipMarketplace", package: "skip-marketplace"),
.product(name: "SkipAuthenticationServices", package: "skip-authentication-services"),
+ .product(name: "SkipNotify", package: "skip-notify"),
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
]
)
diff --git a/Sources/Showcase/NotificationPlayground.swift b/Sources/Showcase/NotificationPlayground.swift
index de11688..7a9221a 100644
--- a/Sources/Showcase/NotificationPlayground.swift
+++ b/Sources/Showcase/NotificationPlayground.swift
@@ -1,37 +1,102 @@
// Copyright 2023–2026 Skip
import SwiftUI
+import SkipKit
+import SkipNotify
struct NotificationPlayground: View {
+ @State var notificationPermission: String = ""
- // MARK: - Variables
- @State private var isAuthorized: Bool = false
+ var body: some View {
+ List {
+ Section {
+ Text("Permission Status: \(self.notificationPermission)")
+ .task {
+ self.notificationPermission = await PermissionManager.queryPostNotificationPermission().rawValue
+ }
+ .foregroundStyle(self.notificationPermission == "authorized" ? .green : .red)
+
+ Button("Request Push Notification Permission") {
+ Task { @MainActor in
+ do {
+ self.notificationPermission = try await PermissionManager.requestPostNotificationPermission(alert: true, sound: false, badge: true).rawValue
+ logger.log("obtained push notification permission: \(self.notificationPermission)")
+ } catch {
+ logger.error("error obtaining push notification permission: \(error)")
+ self.notificationPermission = "error: \(error)"
+ }
+ }
+ }
+ .buttonStyle(.plain)
+ }
+
+ Section {
+ NavigationLink("Skip Notify") {
+ SkipNotifyNotificationPlaygroundView()
+ }
+
+ NavigationLink("Local Notifications") {
+ LocalNotificationPlaygroundView()
+ }
+ }
+ }
+ .navigationTitle("Notification")
+ }
+}
+
+struct SkipNotifyNotificationPlaygroundView: View {
+ @State var token: String = ""
+ var body: some View {
+ VStack {
+ HStack {
+ TextField("Push Notification Client Token", text: $token)
+ .textFieldStyle(.roundedBorder)
+ Button("Copy") {
+ UIPasteboard.general.string = token
+ }
+ .buttonStyle(.automatic)
+ }
+
+ Button("Generate Push Notification Token") {
+ Task { @MainActor in
+ do {
+ self.token = try await SkipNotify.shared.fetchNotificationToken()
+ logger.log("obtained push notification token: \(self.token)")
+ } catch {
+ logger.error("error obtaining push notification token: \(error)")
+ }
+ }
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .navigationTitle("Skip Notify")
+ .padding()
+ }
+}
+
+struct LocalNotificationPlaygroundView: View {
+ @State var isAuthorized: Bool = false
- @State private var now = Date()
- @State private var nextTriggerDate: Date?
- private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
+ @State var timerDate: Date?
+ @State var nextTriggerDate: Date?
private var secondsUntilNextTrigger: Int? {
- guard let nextTriggerDate = self.nextTriggerDate else { return nil }
- let seconds = Int(nextTriggerDate.timeIntervalSince(self.now))
+ guard let timerDate, let nextTriggerDate else { return nil }
+ let seconds = Int(nextTriggerDate.timeIntervalSince(timerDate))
return seconds > 0 ? seconds : nil
}
private let notificationCenterDelegate = NotificationCenterDelegate()
- // MARK: - Initialization
-
- /// Initializes a new instance of the `NotificationPlayground` struct.
init() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self.notificationCenterDelegate
}
- // MARK: - View
var body: some View {
VStack(spacing: 20) {
Text("Is Authorized: \(self.isAuthorized ? "True" : "False")")
VStack(spacing: 10) {
- Button("Request Notification Permission") {
+ Button("Request Push Notification Permission") {
Task {
await self.requestNotificationPermission()
}
@@ -39,7 +104,7 @@ struct NotificationPlayground: View {
.buttonStyle(.bordered)
.disabled(self.isAuthorized)
- Button("Trigger Immediate Notification") {
+ Button("Trigger Immediate Push Notification") {
Task {
await self.addNotificationRequest(
title: "Title",
@@ -52,7 +117,7 @@ struct NotificationPlayground: View {
.buttonStyle(.borderedProminent)
.disabled(!self.isAuthorized)
- Button("Trigger Scheduled Notification") {
+ Button("Trigger Scheduled Push Notification") {
Task {
await self.addNotificationRequest(
title: "Title",
@@ -88,31 +153,29 @@ struct NotificationPlayground: View {
.foregroundStyle(.secondary)
}
}
- .navigationTitle("Notifications")
+ .navigationTitle("Local Notifications")
.onAppear {
Task {
await self.requestNotificationPermission()
}
}
- .onReceive(self.timer) { input in
- self.now = input
+ .task {
+ while !Task.isCancelled {
+ do {
+ try await Task.sleep(for: .seconds(1))
+ self.timerDate = Date()
+ } catch {
+ }
+ }
}
}
- /// Requests the permission to push notifications.
private func requestNotificationPermission() async {
let options: UNAuthorizationOptions = [.alert, .sound, .badge]
let notificationCenter = UNUserNotificationCenter.current()
self.isAuthorized = (try? await notificationCenter.requestAuthorization(options: options)) ?? false
}
- /// Adds a new notification request.
- ///
- /// - Parameters:
- /// - title: The notification title.
- /// - body: The notification body.
- /// - identifier: The notification identifier.
- /// - trigger: The (optional) notification trigger. If nil, the notification will trigger immediately.
private func addNotificationRequest(
title: String,
body: String,
@@ -120,7 +183,6 @@ struct NotificationPlayground: View {
trigger: UNNotificationTrigger? = nil
) async {
- // Create a new notificiation content.
let content = UNMutableNotificationContent()
content.title = title
content.body = body
@@ -128,38 +190,27 @@ struct NotificationPlayground: View {
"identifier": UUID().uuidString
]
- // Create a new notification request.
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: trigger
)
- // Schedule the delivery of the notification.
let notificationCenter = UNUserNotificationCenter.current()
try? await notificationCenter.add(request)
}
- /// Removes all pending notification requests.
private func removeAllPendingNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.removeAllPendingNotificationRequests()
}
}
-// MARK: - UNUserNotificationCenterDelegate
private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
-
- /// Asks the delegate how to handle a notification that arrived while the app was running in the foreground.
- ///
- /// - Parameters:
- /// - center: The shared user notification center object that received the notification.
- /// - notification: The notification that is about to be delivered.
- /// - Returns: Constants indicating how to present a notification in a foreground app.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
- return [.banner, .sound, .badge]
+ return [.banner]
}
}
diff --git a/Sources/Showcase/Resources/Localizable.xcstrings b/Sources/Showcase/Resources/Localizable.xcstrings
index 223f405..9a640f0 100644
--- a/Sources/Showcase/Resources/Localizable.xcstrings
+++ b/Sources/Showcase/Resources/Localizable.xcstrings
@@ -1421,6 +1421,9 @@
},
"Fullscreen cover" : {
+ },
+ "Generate Push Notification Token" : {
+
},
"GeometryReader" : {
@@ -1768,6 +1771,9 @@
},
"Local frame: %@" : {
+ },
+ "Local Notifications" : {
+
},
"Localization" : {
@@ -1971,9 +1977,6 @@
},
"Notification" : {
"extractionState" : "manual"
- },
- "Notifications" : {
-
},
"Observable" : {
@@ -2123,6 +2126,9 @@
},
"PDF Image" : {
+ },
+ "Permission Status: %@" : {
+
},
"Pick Document" : {
@@ -2252,6 +2258,9 @@
},
"Push binding view" : {
+ },
+ "Push Notification Client Token" : {
+
},
"Push Text.position(100, 100)" : {
@@ -2298,7 +2307,7 @@
"Repository item tap count: %lld" : {
},
- "Request Notification Permission" : {
+ "Request Push Notification Permission" : {
},
"Requires iOS 17+" : {
@@ -2752,6 +2761,9 @@
},
"Skip Intro" : {
+ },
+ "Skip Notify" : {
+
},
"Skip Technology" : {
"extractionState" : "manual",
From bc88aeeea44115f500a4eef7252c717657da786f Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Sun, 15 Feb 2026 02:07:40 +0100
Subject: [PATCH 3/9] Update NotificationPlayground.swift
---
Sources/Showcase/NotificationPlayground.swift | 26 +------------------
1 file changed, 1 insertion(+), 25 deletions(-)
diff --git a/Sources/Showcase/NotificationPlayground.swift b/Sources/Showcase/NotificationPlayground.swift
index 7a9221a..e394b4c 100644
--- a/Sources/Showcase/NotificationPlayground.swift
+++ b/Sources/Showcase/NotificationPlayground.swift
@@ -74,8 +74,6 @@ struct SkipNotifyNotificationPlaygroundView: View {
}
struct LocalNotificationPlaygroundView: View {
- @State var isAuthorized: Bool = false
-
@State var timerDate: Date?
@State var nextTriggerDate: Date?
private var secondsUntilNextTrigger: Int? {
@@ -93,17 +91,7 @@ struct LocalNotificationPlaygroundView: View {
var body: some View {
VStack(spacing: 20) {
- Text("Is Authorized: \(self.isAuthorized ? "True" : "False")")
-
VStack(spacing: 10) {
- Button("Request Push Notification Permission") {
- Task {
- await self.requestNotificationPermission()
- }
- }
- .buttonStyle(.bordered)
- .disabled(self.isAuthorized)
-
Button("Trigger Immediate Push Notification") {
Task {
await self.addNotificationRequest(
@@ -115,7 +103,6 @@ struct LocalNotificationPlaygroundView: View {
}
.backgroundStyle(.blue)
.buttonStyle(.borderedProminent)
- .disabled(!self.isAuthorized)
Button("Trigger Scheduled Push Notification") {
Task {
@@ -136,7 +123,7 @@ struct LocalNotificationPlaygroundView: View {
}
.backgroundStyle(.blue)
.buttonStyle(.borderedProminent)
- .disabled(!self.isAuthorized || self.secondsUntilNextTrigger != nil)
+ .disabled(self.secondsUntilNextTrigger != nil)
}
if let seconds = self.secondsUntilNextTrigger {
@@ -154,11 +141,6 @@ struct LocalNotificationPlaygroundView: View {
}
}
.navigationTitle("Local Notifications")
- .onAppear {
- Task {
- await self.requestNotificationPermission()
- }
- }
.task {
while !Task.isCancelled {
do {
@@ -170,12 +152,6 @@ struct LocalNotificationPlaygroundView: View {
}
}
- private func requestNotificationPermission() async {
- let options: UNAuthorizationOptions = [.alert, .sound, .badge]
- let notificationCenter = UNUserNotificationCenter.current()
- self.isAuthorized = (try? await notificationCenter.requestAuthorization(options: options)) ?? false
- }
-
private func addNotificationRequest(
title: String,
body: String,
From 49a7e36d91d3ade578b624d4adef90deeeda181d Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Sun, 15 Feb 2026 02:55:34 +0100
Subject: [PATCH 4/9] Cleanup
---
Darwin/Entitlements.plist | 4 ++--
Sources/Showcase/NotificationPlayground.swift | 11 +++--------
Sources/Showcase/Resources/Localizable.xcstrings | 7 ++-----
3 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/Darwin/Entitlements.plist b/Darwin/Entitlements.plist
index a28da91..f4e434b 100644
--- a/Darwin/Entitlements.plist
+++ b/Darwin/Entitlements.plist
@@ -3,8 +3,8 @@
aps-environment
- development
+ production
com.apple.developer.aps-environment
- development
+ production
diff --git a/Sources/Showcase/NotificationPlayground.swift b/Sources/Showcase/NotificationPlayground.swift
index e394b4c..3c4ee60 100644
--- a/Sources/Showcase/NotificationPlayground.swift
+++ b/Sources/Showcase/NotificationPlayground.swift
@@ -76,6 +76,7 @@ struct SkipNotifyNotificationPlaygroundView: View {
struct LocalNotificationPlaygroundView: View {
@State var timerDate: Date?
@State var nextTriggerDate: Date?
+ let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
private var secondsUntilNextTrigger: Int? {
guard let timerDate, let nextTriggerDate else { return nil }
let seconds = Int(nextTriggerDate.timeIntervalSince(timerDate))
@@ -141,14 +142,8 @@ struct LocalNotificationPlaygroundView: View {
}
}
.navigationTitle("Local Notifications")
- .task {
- while !Task.isCancelled {
- do {
- try await Task.sleep(for: .seconds(1))
- self.timerDate = Date()
- } catch {
- }
- }
+ .onReceive(self.timer) { date in
+ self.timerDate = Date()
}
}
diff --git a/Sources/Showcase/Resources/Localizable.xcstrings b/Sources/Showcase/Resources/Localizable.xcstrings
index 9a640f0..a7ec1d4 100644
--- a/Sources/Showcase/Resources/Localizable.xcstrings
+++ b/Sources/Showcase/Resources/Localizable.xcstrings
@@ -1621,9 +1621,6 @@
},
"iOS 17 / macOS 14 required for Observation framework" : {
- },
- "Is Authorized: %@" : {
-
},
"isOn" : {
@@ -3155,10 +3152,10 @@
"Transition" : {
},
- "Trigger Immediate Notification" : {
+ "Trigger Immediate Push Notification" : {
},
- "Trigger Scheduled Notification" : {
+ "Trigger Scheduled Push Notification" : {
},
"Two Buttons" : {
From 997bbaf520f52ebe6af7bea4aeb7b154bb038b30 Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Sat, 21 Feb 2026 13:37:38 +0100
Subject: [PATCH 5/9] Removed NotificationReceiver
---
Android/app/src/main/AndroidManifest.xml | 11 ------
.../src/main/kotlin/NotificationReceiver.kt | 34 -------------------
2 files changed, 45 deletions(-)
delete mode 100644 Android/app/src/main/kotlin/NotificationReceiver.kt
diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
index 64b817f..eb6e94f 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -14,7 +14,6 @@
-
@@ -40,16 +39,6 @@
android:name="showcase.module.default_notification_icon"
android:resource="@drawable/ic_notification" />
-
-
-
-
-
-
-
diff --git a/Android/app/src/main/kotlin/NotificationReceiver.kt b/Android/app/src/main/kotlin/NotificationReceiver.kt
deleted file mode 100644
index 31dc9e7..0000000
--- a/Android/app/src/main/kotlin/NotificationReceiver.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package showcase.module
-
-import skip.ui.*
-import skip.foundation.*
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class NotificationReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
-
- val title = intent.getStringExtra("title") ?: ""
- val body = intent.getStringExtra("body") ?: ""
-
- val content = UNNotificationContent(title = title, body = body)
- val request = UNNotificationRequest(identifier = UUID().uuidString, content = content)
-
- val pendingResult = goAsync()
- val scope = CoroutineScope(Dispatchers.Main)
- scope.launch {
- try {
- UNUserNotificationCenter.current().add(request)
- } catch (e: Exception) {
- e.printStackTrace()
- } finally {
- pendingResult.finish()
- }
- }
- }
-}
From c12d62621cc7a9cd7c7a1f449234e7510ee8126b Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Tue, 3 Mar 2026 08:36:59 +0100
Subject: [PATCH 6/9] Update Package.swift
---
Package.swift | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Package.swift b/Package.swift
index 64b2ff2..887e66a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -11,9 +11,9 @@ let package = Package(
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
- // TODO: Update package URLs and version after PRs have been merged (see: TODO)
- .package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-foundation"),
+ // TODO: Update skip ui package URL and version after PR has been merged
.package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-ui"),
+ //.package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),
.package(url: "https://source.skip.tools/skip-av.git", "0.0.0"..<"2.0.0"),
.package(url: "https://source.skip.tools/skip-kit.git", "0.0.0"..<"2.0.0"),
From 8d7bdf04eb013425af499dc5108ad5b6e072db62 Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Tue, 3 Mar 2026 08:39:32 +0100
Subject: [PATCH 7/9] Update Package.swift
---
Package.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Package.swift b/Package.swift
index 887e66a..c01de5e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,7 +12,7 @@ let package = Package(
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
// TODO: Update skip ui package URL and version after PR has been merged
- .package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-ui"),
+ .package(url: "https://github.com/fhasse95/skip-ui.git", branch: "Triggered-Notification-Support"),
//.package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),
.package(url: "https://source.skip.tools/skip-av.git", "0.0.0"..<"2.0.0"),
From 550591260b56c6a076d1e6147493b27155d3464b Mon Sep 17 00:00:00 2001
From: fhasse95 <49185957+fhasse95@users.noreply.github.com>
Date: Tue, 3 Mar 2026 08:44:46 +0100
Subject: [PATCH 8/9] Update Package.swift
---
Package.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Package.swift b/Package.swift
index c01de5e..d95b9b4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -11,7 +11,7 @@ let package = Package(
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
- // TODO: Update skip ui package URL and version after PR has been merged
+ // TODO: Update skip ui package URL and version after skip ui PR has been merged
.package(url: "https://github.com/fhasse95/skip-ui.git", branch: "Triggered-Notification-Support"),
//.package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),
From 0d0fec35ba6a8625eb5d5df27bc7ea266506a0a3 Mon Sep 17 00:00:00 2001
From: Marc Prud'hommeaux
Date: Sat, 7 Mar 2026 17:26:31 -0500
Subject: [PATCH 9/9] Update Package.swift to use skip-ui branch main
---
Package.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Package.swift b/Package.swift
index d95b9b4..723355c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,7 +12,7 @@ let package = Package(
.package(url: "https://source.skip.tools/skip.git", from: "1.4.0"),
// TODO: Update skip ui package URL and version after skip ui PR has been merged
- .package(url: "https://github.com/fhasse95/skip-ui.git", branch: "Triggered-Notification-Support"),
+ .package(url: "https://source.skip.tools/skip-ui.git", branch: "main"),
//.package(url: "https://source.skip.tools/skip-ui.git", from: "1.26.0"),
.package(url: "https://source.skip.tools/skip-av.git", "0.0.0"..<"2.0.0"),