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"),