,
34 | isModal: Bool = false
35 | ) {
36 | self._style = style
37 | self.isModal = isModal
38 | }
39 |
40 | private let isModal: Bool
41 |
42 | @Binding
43 | var style: SystemNotificationStyle
44 |
45 | @Environment(\.dismiss)
46 | var dismiss
47 |
48 | @EnvironmentObject
49 | var notification: SystemNotificationContext
50 |
51 | @State
52 | var isCoverActive = false
53 |
54 | @State
55 | var isSilentModeOn = false
56 |
57 | @State
58 | var isSheetActive = false
59 |
60 | @StateObject
61 | var toast = SystemNotificationContext()
62 |
63 | var body: some View {
64 | List {
65 | Section("Section.Notifications") {
66 | Toggle(isOn: $isSilentModeOn) {
67 | label(.silentModeOff, "Toggle.SilentMode")
68 | }
69 | listItem(.flag, "Menu.LocalizedMessage", presentLocalizedMessage)
70 | listItem(.static, "Menu.CustomView", presentCustomView)
71 | }
72 | Section("Section.Predefined") {
73 | listItem(.error, "Menu.Error", presentError)
74 | listItem(.success, "Menu.Success", presentSuccess)
75 | listItem(.warning, "Menu.Warning", presentWarning)
76 | }
77 | Section("Section.Toasts") {
78 | listItem(.sheet, "Menu.BottomToast", presentBottomToast)
79 |
80 | }
81 | Section("Section.Modals") {
82 | listItem(.sheet, "Menu.Sheet", presentModalSheet)
83 | listItem(.cover, "Menu.Cover", presentModalCover)
84 | if isModal {
85 | listItem(.dismiss, "Dismiss", dismiss.callAsFunction)
86 | }
87 | }
88 | }
89 | .buttonStyle(.plain)
90 | .navigationTitle("SystemNotification")
91 | .sheet(isPresented: $isSheetActive) {
92 | ContentView(style: $style, isModal: true)
93 | .systemNotification(notification)
94 | .systemNotificationStyle(style)
95 | }
96 | #if os(iOS)
97 | .fullScreenCover(isPresented: $isCoverActive) {
98 | ContentView(style: $style, isModal: true)
99 | .systemNotification(notification)
100 | .systemNotificationStyle(style)
101 | }
102 | #endif
103 | .systemNotification(toast)
104 | .systemNotificationConfiguration(.standardToast)
105 | .onChange(of: isSilentModeOn) { _ in presentSilentMode() }
106 | }
107 | }
108 |
109 | private extension ContentView {
110 |
111 | func label(_ icon: Image, _ text: LocalizedStringKey) -> some View {
112 | Label {
113 | Text(text)
114 | } icon: {
115 | icon
116 | }
117 | }
118 |
119 | func listItem(_ icon: Image, _ text: LocalizedStringKey, _ action: @escaping () -> Void) -> some View {
120 | Button(action: action) {
121 | label(icon, text)
122 | .frame(maxWidth: .infinity, alignment: .leading)
123 | .contentShape(Rectangle())
124 | }
125 | }
126 | }
127 |
128 | private extension ContentView {
129 |
130 | var flagView: some View {
131 | VStack(spacing: 0) {
132 | HStack(spacing: 0) {
133 | Color.blue
134 | .frame(width: 45)
135 | Color.yellow
136 | .frame(width: 15)
137 | Color.blue
138 | }
139 | .frame(height: 30)
140 |
141 | Color.yellow
142 | .frame(height: 15)
143 |
144 | HStack(spacing: 0) {
145 | Color.blue
146 | .frame(width: 45)
147 | Color.yellow
148 | .frame(width: 15)
149 | Color.blue
150 | }
151 | .frame(height: 30)
152 | }
153 | }
154 |
155 | func presentBottomToast() {
156 | toast.present {
157 | SystemNotificationMessage(
158 | title: "Message.Toast.Title",
159 | text: "Message.Toast.Text",
160 | style: .prominent(backgroundColor: .black)
161 | )
162 | }
163 | }
164 |
165 | func presentCustomView() {
166 | notification.present(
167 | flagView
168 | )
169 | }
170 |
171 | func presentError() {
172 | notification.presentMessage(
173 | .error(
174 | title: "Message.Error.Title",
175 | text: "Message.Error.Text"
176 | )
177 | )
178 | }
179 |
180 | func presentLocalizedMessage() {
181 | notification.present(
182 | SystemNotificationMessage(
183 | icon: Text("🇸🇪"),
184 | title: "Message.Localized.Title",
185 | text: "Message.Localized.Text"
186 | )
187 | )
188 | }
189 |
190 | func presentModalCover() {
191 | isCoverActive = true
192 | }
193 |
194 | func presentModalSheet() {
195 | isSheetActive = true
196 | }
197 |
198 | func presentSilentMode() {
199 | notification.presentMessage(
200 | .silentMode(isOn: isSilentModeOn)
201 | )
202 | }
203 |
204 | func presentSuccess() {
205 | notification.presentMessage(
206 | .success(
207 | title: "Message.Success.Title",
208 | text: "Message.Success.Text"
209 | )
210 | )
211 | }
212 |
213 | func presentWarning() {
214 | notification.presentMessage(
215 | .warning(
216 | title: "Message.Warning.Title",
217 | text: "Message.Warning.Text"
218 | )
219 | )
220 | }
221 | }
222 |
223 | #Preview {
224 |
225 | struct Preview: View {
226 |
227 | @StateObject
228 | var notification = SystemNotificationContext()
229 |
230 | @State
231 | var style = SystemNotificationStyle.standard
232 |
233 | var body: some View {
234 | ContentView(style: $style)
235 | .systemNotification(notification)
236 | }
237 | }
238 |
239 | return Preview()
240 | }
241 |
--------------------------------------------------------------------------------
/Demo/Demo/Demo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Demo/Demo/DemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoApp.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2023-01-16.
6 | //
7 |
8 | import SwiftUI
9 | import SystemNotification
10 |
11 | @main
12 | struct DemoApp: App {
13 |
14 | @StateObject
15 | private var context = SystemNotificationContext()
16 |
17 | @State
18 | private var style = SystemNotificationStyle.standard
19 |
20 | var body: some Scene {
21 | WindowGroup {
22 | content
23 | .systemNotification(context) // Context-based notifications are flexible
24 | .systemNotificationStyle(style) // This is how to set a global style
25 | .tint(.orange)
26 | }
27 | }
28 | }
29 |
30 | private extension DemoApp {
31 |
32 | /// This demo adds the context-based notification to all
33 | /// tabs, to make notifications display above all tabs.
34 | var content: some View {
35 | #if os(iOS)
36 | TabView {
37 | contentView.tabItem(1)
38 | contentView.tabItem(2)
39 | contentView.tabItem(3)
40 | contentView.tabItem(4)
41 | }
42 | #else
43 | contentView
44 | #endif
45 | }
46 |
47 | var contentView: some View {
48 | #if os(iOS)
49 | NavigationStack {
50 | ContentView(style: $style)
51 | }
52 | #else
53 | ContentView()
54 | #endif
55 | }
56 | }
57 |
58 | private extension View {
59 |
60 | func tabItem(_ index: Int) -> some View {
61 | self.tabItem {
62 | Label(
63 | "Tab \(index)",
64 | systemImage: "0\(index).circle")
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Demo/Demo/Localizable.xcstrings:
--------------------------------------------------------------------------------
1 | {
2 | "sourceLanguage" : "en",
3 | "strings" : {
4 | "🇸🇪" : {
5 |
6 | },
7 | "Dismiss" : {
8 | "localizations" : {
9 | "en" : {
10 | "stringUnit" : {
11 | "state" : "translated",
12 | "value" : "Dismiss"
13 | }
14 | }
15 | }
16 | },
17 | "Menu.BottomToast" : {
18 | "localizations" : {
19 | "en" : {
20 | "stringUnit" : {
21 | "state" : "translated",
22 | "value" : "Present a bottom toast"
23 | }
24 | }
25 | }
26 | },
27 | "Menu.Cover" : {
28 | "localizations" : {
29 | "en" : {
30 | "stringUnit" : {
31 | "state" : "translated",
32 | "value" : "Present a full screen cover"
33 | }
34 | }
35 | }
36 | },
37 | "Menu.CustomView" : {
38 | "localizations" : {
39 | "en" : {
40 | "stringUnit" : {
41 | "state" : "translated",
42 | "value" : "Present a custom view"
43 | }
44 | }
45 | }
46 | },
47 | "Menu.Error" : {
48 | "localizations" : {
49 | "en" : {
50 | "stringUnit" : {
51 | "state" : "translated",
52 | "value" : "Present an error message"
53 | }
54 | }
55 | }
56 | },
57 | "Menu.LocalizedMessage" : {
58 | "localizations" : {
59 | "en" : {
60 | "stringUnit" : {
61 | "state" : "translated",
62 | "value" : "Present a localized message"
63 | }
64 | }
65 | }
66 | },
67 | "Menu.Sheet" : {
68 | "localizations" : {
69 | "en" : {
70 | "stringUnit" : {
71 | "state" : "translated",
72 | "value" : "Present a sheet"
73 | }
74 | }
75 | }
76 | },
77 | "Menu.Success" : {
78 | "localizations" : {
79 | "en" : {
80 | "stringUnit" : {
81 | "state" : "translated",
82 | "value" : "Present a success message"
83 | }
84 | }
85 | }
86 | },
87 | "Menu.Warning" : {
88 | "localizations" : {
89 | "en" : {
90 | "stringUnit" : {
91 | "state" : "translated",
92 | "value" : "Present a warning"
93 | }
94 | }
95 | }
96 | },
97 | "Message.Error.Text" : {
98 | "localizations" : {
99 | "en" : {
100 | "stringUnit" : {
101 | "state" : "translated",
102 | "value" : "Something went wrong"
103 | }
104 | }
105 | }
106 | },
107 | "Message.Error.Title" : {
108 | "localizations" : {
109 | "en" : {
110 | "stringUnit" : {
111 | "state" : "translated",
112 | "value" : "Error!"
113 | }
114 | }
115 | }
116 | },
117 | "Message.Localized.Text" : {
118 | "localizations" : {
119 | "en" : {
120 | "stringUnit" : {
121 | "state" : "translated",
122 | "value" : "This notification is localized"
123 | }
124 | }
125 | }
126 | },
127 | "Message.Localized.Title" : {
128 | "localizations" : {
129 | "en" : {
130 | "stringUnit" : {
131 | "state" : "translated",
132 | "value" : "Localization Support!"
133 | }
134 | }
135 | }
136 | },
137 | "Message.Success.Text" : {
138 | "localizations" : {
139 | "en" : {
140 | "stringUnit" : {
141 | "state" : "translated",
142 | "value" : "This was a big success"
143 | }
144 | }
145 | }
146 | },
147 | "Message.Success.Title" : {
148 | "localizations" : {
149 | "en" : {
150 | "stringUnit" : {
151 | "state" : "translated",
152 | "value" : "Yay!"
153 | }
154 | }
155 | }
156 | },
157 | "Message.Toast.Text" : {
158 | "localizations" : {
159 | "en" : {
160 | "stringUnit" : {
161 | "state" : "translated",
162 | "value" : "This black toast is presented from the bottom"
163 | }
164 | }
165 | }
166 | },
167 | "Message.Toast.Title" : {
168 | "localizations" : {
169 | "en" : {
170 | "stringUnit" : {
171 | "state" : "translated",
172 | "value" : "Toastie!"
173 | }
174 | }
175 | }
176 | },
177 | "Message.Warning.Text" : {
178 | "localizations" : {
179 | "en" : {
180 | "stringUnit" : {
181 | "state" : "translated",
182 | "value" : "Look at the orange color - this is a serious message!"
183 | }
184 | }
185 | }
186 | },
187 | "Message.Warning.Title" : {
188 | "localizations" : {
189 | "en" : {
190 | "stringUnit" : {
191 | "state" : "translated",
192 | "value" : "Warning, warning!"
193 | }
194 | }
195 | }
196 | },
197 | "Section.Modals" : {
198 | "localizations" : {
199 | "en" : {
200 | "stringUnit" : {
201 | "state" : "translated",
202 | "value" : "Modals"
203 | }
204 | }
205 | }
206 | },
207 | "Section.Notifications" : {
208 | "localizations" : {
209 | "en" : {
210 | "stringUnit" : {
211 | "state" : "translated",
212 | "value" : "Notifications"
213 | }
214 | }
215 | }
216 | },
217 | "Section.Predefined" : {
218 | "localizations" : {
219 | "en" : {
220 | "stringUnit" : {
221 | "state" : "translated",
222 | "value" : "Predefined message types"
223 | }
224 | }
225 | }
226 | },
227 | "Section.Toasts" : {
228 | "localizations" : {
229 | "en" : {
230 | "stringUnit" : {
231 | "state" : "translated",
232 | "value" : "Toasts"
233 | }
234 | }
235 | }
236 | },
237 | "SystemNotification" : {
238 |
239 | },
240 | "Tab %lld" : {
241 |
242 | },
243 | "Toggle.SilentMode" : {
244 | "localizations" : {
245 | "en" : {
246 | "stringUnit" : {
247 | "state" : "translated",
248 | "value" : "Silent Mode"
249 | }
250 | }
251 | }
252 | }
253 | },
254 | "version" : "1.0"
255 | }
--------------------------------------------------------------------------------
/Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-visionOS-Back.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-visionOS-Back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-visionOS-Back.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "layers" : [
7 | {
8 | "filename" : "Front.solidimagestacklayer"
9 | },
10 | {
11 | "filename" : "Middle.solidimagestacklayer"
12 | },
13 | {
14 | "filename" : "Back.solidimagestacklayer"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-visionOS-Front.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-visionOS-Front.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-visionOS-Front.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-visionOS-Middle.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Icon-visionOS-Middle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Icon-visionOS-Middle.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-iOS-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "filename" : "Icon-macOS-16.png",
11 | "idiom" : "mac",
12 | "scale" : "1x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Icon-macOS-32.png",
17 | "idiom" : "mac",
18 | "scale" : "2x",
19 | "size" : "16x16"
20 | },
21 | {
22 | "filename" : "Icon-macOS-32.png",
23 | "idiom" : "mac",
24 | "scale" : "1x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Icon-macOS-64.png",
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "32x32"
32 | },
33 | {
34 | "filename" : "Icon-macOS-128.png",
35 | "idiom" : "mac",
36 | "scale" : "1x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Icon-macOS-256.png",
41 | "idiom" : "mac",
42 | "scale" : "2x",
43 | "size" : "128x128"
44 | },
45 | {
46 | "filename" : "Icon-macOS-256.png",
47 | "idiom" : "mac",
48 | "scale" : "1x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Icon-macOS-512.png",
53 | "idiom" : "mac",
54 | "scale" : "2x",
55 | "size" : "256x256"
56 | },
57 | {
58 | "filename" : "Icon-macOS-512.png",
59 | "idiom" : "mac",
60 | "scale" : "1x",
61 | "size" : "512x512"
62 | },
63 | {
64 | "filename" : "Icon-macOS-1024.png",
65 | "idiom" : "mac",
66 | "scale" : "2x",
67 | "size" : "512x512"
68 | }
69 | ],
70 | "info" : {
71 | "author" : "xcode",
72 | "version" : 1
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-iOS-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-iOS-1024.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-1024.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-128.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-16.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-256.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-32.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-512.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-64.png
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Image+Demo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Image+Demo.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2021-06-08.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension Image {
12 |
13 | static let cover = Image(systemName: "rectangle.inset.fill")
14 | static let dismiss = Image(systemName: "xmark.circle")
15 | static let error = Image(systemName: "xmark.octagon")
16 | static let flag = Image(systemName: "flag")
17 | static let globe = Image(systemName: "globe")
18 | static let sheet = Image(systemName: "rectangle.bottomthird.inset.fill")
19 | static let silentModeOff = Image(systemName: "bell.fill")
20 | static let silentModeOn = Image(systemName: "bell.slash.fill")
21 | static let `static` = Image(systemName: "viewfinder")
22 | static let success = Image(systemName: "checkmark")
23 | static let warning = Image(systemName: "exclamationmark.triangle")
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-2024 Daniel Saidi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:6.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SystemNotification",
7 | platforms: [
8 | .iOS(.v15),
9 | .macOS(.v12),
10 | .tvOS(.v15),
11 | .watchOS(.v10),
12 | .visionOS(.v1)
13 | ],
14 | products: [
15 | .library(
16 | name: "SystemNotification",
17 | targets: ["SystemNotification"]
18 | )
19 | ],
20 | targets: [
21 | .target(
22 | name: "SystemNotification"
23 | ),
24 | .testTarget(
25 | name: "SystemNotificationTests",
26 | dependencies: ["SystemNotification"]
27 | )
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | # SystemNotification
17 |
18 | SystemNotification is a SwiftUI library that lets you mimic the native iOS system notification that is presented when you toggle silent mode, connect your AirPods, etc.
19 |
20 |
21 |
22 |
23 |
24 | System notifications can be styled and customized. You can use a native-looking `SystemNotificationMessage` view as the content view, or any custom view.
25 |
26 |
27 |
28 | ## Installation
29 |
30 | SystemNotification can be installed with the Swift Package Manager:
31 |
32 | ```
33 | https://github.com/danielsaidi/SystemNotification.git
34 | ```
35 |
36 |
37 | ## Support My Work
38 |
39 | You can [become a sponsor][Sponsors] to help me dedicate more time on my various [open-source tools][OpenSource]. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed.
40 |
41 |
42 |
43 | ## Getting started
44 |
45 | With SystemNotification, you can add a system notification to any view just as you add a `sheet`, `alert` and `fullScreenModal`, by applying a `systemNotification` view modifier (preferably to the application root view).
46 |
47 | State-based notifications take a boolean state binding and a view builder:
48 |
49 | ```swift
50 | import SystemNotification
51 |
52 | struct MyView: View {
53 |
54 | @State
55 | var isActive = false
56 |
57 | var body: some View {
58 | VStack {
59 | Button("Show notification") {
60 | isActive = true
61 | }
62 | }
63 | .systemNotification(isActive: $isActive) {
64 | Text("You can use any custom content view")
65 | .padding()
66 | }
67 | }
68 | }
69 | ```
70 |
71 | Context-based notifications just take a `SystemNotificationContext` instance and can then show many different notifications with a single modifier:
72 |
73 | ```swift
74 | import SystemNotification
75 |
76 | struct MyView: View {
77 |
78 | @StateObject
79 | var notification = SystemNotificationContext()
80 |
81 | var body: some View {
82 | VStack {
83 | Button("Show text") {
84 | notification.present {
85 | Text("Context-based notifications are more flexible.")
86 | .padding()
87 | .multilineTextAlignment(.center)
88 | }
89 | }
90 | Button("Show message") {
91 | notification.present {
92 | SystemNotificationMessage(
93 | icon: Text("👍"),
94 | title: "Great job!",
95 | text: "You presented a native-looking message!"
96 | )
97 | }
98 | }
99 | }
100 | .systemNotification(notification)
101 | }
102 | }
103 | ```
104 |
105 | The `SystemNotificationMessage` view lets you easily mimic a native notification view, with an icon, title and text, but you can use any custom view as the notification body.
106 |
107 | See the online [getting started guide][Getting-Started] for more information.
108 |
109 |
110 |
111 | ## Documentation
112 |
113 | The online [documentation][Documentation] has more information, articles, code examples, etc.
114 |
115 |
116 |
117 | ## Demo Application
118 |
119 | The `Demo` folder has an app that lets you explore the library.
120 |
121 |
122 |
123 | ## Contact
124 |
125 | Feel free to reach out if you have questions, or want to contribute in any way:
126 |
127 | * Website: [danielsaidi.com][Website]
128 | * E-mail: [daniel.saidi@gmail.com][Email]
129 | * Bluesky: [@danielsaidi@bsky.social][Bluesky]
130 | * Mastodon: [@danielsaidi@mastodon.social][Mastodon]
131 |
132 |
133 |
134 | ## License
135 |
136 | SystemNotification is available under the MIT license. See the [LICENSE][License] file for more info.
137 |
138 |
139 |
140 | [Email]: mailto:daniel.saidi@gmail.com
141 | [Website]: https://danielsaidi.com
142 | [GitHub]: https://github.com/danielsaidi
143 | [OpenSource]: https://danielsaidi.com/opensource
144 | [Sponsors]: https://github.com/sponsors/danielsaidi
145 |
146 | [Bluesky]: https://bsky.app/profile/danielsaidi.bsky.social
147 | [Mastodon]: https://mastodon.social/@danielsaidi
148 | [Twitter]: https://twitter.com/danielsaidi
149 |
150 | [Documentation]: https://danielsaidi.github.io/SystemNotification
151 | [Getting-Started]: https://danielsaidi.github.io/SystemNotification/documentation/systemnotification/getting-started
152 | [License]: https://github.com/danielsaidi/SystemNotification/blob/master/LICENSE
153 |
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 | # Release notes
2 |
3 | SystemNotification will use semver after 1.0.
4 |
5 | Until then, breaking changes can happen in any version, and deprecated features may be removed in any minor version bump.
6 |
7 |
8 |
9 | ## 1.3
10 |
11 | This version changes the default animation to `.bouncy`.
12 |
13 |
14 |
15 | ## 1.2.1
16 |
17 | This patch lets you provide a custom bundle to `SystemNotificatonMessage`.
18 |
19 |
20 |
21 | ## 1.2
22 |
23 | This version renames the `master` branch to `main` and updates to Swift 6.
24 |
25 |
26 |
27 | ## 1.1.2
28 |
29 | Thanks to @martindufort there's now an AppKit-specific overlay.
30 |
31 | ### ✨ New features
32 |
33 | `SystemNotificationAppKitOverlay` is a new AppKit-specific overlay view. This version renames the `master` branch to `main` and updates to Swift 6.
34 |
35 |
36 |
37 | ## 1.1.1
38 |
39 | This version adds support for strict concurrency.
40 |
41 |
42 |
43 | ## 1.1
44 |
45 | This version adds predefined system notification messages and styles and makes it easier to present a message.
46 |
47 | ### ✨ New features
48 |
49 | * `SystemNotificationContext` has a new `presentMessage` function.
50 | * `SystemNotificationMessage` has new, predefined `error`, `success`, `warning` and `silentMode` messages.
51 | * `SystemNotificationMessageStyle` has new, predefined `prominent`, `error`, `success` and `warning` styles.
52 |
53 |
54 |
55 | ## 1.0
56 |
57 | This version bumps the deployment targets and moves styling and configuration to view modifiers.
58 |
59 | ### 🚨 Important Information
60 |
61 | * All previous style- and config-based initializers have been removed.
62 |
63 | ### 📱 New Deployment Targets
64 |
65 | * .iOS(.v15)
66 | * .macOS(.v12)
67 | * .tvOS(.v15)
68 | * .watchOS(.v8)
69 | * .visionOS(.v1)
70 |
71 | ### ✨ New features
72 |
73 | * `SystemNotification` is more self-managed than before.
74 | * `SystemNotificationConfiguration` can now be used as an environment value.
75 | * `SystemNotificationStyle` now supports background materials.
76 | * `SystemNotificationStyle` can now be used as an environment value.
77 | * `SystemNotificationMessageStyle` can now be used as an environment value.
78 | * `SystemNotificationMessageStyle` now supports specifying a foreground color.
79 | * `SystemNotificationMessageStyle` now supports specifying a background color.
80 | * `View` has new system notification-related style- and config view modifiers.
81 |
82 | ### 🐛 Bug fixes
83 |
84 | * `SystemNotification` now correctly applies the configuration animation.
85 |
86 |
87 |
88 | ## 0.8
89 |
90 | ### ✨ New features
91 |
92 | * SystemNotification now supports visionOS.
93 |
94 | ### 💥 Breaking changes
95 |
96 | * SystemNotification now requires Swift 5.9.
97 |
98 |
99 |
100 | ## 0.7.3
101 |
102 | ### ✨ New features
103 |
104 | * The `SystemNotificationPresenter` feature was a bad addition and has been deprecated.
105 |
106 |
107 |
108 | ## 0.7.2
109 |
110 | ### ✨ New features
111 |
112 | * `SystemNotificationPresenter` is a new convenience protocol.
113 |
114 |
115 |
116 | ## 0.7.1
117 |
118 | This version rolls back the UIKit support deprecation.
119 |
120 | ### 🗑 Deprecations
121 |
122 | * `SystemNotificationUIKitOverlay` is no longer deprecated.
123 |
124 |
125 |
126 | ## 0.7
127 |
128 | This version splits up `SystemNotificationConfiguration` in a configuration and style type.
129 |
130 | Due to changes in the `SystemNotificationMessage` capabilities, the `LocalizedStringKey` support has been deprecated.
131 |
132 | Also, since SystemNotification aims to be a pure SwiftUI project, the `SystemNotificationUIKitOverlay` has been deprecated. Please let me know if you really need it, and I'll re-add it to the library.
133 |
134 | ### ✨ New features
135 |
136 | * `SystemNotificationMessage` now supports a custom icon view.
137 | * `SystemNotificationStyle` is a new type that's extracted from `SystemNotificationConfiguration`.
138 |
139 | ### 💡 Behavior changes
140 |
141 | * `SystemNotification` no longer uses async size bindings to apply the corner radius.
142 |
143 | ### 🗑 Deprecated
144 |
145 | * `SystemNotificationConfiguration` moves all styles to `SystemNotificationStyle`.
146 | * `SystemNotificationMessage` has deprecated its `LocalizedString` initializer.
147 | * `SystemNotificationMessageConfiguration` is renamed to `SystemNotificationMessageStyle`.
148 |
149 |
150 |
151 | ## 0.6
152 |
153 | ### ✨ New features
154 |
155 | * `SystemNotificationConfiguration` has a new `padding` parameter.
156 | * `SystemNotificationConfiguration` has a new `standardBackgroundColor` function.
157 | * `SystemNotificationConfiguration` has a new `standardPadding` property.
158 |
159 | ### 💡 Behavior changes
160 |
161 | * `SystemNotificationContext` handles custom presentation configurations better.
162 |
163 | ### 💥 Breaking changes
164 |
165 | * All deprecated code has been removed.
166 |
167 |
168 |
169 | ## 0.5.3
170 |
171 | ### 💡 Behavior changes
172 |
173 | * `SystemNotificationContext` `present` now has an optional configuration.
174 | * `SystemNotificationContext` now uses its own configuration if none is provided.
175 |
176 |
177 |
178 | ## 0.5.2
179 |
180 | This release fixes compile errors on tvOS and watchOS.
181 |
182 |
183 |
184 | ## 0.5.1
185 |
186 | This release makes configuration properties mutable.
187 |
188 |
189 |
190 | ## 0.5
191 |
192 | This release greatly improves how notifications are presented and dismissed and simplifies usage.
193 |
194 | The demo app now uses a local package, which makes it a lot easier to develop the library.
195 |
196 | ### 📖 Documentation
197 |
198 | SystemNotification has a brand new DocC documentation.
199 |
200 | Due to the new documentation, the package now requires Swift 5.5.
201 |
202 | ### ✨ New features
203 |
204 | * `SystemNotificationContext` has a new completion-based dismiss function.
205 | * `SystemNotificationMessageConfiguration` has new `iconTextSpacing` and `titleTextSpacing` properties.
206 | * `SystemNotificationUIKitOverlay` is a new view that simplifies adding a system notification to a UIKit view.
207 | * `View+SystemNotification` has a new parameter-based extension that replaces the old notification-based one.
208 |
209 | ### 💡 Behavior changes
210 |
211 | * `SystemNotificationMessageConfiguration` is adjusted to make a message look more like an iPhone system notification.
212 | * Presenting a new notification first dismisses the current notification, if any.
213 | * The auto-dismiss logic is moved from the system notification to the notification context.
214 |
215 | ### 🐛 Bug fixes
216 |
217 | * This version fixes a bug, where the message configuration padding was incorrectly applied.
218 |
219 | ### 🗑 Deprecated
220 |
221 | * The notification-based `systemNotification(:)` function is deprecated.
222 |
223 | ### 💥 Breaking changes
224 |
225 | * `SystemNotification+Message` has been deprecated.
226 | * `SystemNotificationConfiguration` `minWidth` is no longer used and has been removed.
227 | * `View+SystemNotification` has deprecated the `SystemNotification`-based extension.
228 |
229 |
230 |
231 | ## 0.4.3
232 |
233 | ### ✨ New features
234 |
235 | * `SystemNotificationConfiguration` has a new `isSwipeToDismissEnabled` parameter.
236 | * `SystemNotification` can now be swiped to be dismissed, if `isSwipeToDismissEnabled` is `true`.
237 |
238 |
239 |
240 | ## 0.4.2
241 |
242 | This relase makes it possible to provide a `nil` title to `SystemNotificationMessage`.
243 |
244 |
245 |
246 | ## 0.4.1
247 |
248 | This relase makes it possible to use plain `String` values when creating `SystemNotification` and `SystemNotificationMessage`.
249 |
250 |
251 |
252 | ## 0.4
253 |
254 | ### ✨ New features
255 |
256 | * The context-based view modifier no longer requires a `context` parameter name.
257 |
258 | ### 🗑 Deprecated
259 |
260 | * `systemNotification(context:)` is replaced with `systemNotification(_ context:)`.
261 |
262 | ### 🐛 Bug fixes
263 |
264 | * This version fixes a bug, where the configuration duration wasn't applied.
265 | * This version fixes a bug, where the default dark mode background was transparent.
266 |
267 |
268 |
269 | ## 0.3.2
270 |
271 | ### 🐛 Bug fixes
272 |
273 | * This version fixes a preview bug that caused the library not to build for macOS.
274 |
275 |
276 |
277 | ## 0.3.1
278 |
279 | ### ✨ New features
280 |
281 | * Thanks to Christian Mitteldorf, system notifications now use localized string keys, which makes it super simple to create localized notifications.
282 |
283 |
284 |
285 | ## 0.3.0
286 |
287 | This release has some breaking name changes and makes it easier to present multiple notifications with a single modifier.
288 |
289 | ### ✨ New features
290 |
291 | * `SystemNotificationContext` makes it easy to present multiple notifications with a single modifier.
292 |
293 | ### 💥 Breaking changes
294 |
295 | * `SystemNotification.Configuration` has been renamed to `SystemNotificationConfiguration`
296 | * `SystemNotificationMessage.Configuration` has been renamed to `SystemNotificationMessageConfiguration`
297 |
298 |
299 |
300 | ## 0.2.0
301 |
302 | This release improves platform supports, adds convenience utils and tweaks design.
303 |
304 | ### ✨ New features
305 |
306 | * The library now supports macOS, tvOS and watchOS as well.
307 | * `SystemNotification.Configuration` has new shadow properties.
308 |
309 | ### 💡 Behavior changes
310 |
311 | * The configuration types are no longed nested, to avoid generic limitations.
312 |
313 | ### 🎨 Design changes
314 |
315 | * `SystemNotification.Configuration` has removed the background opacity modifier.
316 | * `SystemNotification.Configuration` has now applies a more subtle standard shadow.
317 | * `SystemNotificationMessage.Configuration` now uses `title3` as standard icon font.
318 |
319 | ### 🐛 Bug fixes
320 |
321 | * The corner radius now works even when no image is provided.
322 |
323 |
324 |
325 | ## 0.1.0
326 |
327 | This is the first public release of SystemNotification.
328 |
329 | Check out the readme and the demo app for information about how to use it.
330 |
--------------------------------------------------------------------------------
/Resources/Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Resources/Demo.gif
--------------------------------------------------------------------------------
/Resources/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Resources/Icon.png
--------------------------------------------------------------------------------
/Sources/SystemNotification/Overlays/SystemNotificationAppKitOverlay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotification.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2022-01-20.
6 | // Copyright © 2022-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(macOS)
10 | import SwiftUI
11 |
12 | /// This view can be used to create an overlay that can then
13 | /// be added to any AppKit view, using `addAsOverlay(to:)`.
14 | public struct SystemNotificationAppKitOverlay: View {
15 |
16 | public init(context: SystemNotificationContext) {
17 | self._context = ObservedObject(wrappedValue: context)
18 | }
19 |
20 | @ObservedObject
21 | var context: SystemNotificationContext
22 |
23 | public var body: some View {
24 | Color.clear
25 | .disabled(true)
26 | .edgesIgnoringSafeArea(.all)
27 | .systemNotification(context)
28 | }
29 | }
30 |
31 | public extension SystemNotificationAppKitOverlay {
32 |
33 | /// Add the overlay view to a certain AppKit view.
34 | func addAsOverlay(to view: NSView) {
35 | let overlay = NSHostingController(rootView: self)
36 | overlay.view.wantsLayer = true
37 | overlay.view.layer!.backgroundColor = .clear
38 | view.addSubview(overlay.view)
39 |
40 | overlay.view.translatesAutoresizingMaskIntoConstraints = false
41 | overlay.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
42 | overlay.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
43 | overlay.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
44 | overlay.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
45 | }
46 | }
47 | #endif
48 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/Overlays/SystemNotificationUIKitOverlay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotification.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2022-01-20.
6 | // Copyright © 2022-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /// This view can be used to create an overlay that can then
13 | /// be added to any UIKit view, using `addAsOverlay(to:)`.
14 | public struct SystemNotificationUIKitOverlay: View {
15 |
16 | public init(context: SystemNotificationContext) {
17 | self._context = ObservedObject(wrappedValue: context)
18 | }
19 |
20 | @ObservedObject
21 | var context: SystemNotificationContext
22 |
23 | public var body: some View {
24 | Color.clear
25 | .disabled(true)
26 | .edgesIgnoringSafeArea(.all)
27 | .systemNotification(context)
28 | }
29 | }
30 |
31 | public extension SystemNotificationUIKitOverlay {
32 |
33 | /// Add the overlay view to a certain UIKit view.
34 | func addAsOverlay(to view: UIView) {
35 | let overlay = UIHostingController(rootView: self)
36 | view.addSubview(overlay.view)
37 |
38 | // Prevent the UIHostingController from grabbing all touch events going to the UIKit view
39 | overlay.view.isUserInteractionEnabled = false
40 | overlay.view.backgroundColor = .clear
41 | overlay.view.translatesAutoresizingMaskIntoConstraints = false
42 | overlay.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
43 | overlay.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
44 | overlay.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
45 | overlay.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
46 | }
47 | }
48 | #endif
49 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotification.docc/Articles/Demo-Article.md:
--------------------------------------------------------------------------------
1 | # Demo App
2 |
3 | This article describes the SystemNotification demo app.
4 |
5 | @Metadata {
6 |
7 | @CallToAction(purpose: link, url: https://github.com/danielsaidi/SystemNotification)
8 |
9 | @PageKind(sampleCode)
10 |
11 | @PageImage(
12 | purpose: card,
13 | source: "Page",
14 | alt: "Page icon"
15 | )
16 | }
17 |
18 | SystemNotification has a multi-platform demo app that show how to use the library on multiple platforms.
19 |
20 | The demo app can be explored from the [GitHub repository][GitHub]. It's a SwiftUI app that runs on macOS, iOS & iPadOS.
21 |
22 |
23 |
24 | ## How to run the demo on device
25 |
26 | The demo app has disabled code signing, to simplify its setup.
27 |
28 | You can run the demo app on any iOS and iPadOS simulator, as well as on macOS. To run it on a physical iOS or iPadOS device, you must set up code signing just like you would with any other app.
29 |
30 |
31 |
32 | [GitHub]: https://github.com/danielsaidi/SystemNotification
33 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotification.docc/Articles/Getting Started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | This article describes how to get started with SystemNotification.
4 |
5 | @Metadata {
6 |
7 | @PageImage(
8 | purpose: card,
9 | source: "Page",
10 | alt: "Page icon"
11 | )
12 |
13 | @PageColor(blue)
14 | }
15 |
16 |
17 |
18 | ## Overview
19 |
20 | After adding SystemNotification to your project, you can add a system notification to any view just as you add a `sheet`, `alert` and `fullScreenModal`, with a simple view modifier.
21 |
22 | To add a system notification to a view, just use the ``SwiftUI/View/systemNotification(_:)`` view modifier with a state binding or a ``SystemNotificationContext`` instance.
23 |
24 | > Important: Since system notifications should be as global as possible, make sure to apply the view modifier to the application root view, e.g. the main `NavigationStack` or ``TabView``. Any new sheets or modals must also have the modifier applied.
25 |
26 | State-based notifications take a boolean state binding and a view builder:
27 |
28 | ```swift
29 | import SystemNotification
30 |
31 | struct MyView: View {
32 |
33 | @State
34 | var isActive = false
35 |
36 | var body: some View {
37 | VStack {
38 | Button("Show notification") {
39 | isActive = true
40 | }
41 | }
42 | .systemNotification(isActive: $isActive) {
43 | Text("You can use any custom content view")
44 | .padding()
45 | }
46 | }
47 | }
48 | ```
49 |
50 | Context-based notifications take a ``SystemNotificationContext`` and can then show different notifications with a single modifier:
51 |
52 | ```swift
53 | import SystemNotification
54 |
55 | struct MyView: View {
56 |
57 | @StateObject
58 | var notification = SystemNotificationContext()
59 |
60 | var body: some View {
61 | VStack {
62 | Button("Show text") {
63 | notification.present {
64 | Text("Context-based notifications are more flexible.")
65 | .padding()
66 | .multilineTextAlignment(.center)
67 | }
68 | }
69 | Button("Show message") {
70 | notification.present {
71 | SystemNotificationMessage(
72 | icon: Text("👍"),
73 | title: "Great job!",
74 | text: "You presented a native-looking message!"
75 | )
76 | }
77 | }
78 | }
79 | .systemNotification(notification)
80 | }
81 | }
82 | ```
83 |
84 | The ``SystemNotificationMessage`` view lets you easily mimic a native notification view, with an icon, an optional title and a text, but you can use any custom view as the notification content view.
85 |
86 | You can use the ``SwiftUI/View/systemNotificationConfiguration(_:)`` and ``SwiftUI/View/systemNotificationStyle(_:)`` view modifiers to apply custom configurations and styles.
87 |
88 |
89 |
90 | ## How to create custom notification messages
91 |
92 | The ``SystemNotificationMessage`` view lets you easily mimic a native notification message, with an icon, an optional title and a text, as well as an explicit style that overrides any environment style.
93 |
94 | You can easily extend ``SystemNotificationMessage`` with your own custom messages, which can then be easily presented with the context's ``SystemNotificationContext/presentMessage(_:afterDelay:)`` function:
95 |
96 | ```swift
97 | extension SystemNotificationMessage where IconView == Image {
98 |
99 | static var itemCreated: Self {
100 | .init(
101 | icon: Image(systemName: "checkmark"),
102 | title: "Item created!",
103 | text: "A new item was created",
104 | style: ....
105 | )
106 | }
107 | }
108 | ```
109 |
110 | You can also use the ``SwiftUI/View/systemNotificationMessageStyle(_:)`` view modifier to provide a standard style for all other messages.
111 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotification.docc/Resources/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Sources/SystemNotification/SystemNotification.docc/Resources/Logo.png
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotification.docc/Resources/Page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Sources/SystemNotification/SystemNotification.docc/Resources/Page.png
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotification.docc/SystemNotification.md:
--------------------------------------------------------------------------------
1 | # ``SystemNotification``
2 |
3 | SystemNotification is a SwiftUI library that lets you mimic the native iOS system notification.
4 |
5 |
6 |
7 | ## Overview
8 |
9 | 
10 |
11 | SystemNotification is a SwiftUI library that lets you mimic the native iOS system notification that is presented when you toggle silent mode, connect your AirPods, etc.
12 |
13 | System notifications can be styled and customized. You can use a native-looking ``SystemNotificationMessage`` view as the content view, or any custom view.
14 |
15 |
16 |
17 | ## Installation
18 |
19 | SystemNotification can be installed with the Swift Package Manager:
20 |
21 | ```
22 | https://github.com/danielsaidi/SystemNotification.git
23 | ```
24 |
25 |
26 | ## Support My Work
27 |
28 | You can [become a sponsor][Sponsors] to help me dedicate more time on my various [open-source tools][OpenSource]. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed.
29 |
30 |
31 |
32 | ## Getting started
33 |
34 | @Links(visualStyle: detailedGrid) {
35 |
36 | -
37 | -
38 | }
39 |
40 |
41 |
42 | ## Repository
43 |
44 | For more information, source code, etc., visit the [project repository](https://github.com/danielsaidi/SystemNotification).
45 |
46 |
47 |
48 | ## License
49 |
50 | SystemNotification is available under the MIT license.
51 |
52 |
53 |
54 | ## Topics
55 |
56 | ### Articles
57 |
58 | -
59 | -
60 | -
61 |
62 | ### Essentials
63 |
64 | - ``SystemNotification/SystemNotification``
65 | - ``SystemNotificationConfiguration``
66 | - ``SystemNotificationContext``
67 | - ``SystemNotificationEdge``
68 | - ``SystemNotificationMessage``
69 | - ``SystemNotificationMessageStyle``
70 | - ``SystemNotificationStyle``
71 | - ``SystemNotificationUIKitOverlay``
72 |
73 |
74 |
75 | [Email]: mailto:daniel.saidi@gmail.com
76 | [Website]: https://danielsaidi.com
77 | [GitHub]: https://github.com/danielsaidi
78 | [OpenSource]: https://danielsaidi.com/opensource
79 | [Sponsors]: https://github.com/sponsors/danielsaidi
80 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotification.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotification.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This view mimics the native iOS system notification that
12 | /// for instance is shown when toggling silent mode.
13 | ///
14 | /// This view renders a notification shape that contains the
15 | /// provided content view. You can use a custom view, or use
16 | /// a ``SystemNotificationMessage`` for convenience.
17 | ///
18 | /// You can use a ``SwiftUI/View/systemNotificationStyle(_:)``
19 | /// and a ``SwiftUI/View/systemNotificationConfiguration(_:)``
20 | /// to style and configure a system notification.
21 | public struct SystemNotification: View {
22 |
23 | /// Create a system notification view.
24 | ///
25 | /// - Parameters:
26 | /// - isActive: A binding that controls the active state of the notification.
27 | /// - content: The view to present within the notification badge.
28 | public init(
29 | isActive: Binding,
30 | @ViewBuilder content: @escaping ContentBuilder
31 | ) {
32 | _isActive = isActive
33 | self.initStyle = nil
34 | self.initConfig = nil
35 | self.content = content
36 | }
37 |
38 | public typealias ContentBuilder = (_ isActive: Bool) -> Content
39 |
40 | private let initConfig: SystemNotificationConfiguration?
41 | private let initStyle: SystemNotificationStyle?
42 | private let content: ContentBuilder
43 |
44 | @Binding
45 | private var isActive: Bool
46 |
47 | @Environment(\.colorScheme)
48 | private var colorScheme
49 |
50 | @Environment(\.systemNotificationConfiguration)
51 | private var envConfig
52 |
53 | @Environment(\.systemNotificationStyle)
54 | private var envStyle
55 |
56 | @State
57 | private var currentId = UUID()
58 |
59 | public var body: some View {
60 | ZStack(alignment: edge.alignment) {
61 | Color.clear
62 | content(isActive)
63 | .background(style.backgroundColor)
64 | .background(style.backgroundMaterial)
65 | .compositingGroup()
66 | .cornerRadius(style.cornerRadius ?? 1_000)
67 | .shadow(
68 | color: style.shadowColor,
69 | radius: style.shadowRadius,
70 | y: style.shadowOffset)
71 | .animation(config.animation, value: isActive)
72 | .offset(x: 0, y: verticalOffset)
73 | #if os(iOS) || os(macOS) || os(watchOS) || os(visionOS)
74 | .gesture(swipeGesture, if: config.isSwipeToDismissEnabled)
75 | #endif
76 | .padding(style.padding)
77 | .onChange(of: isActive, perform: handlePresentation)
78 | }
79 | }
80 | }
81 |
82 | private extension SystemNotification {
83 |
84 | var config: SystemNotificationConfiguration {
85 | initConfig ?? envConfig
86 | }
87 |
88 | var edge: SystemNotificationEdge {
89 | config.edge
90 | }
91 |
92 | var verticalOffset: CGFloat {
93 | if isActive { return 0 }
94 | switch edge {
95 | case .top: return -250
96 | case .bottom: return 250
97 | }
98 | }
99 |
100 | func dismiss() {
101 | isActive = false
102 | }
103 |
104 | var style: SystemNotificationStyle {
105 | initStyle ?? envStyle
106 | }
107 | }
108 |
109 | @MainActor
110 | private extension SystemNotification {
111 |
112 | func handlePresentation(_ isPresented: Bool) {
113 | guard isPresented else { return }
114 | currentId = UUID()
115 | let id = currentId
116 | DispatchQueue.main.asyncAfter(deadline: .now() + config.duration) {
117 | guard id == currentId else { return }
118 | isActive = false
119 | }
120 | }
121 | }
122 |
123 |
124 | // MARK: - Private View Logic
125 |
126 | private extension View {
127 |
128 | @ViewBuilder
129 | func gesture(
130 | _ gesture: GestureType,
131 | if condition: Bool
132 | ) -> some View {
133 | if condition {
134 | self.gesture(gesture)
135 | } else {
136 | self
137 | }
138 | }
139 | }
140 |
141 | private extension SystemNotification {
142 |
143 | @ViewBuilder
144 | var background: some View {
145 | if let color = style.backgroundColor {
146 | color
147 | } else {
148 | SystemNotificationStyle.standardBackgroundColor(for: colorScheme)
149 | }
150 | }
151 |
152 | #if os(iOS) || os(macOS) || os(watchOS) || os(visionOS)
153 | var swipeGesture: some Gesture {
154 | DragGesture(minimumDistance: 20, coordinateSpace: .global)
155 | .onEnded { value in
156 | let horizontalTranslation = value.translation.width as CGFloat
157 | let verticalTranslation = value.translation.height as CGFloat
158 | let isVertical = abs(verticalTranslation) > abs(horizontalTranslation)
159 | let isUp = verticalTranslation < 0
160 | // let isLeft = horizontalTranslation < 0
161 | guard isVertical else { return } // We only use vertical edges
162 | if isUp && edge == .top { dismiss() }
163 | if !isUp && edge == .bottom { dismiss() }
164 | }
165 | }
166 | #endif
167 | }
168 |
169 | #Preview {
170 |
171 | struct Preview: View {
172 |
173 | @State var isPresented = false
174 |
175 | var body: some View {
176 | ZStack {
177 | AsyncImage(url: .init(string: "https://picsum.photos/500/500")) {
178 | $0.image?
179 | .resizable()
180 | .aspectRatio(contentMode: .fill)
181 | }
182 | .clipped()
183 | .ignoresSafeArea()
184 |
185 | SystemNotification(
186 | isActive: $isPresented
187 | ) { _ in
188 | SystemNotificationMessage(
189 | icon: Image(systemName: "bell.fill"),
190 | title: "Silent mode",
191 | text: "Silent mode is off"
192 | )
193 | }
194 | .systemNotificationStyle(.standard)
195 | .systemNotificationConfiguration(
196 | .init(animation: .bouncy)
197 | )
198 |
199 | SystemNotification(
200 | isActive: $isPresented
201 | ) { _ in
202 | Text("HELLO")
203 | .padding()
204 | }
205 | .systemNotificationStyle(
206 | .init(backgroundColor: .blue)
207 | )
208 | .systemNotificationConfiguration(
209 | .init(animation: .smooth, edge: .bottom)
210 | )
211 | }
212 | #if os(iOS)
213 | .onTapGesture {
214 | isPresented.toggle()
215 | }
216 | #endif
217 | }
218 | }
219 |
220 | return Preview()
221 | }
222 |
223 | #Preview("README #1") {
224 |
225 | struct MyView: View {
226 |
227 | @State
228 | var isActive = false
229 |
230 | var body: some View {
231 | VStack {
232 | Button("Show notification") {
233 | isActive = true
234 | }
235 | }
236 | .systemNotification(isActive: $isActive) {
237 | Text("You can use any custom content view")
238 | .padding()
239 | }
240 | }
241 | }
242 |
243 | return MyView()
244 | }
245 |
246 |
247 | #Preview("README #2") {
248 |
249 | struct MyView: View {
250 |
251 | @StateObject
252 | var notification = SystemNotificationContext()
253 |
254 | var body: some View {
255 | VStack {
256 | Button("Show text") {
257 | notification.present {
258 | Text("Context-based notifications are more flexible.")
259 | .padding()
260 | .multilineTextAlignment(.center)
261 | }
262 | }
263 | Button("Show message") {
264 | notification.present {
265 | SystemNotificationMessage(
266 | icon: Text("👍"),
267 | title: "Great job!",
268 | text: "You presented a native-looking message!"
269 | )
270 | }
271 | }
272 | }
273 | .systemNotification(notification)
274 | .systemNotificationConfiguration(.init(animation: .bouncy))
275 | }
276 | }
277 |
278 | return MyView()
279 | }
280 |
281 |
282 | #Preview("README #3") {
283 |
284 | struct MyView: View {
285 |
286 | @State
287 | var isSilentModeEnabled = false
288 |
289 | @StateObject
290 | var notification = SystemNotificationContext()
291 |
292 | var body: some View {
293 | List {
294 | Toggle("Silent Mode", isOn: $isSilentModeEnabled)
295 | }
296 | .systemNotification(notification)
297 | .onChange(of: isSilentModeEnabled) { value in
298 | notification.presentMessage(.silentMode(isOn: value))
299 | }
300 | }
301 | }
302 |
303 | return MyView()
304 | }
305 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationConfiguration.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This type can configure a ``SystemNotification``.
12 | ///
13 | /// See for more information on how to
14 | /// style and configure system notifications.
15 | ///
16 | /// You can apply a custom value with the corresponding view
17 | /// modifier. The ``standard`` value is used by default when
18 | /// you don't apply a custom value.
19 | ///
20 | /// You can use the ``standardToast`` configuration when you
21 | /// want to present the notification as a bottom toast.
22 | public struct SystemNotificationConfiguration {
23 |
24 | /// Create a custom system notification configuration.
25 | ///
26 | /// - Parameters:
27 | /// - animation: The animation to apply when sliding in the notification, by default `.bouncy`.
28 | /// - duration: The number of seconds the notification should be presented, by default `3`.
29 | /// - edge: The edge from which to present the notification, by default `.top`.
30 | /// - isSwipeToDismissEnabled: Whether or not a user can swipe to dismiss a notification, by default `true`.
31 | public init(
32 | animation: Animation = .bouncy,
33 | duration: TimeInterval = 3,
34 | edge: SystemNotificationEdge = .top,
35 | isSwipeToDismissEnabled: Bool = true
36 | ) {
37 | self.animation = animation
38 | self.duration = duration
39 | self.edge = edge
40 | self.isSwipeToDismissEnabled = isSwipeToDismissEnabled
41 | }
42 |
43 | /// The animation to use when presenting a notification.
44 | public var animation: Animation
45 |
46 | /// The number of seconds a notification should be shown.
47 | public var duration: TimeInterval
48 |
49 | /// The edge to present from.
50 | public var edge: SystemNotificationEdge = .top
51 |
52 | /// Whether or not swiping can to dismiss a notification.
53 | public var isSwipeToDismissEnabled: Bool
54 | }
55 |
56 | public extension SystemNotificationConfiguration {
57 |
58 | /// The standard system notification configuration.
59 | static var standard: Self { .init() }
60 |
61 | /// A standard toast configuration.
62 | static var standardToast: Self {
63 | .init(
64 | animation: .bouncy,
65 | edge: .bottom
66 | )
67 | }
68 | }
69 |
70 | public extension View {
71 |
72 | /// Apply a ``SystemNotificationConfiguration`` to the view.
73 | func systemNotificationConfiguration(
74 | _ configuration: SystemNotificationConfiguration
75 | ) -> some View {
76 | self.environment(\.systemNotificationConfiguration, configuration)
77 | }
78 | }
79 |
80 | extension View {
81 |
82 | @ViewBuilder
83 | func systemNotificationConfiguration(
84 | _ configuration: SystemNotificationConfiguration?
85 | ) -> some View {
86 | if let configuration {
87 | self.environment(\.systemNotificationConfiguration, configuration)
88 | } else {
89 | self
90 | }
91 | }
92 | }
93 |
94 | private extension SystemNotificationConfiguration {
95 |
96 | struct Key: EnvironmentKey {
97 |
98 | static var defaultValue: SystemNotificationConfiguration { .init() }
99 | }
100 | }
101 |
102 | public extension EnvironmentValues {
103 |
104 | var systemNotificationConfiguration: SystemNotificationConfiguration {
105 | get { self [SystemNotificationConfiguration.Key.self] }
106 | set { self [SystemNotificationConfiguration.Key.self] = newValue }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationContext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationContext.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-02.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This context can be used to present system notifications
12 | /// in a more flexible way.
13 | public class SystemNotificationContext: ObservableObject {
14 |
15 | public init() {}
16 |
17 | public typealias Action = () -> Void
18 |
19 | @Published
20 | public var content = AnyView(EmptyView())
21 |
22 | @Published
23 | public var isActive = false
24 | }
25 |
26 | @MainActor
27 | public extension SystemNotificationContext {
28 |
29 | var isActiveBinding: Binding {
30 | .init(get: { self.isActive },
31 | set: { self.isActive = $0 }
32 | )
33 | }
34 |
35 | /// Dismiss the current notification, if any.
36 | func dismiss() {
37 | dismiss {}
38 | }
39 |
40 | /// Dismiss the current notification, if any.
41 | func dismiss(
42 | completion: @escaping Action
43 | ) {
44 | guard isActive else { return completion() }
45 | isActive = false
46 | perform(after: 0.3, action: completion)
47 | }
48 |
49 | /// Present a system notification.
50 | func present(
51 | _ content: Content,
52 | afterDelay delay: TimeInterval = 0
53 | ) {
54 | dismiss {
55 | self.perform(after: delay) {
56 | self.presentAfterDismiss(content)
57 | }
58 | }
59 | }
60 |
61 | /// Present a system notification.
62 | func present(
63 | afterDelay delay: TimeInterval = 0,
64 | @ViewBuilder content: @escaping () -> Content
65 | ) {
66 | present(content(), afterDelay: delay)
67 | }
68 |
69 | /// Present a system notification message.
70 | func presentMessage(
71 | _ message: SystemNotificationMessage,
72 | afterDelay delay: TimeInterval = 0
73 | ) {
74 | present(message, afterDelay: delay)
75 | }
76 | }
77 |
78 | @MainActor
79 | private extension SystemNotificationContext {
80 |
81 | func perform(
82 | _ action: @escaping Action,
83 | after seconds: TimeInterval
84 | ) {
85 | guard seconds > 0 else { return action() }
86 | DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
87 | action()
88 | }
89 | }
90 |
91 | func perform(after seconds: TimeInterval, action: @escaping Action) {
92 | perform(action, after: seconds)
93 | }
94 |
95 | func presentAfterDismiss(_ content: Content) {
96 | self.content = AnyView(content)
97 | perform(setActive, after: 0.1)
98 | }
99 |
100 | func setActive() {
101 | isActive = true
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationEdge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationEdge.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This enum defines edges from which a system notification
12 | /// can be presented.
13 | public enum SystemNotificationEdge {
14 |
15 | case top, bottom
16 |
17 | public var alignment: Alignment {
18 | switch self {
19 | case .top: .top
20 | case .bottom: .bottom
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationMessage+Predefined.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationMessage+Predefined.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2024-04-24.
6 | // Copyright © 2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public extension SystemNotificationMessageStyle {
12 |
13 | static var error: Self {
14 | prominent(backgroundColor: .red)
15 | }
16 |
17 | static var success: Self {
18 | prominent(backgroundColor: .green)
19 | }
20 |
21 | static var warning: Self {
22 | prominent(backgroundColor: .orange)
23 | }
24 |
25 | static func prominent(
26 | backgroundColor: Color
27 | ) -> Self {
28 | .init(
29 | backgroundColor: backgroundColor,
30 | iconColor: .white,
31 | textColor: .white.opacity(0.8),
32 | titleColor: .white
33 | )
34 | }
35 | }
36 |
37 | public extension SystemNotificationMessage where IconView == Image {
38 |
39 | static func error(
40 | icon: Image = .init(systemName: "exclamationmark.triangle"),
41 | title: LocalizedStringKey? = nil,
42 | text: LocalizedStringKey,
43 | bundle: Bundle? = nil
44 | ) -> Self {
45 | .init(
46 | icon: icon,
47 | title: title,
48 | text: text,
49 | style: .error,
50 | bundle: bundle
51 | )
52 | }
53 |
54 | static func success(
55 | icon: Image = .init(systemName: "checkmark"),
56 | title: LocalizedStringKey? = nil,
57 | text: LocalizedStringKey,
58 | bundle: Bundle? = nil
59 | ) -> Self {
60 | .init(
61 | icon: icon,
62 | title: title,
63 | text: text,
64 | style: .success,
65 | bundle: bundle
66 | )
67 | }
68 |
69 | static func warning(
70 | icon: Image = .init(systemName: "exclamationmark.triangle"),
71 | title: LocalizedStringKey? = nil,
72 | text: LocalizedStringKey,
73 | bundle: Bundle? = nil
74 | ) -> Self {
75 | .init(
76 | icon: icon,
77 | title: title,
78 | text: text,
79 | style: .warning,
80 | bundle: bundle
81 | )
82 | }
83 | }
84 |
85 | public extension SystemNotificationMessage where IconView == AnyView {
86 |
87 | /// This message mimics a native iOS silent mode message.
88 | static func silentMode(
89 | isOn: Bool,
90 | title: LocalizedStringKey? = nil
91 | ) -> Self {
92 | .init(
93 | icon: AnyView(SilentModeBell(isSilentModeOn: isOn)),
94 | text: title ?? "Silent Mode \(isOn ? "On" : "Off")"
95 | )
96 | }
97 | }
98 |
99 | private struct SilentModeBell: View {
100 |
101 | var isSilentModeOn = false
102 |
103 | @State
104 | private var isRotated: Bool = false
105 |
106 | @State
107 | private var isAnimated: Bool = false
108 |
109 | var body: some View {
110 | Image(systemName: iconName)
111 | .rotationEffect(
112 | .degrees(isRotated ? -45 : 0),
113 | anchor: .top
114 | )
115 | .animation(
116 | .interpolatingSpring(
117 | mass: 0.5,
118 | stiffness: animationStiffness,
119 | damping: animationDamping,
120 | initialVelocity: 0
121 | ),
122 | value: isAnimated)
123 | .foregroundColor(iconColor)
124 | .onAppear(perform: animate)
125 | }
126 | }
127 |
128 | @MainActor
129 | private extension SilentModeBell {
130 |
131 | func animate() {
132 | withAnimation { isRotated = true }
133 | perform(after: 0.1) {
134 | isRotated = false
135 | isAnimated = true
136 | }
137 | }
138 |
139 | func perform(after: Double, action: @escaping () -> Void) {
140 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
141 | action()
142 | }
143 | }
144 |
145 | var animationDamping: Double {
146 | isSilentModeOn ? 4 : 1.5
147 | }
148 |
149 | var animationStiffness: Double {
150 | isSilentModeOn ? 129 : 179
151 | }
152 |
153 | var iconName: String {
154 | isSilentModeOn ? "bell.slash.fill" : "bell.fill"
155 | }
156 |
157 | var iconColor: Color {
158 | isSilentModeOn ? .red : .gray
159 | }
160 | }
161 |
162 | #Preview {
163 |
164 | VStack {
165 | SystemNotificationMessage.silentMode(isOn: true)
166 | SystemNotificationMessage.silentMode(isOn: false)
167 | SystemNotificationMessage.error(title: "Error!", text: "Something failed!")
168 | SystemNotificationMessage.success(title: "Success!", text: "You did it!")
169 | SystemNotificationMessage.warning(title: "Warning!", text: "Danger ahead!")
170 | }
171 | .padding()
172 | .background(Color.black.opacity(0.1))
173 | .clipShape(.rect(cornerRadius: 10))
174 | }
175 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationMessage.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This view mimics the message view that is shown within a
12 | /// native iOS system notification.
13 | ///
14 | /// You can provide a custom icon view, title, and text, and
15 | /// e.g. animate the icon when it's presented.
16 | ///
17 | /// You can easily create custom messages, by extending this
18 | /// type with static message builders, for instance:
19 | ///
20 | /// ```swift
21 | /// extension SystemNotificationMessage where IconView == Image {
22 | ///
23 | /// static func silentMode(on: Bool) -> Self {
24 | /// ...
25 | /// }
26 | /// }
27 | /// ```
28 | public struct SystemNotificationMessage: View {
29 |
30 | /// Create a system notification message view.
31 | ///
32 | /// - Parameters:
33 | /// - icon: The leading icon view.
34 | /// - title: The bold title text, by default `nil`.
35 | /// - text: The plain message text.
36 | /// - style: An optional, explicit style to apply..
37 | /// - bundle: The bundle of the localized texts, by default `.main`.
38 | public init(
39 | icon: IconView,
40 | title: LocalizedStringKey? = nil,
41 | text: LocalizedStringKey,
42 | style: SystemNotificationMessageStyle? = nil,
43 | bundle: Bundle? = nil
44 | ) {
45 | self.icon = icon
46 | self.title = title
47 | self.text = text
48 | self.initStyle = style
49 | self.bundle = bundle
50 | }
51 |
52 | /// Create a system notification message view.
53 | ///
54 | /// - Parameters:
55 | /// - icon: The leading icon image.
56 | /// - title: The bold title text, by default `nil`.
57 | /// - text: The plain message text.
58 | /// - style: An optional, explicit style to apply.
59 | /// - bundle: The bundle of the localized texts, by default `.main`.
60 | public init(
61 | icon: Image,
62 | title: LocalizedStringKey? = nil,
63 | text: LocalizedStringKey,
64 | style: SystemNotificationMessageStyle? = nil,
65 | bundle: Bundle? = nil
66 | ) where IconView == Image {
67 | self.icon = icon
68 | self.title = title
69 | self.text = text
70 | self.initStyle = style
71 | self.bundle = bundle
72 | }
73 |
74 | /// Create a system notification message view.
75 | ///
76 | /// - Parameters:
77 | /// - title: The bold title text, by default `nil`.
78 | /// - text: The plain message text.
79 | /// - style: An optional, explicit style to apply.
80 | /// - bundle: The bundle of the localized texts, by default `.main`.
81 | public init(
82 | title: LocalizedStringKey? = nil,
83 | text: LocalizedStringKey,
84 | style: SystemNotificationMessageStyle? = nil,
85 | bundle: Bundle? = nil
86 | ) where IconView == EmptyView {
87 | self.icon = EmptyView()
88 | self.title = title
89 | self.text = text
90 | self.initStyle = style
91 | self.bundle = bundle
92 | }
93 |
94 | let icon: IconView
95 | let title: LocalizedStringKey?
96 | let text: LocalizedStringKey
97 | let initStyle: SystemNotificationMessageStyle?
98 | let bundle: Bundle?
99 |
100 | @Environment(\.systemNotificationMessageStyle)
101 | private var environmentStyle
102 |
103 | public var body: some View {
104 | HStack(spacing: style.iconTextSpacing) {
105 | iconView
106 | .id(UUID())
107 | textContent
108 | iconView.opacity(0.001)
109 | }
110 | .padding(.vertical, style.padding.height)
111 | .padding(.horizontal, style.padding.width)
112 | .background(style.backgroundColor)
113 | }
114 | }
115 |
116 | private extension SystemNotificationMessage {
117 |
118 | var style: SystemNotificationMessageStyle {
119 | initStyle ?? environmentStyle
120 | }
121 |
122 | func foregroundColor(
123 | for color: Color
124 | ) -> Color {
125 | style.foregroundColor ?? color
126 | }
127 | }
128 |
129 | private extension SystemNotificationMessage {
130 |
131 | var textContent: some View {
132 | VStack(spacing: style.titleTextSpacing) {
133 | if let title = title {
134 | Text(title, bundle: bundle ?? nil)
135 | .font(style.titleFont)
136 | .foregroundStyle(foregroundColor(for: style.titleColor))
137 | }
138 | Text(text, bundle: bundle ?? nil)
139 | .font(style.textFont)
140 | .foregroundStyle(foregroundColor(for: style.textColor))
141 | }
142 | .multilineTextAlignment(.center)
143 | }
144 |
145 | @ViewBuilder
146 | var iconView: some View {
147 | icon.font(style.iconFont)
148 | .foregroundStyle(foregroundColor(for: style.iconColor))
149 | }
150 | }
151 |
152 | #Preview {
153 |
154 | VStack {
155 | Group {
156 | SystemNotificationMessage(
157 | icon: Image(systemName: "bell.slash.fill"),
158 | title: "Silent mode",
159 | text: "On"
160 | )
161 | .systemNotificationMessageStyle(
162 | .init(
163 | backgroundColor: .yellow,
164 | iconColor: .red
165 | )
166 | )
167 |
168 | SystemNotificationMessage(
169 | icon: Color.red.frame(width: 20, height: 20),
170 | text: "Custom icon view, no title"
171 | )
172 | .systemNotificationMessageStyle(
173 | .init(iconColor: .red)
174 | )
175 |
176 | SystemNotificationMessage(
177 | title: "No icon",
178 | text: "On"
179 | )
180 | .systemNotificationMessageStyle(
181 | .init(iconColor: .red)
182 | )
183 |
184 | SystemNotificationMessage(
185 | icon: Image(systemName: "exclamationmark.triangle"),
186 | title: "Warning",
187 | text: "This is a long warning message to demonstrate multiline messages."
188 | )
189 | .systemNotificationMessageStyle(.warning)
190 | }
191 | .background(Color.white)
192 | .cornerRadius(5)
193 | .padding()
194 | }
195 | .background(Color.gray)
196 | }
197 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationMessageStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationMessageStyle.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This style can style a ``SystemNotificationMessage``.
12 | ///
13 | /// You can either set an overall foreground color, which is
14 | /// then applied to all components, or use individual colors.
15 | ///
16 | /// See for more information on how to
17 | /// style and configure system notifications.
18 | ///
19 | /// You can apply a custom value with the corresponding view
20 | /// modifier. The ``standard`` value is used by default when
21 | /// you don't apply a custom value.
22 | public struct SystemNotificationMessageStyle {
23 |
24 | /// Create a custom system notification message style.
25 | ///
26 | /// - Parameters:
27 | /// - backgroundColor: The overall background color.
28 | /// - foregroundColor: The overall foreground color.
29 | /// - iconColor: The color to apply to the icon.
30 | /// - iconFont: The font to apply to the icon.
31 | /// - iconTextSpacing: The spacing to apply between the icon and the text.
32 | /// - padding: The padding to add to the content.
33 | /// - textColor: The color to apply to the text.
34 | /// - textFont: The font to apply to the text.
35 | /// - titleColor: The color to apply to the title.
36 | /// - titleFont: The font to apply to the title.
37 | /// - titleTextSpacing: The spacing to apply between the title and the text.
38 | public init(
39 | backgroundColor: Color? = nil,
40 | foregroundColor: Color? = nil,
41 | iconColor: Color = .primary.opacity(0.6),
42 | iconFont: Font = Font.title3,
43 | iconTextSpacing: CGFloat = 20,
44 | padding: CGSize = .init(width: 15, height: 7),
45 | textColor: Color = .secondary,
46 | textFont: Font = Font.footnote.bold(),
47 | titleColor: Color = .primary,
48 | titleFont: Font = Font.footnote.bold(),
49 | titleTextSpacing: CGFloat = 2
50 | ) {
51 | self.backgroundColor = backgroundColor
52 | self.foregroundColor = foregroundColor
53 | self.iconColor = iconColor
54 | self.iconFont = iconFont
55 | self.iconTextSpacing = iconTextSpacing
56 | self.padding = padding
57 | self.textColor = textColor
58 | self.textFont = textFont
59 | self.titleColor = titleColor
60 | self.titleFont = titleFont
61 | self.titleTextSpacing = titleTextSpacing
62 | }
63 |
64 | /// The overall background color.
65 | public var backgroundColor: Color?
66 |
67 | /// The overall foreground color.
68 | public var foregroundColor: Color?
69 |
70 | /// The color to apply to the icon.
71 | public var iconColor: Color
72 |
73 | /// The font to apply to the icon.
74 | public var iconFont: Font
75 |
76 | /// The spacing to apply between the icon and the text.
77 | public var iconTextSpacing: CGFloat
78 |
79 | /// The padding to add to the content.
80 | public var padding: CGSize
81 |
82 | /// The color to apply to the text.
83 | public var textColor: Color
84 |
85 | /// The font to apply to the text.
86 | public var textFont: Font
87 |
88 | /// The color to apply to the title.
89 | public var titleColor: Color
90 |
91 | /// The font to apply to the title.
92 | public var titleFont: Font
93 |
94 | /// The spacing to apply between the title and the text.
95 | public var titleTextSpacing: CGFloat
96 | }
97 |
98 | public extension SystemNotificationMessageStyle {
99 |
100 | /// The standard system notification message style.
101 | static var standard: Self { .init() }
102 | }
103 |
104 | public extension View {
105 |
106 | /// Apply a ``SystemNotificationMessageStyle`` to the view.
107 | func systemNotificationMessageStyle(
108 | _ style: SystemNotificationMessageStyle
109 | ) -> some View {
110 | self.environment(\.systemNotificationMessageStyle, style)
111 | }
112 | }
113 |
114 | private extension SystemNotificationMessageStyle {
115 |
116 | struct Key: EnvironmentKey {
117 |
118 | static var defaultValue: SystemNotificationMessageStyle { .standard }
119 | }
120 | }
121 |
122 | public extension EnvironmentValues {
123 |
124 | var systemNotificationMessageStyle: SystemNotificationMessageStyle {
125 | get { self [SystemNotificationMessageStyle.Key.self] }
126 | set { self [SystemNotificationMessageStyle.Key.self] = newValue }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/SystemNotificationStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemNotificationStyle.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This style can style a ``SystemNotification``.
12 | ///
13 | /// See for more information on how to
14 | /// style and configure system notifications.
15 | ///
16 | /// You can apply a custom value with the corresponding view
17 | /// modifier. The ``standard`` value is used by default when
18 | /// you don't apply a custom value.
19 | public struct SystemNotificationStyle {
20 |
21 | /// Create a custom system notification style.
22 | ///
23 | /// - Parameters:
24 | /// - backgroundColor: The background color to apply, by default `nil`.
25 | /// - backgroundMaterial: The background material to apply, by default `.thin`.
26 | /// - cornerRadius: The corner radius to apply, by default `nil`.
27 | /// - padding: The edge padding to apply, by default `nil`.
28 | /// - edge: The edge from which to present the notification, by default `.top`.
29 | /// - shadowColor: The shadow color to apply, by default `.black.opacity(0.1)`.
30 | /// - shadowOffset: The shadow offset to apply, by default `5`.
31 | /// - shadowRadius: The shadow radius to apply, by default `7.5`.
32 | public init(
33 | backgroundColor: Color? = nil,
34 | backgroundMaterial: Material = .thin,
35 | cornerRadius: CGFloat? = nil,
36 | padding: EdgeInsets? = nil,
37 | shadowColor: Color = .black.opacity(0.1),
38 | shadowOffset: CGFloat = 5,
39 | shadowRadius: CGFloat = 7.5
40 | ) {
41 | self.backgroundColor = backgroundColor
42 | self.backgroundMaterial = backgroundMaterial
43 | self.cornerRadius = cornerRadius
44 | self.padding = padding ?? Self.standardPadding
45 | self.shadowColor = shadowColor
46 | self.shadowOffset = shadowOffset
47 | self.shadowRadius = shadowRadius
48 | }
49 |
50 | /// The standard background color.
51 | @ViewBuilder
52 | public static func standardBackgroundColor(for colorScheme: ColorScheme) -> some View {
53 | if colorScheme == .light {
54 | Color.primary.colorInvert()
55 | } else {
56 | #if os(iOS)
57 | Color(UIColor.secondarySystemBackground)
58 | #elseif os(macOS)
59 | Color.primary.colorInvert()
60 | #elseif os(macOS)
61 | Color.secondary
62 | .colorInvert()
63 | .background(Color.white)
64 | #endif
65 | }
66 | }
67 |
68 | /// The standard content padding.
69 | public static var standardPadding: EdgeInsets {
70 | #if os(iOS)
71 | EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
72 | #else
73 | EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
74 | #endif
75 | }
76 |
77 | /// The background color to apply.
78 | public var backgroundColor: Color?
79 |
80 | /// The background material to apply.
81 | public var backgroundMaterial: Material
82 |
83 | /// The corner radius to apply.
84 | public var cornerRadius: CGFloat?
85 |
86 | /// The edge padding to apply.
87 | public var padding: EdgeInsets
88 |
89 | /// The shadow color to apply.
90 | public var shadowColor: Color
91 |
92 | /// The shadow offset to apply.
93 | public var shadowOffset: CGFloat
94 |
95 | /// The shadow radius to apply.
96 | public var shadowRadius: CGFloat
97 | }
98 |
99 | public extension SystemNotificationStyle {
100 |
101 | /// The standard system notification style.
102 | static var standard: Self { .init() }
103 | }
104 |
105 | public extension View {
106 |
107 | /// Apply a ``SystemNotificationStyle`` to the view.
108 | func systemNotificationStyle(
109 | _ style: SystemNotificationStyle
110 | ) -> some View {
111 | self.environment(\.systemNotificationStyle, style)
112 | }
113 | }
114 |
115 | extension View {
116 |
117 | @ViewBuilder
118 | func systemNotificationStyle(
119 | _ style: SystemNotificationStyle?
120 | ) -> some View {
121 | if let style {
122 | self.environment(\.systemNotificationStyle, style)
123 | } else {
124 | self
125 | }
126 | }
127 | }
128 |
129 | private extension SystemNotificationStyle {
130 |
131 | struct Key: EnvironmentKey {
132 |
133 | static var defaultValue: SystemNotificationStyle { .standard }
134 | }
135 | }
136 |
137 | public extension EnvironmentValues {
138 |
139 | var systemNotificationStyle: SystemNotificationStyle {
140 | get { self [SystemNotificationStyle.Key.self] }
141 | set { self [SystemNotificationStyle.Key.self] = newValue }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Sources/SystemNotification/View+SystemNotification.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+SystemNotification.swift
3 | // SystemNotification
4 | //
5 | // Created by Daniel Saidi on 2021-06-01.
6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public extension View {
12 |
13 | /// Attach a ``SystemNotification`` to the view.
14 | ///
15 | /// State-based system notifications make it easy to use
16 | /// a single binding to control a specific notification.
17 | ///
18 | /// After applying the modifier, you can use the binding
19 | /// to present the provided `content`.
20 | func systemNotification(
21 | isActive: Binding,
22 | content: @escaping () -> Content
23 | ) -> some View {
24 | ZStack {
25 | self
26 | SystemNotification(
27 | isActive: isActive,
28 | content: { _ in content() }
29 | )
30 | }
31 | }
32 |
33 | /// Attach a system notification context to the view.
34 | ///
35 | /// Context-based system notifications make it very easy
36 | /// to show multiple notifications with a single context.
37 | ///
38 | /// After applying the modifier, you can use the context
39 | /// to present notifications.
40 | ///
41 | /// This modifier will also pass in the context into the
42 | /// environment, as an environment object.
43 | func systemNotification(
44 | _ context: SystemNotificationContext
45 | ) -> some View {
46 | self.systemNotification(
47 | isActive: context.isActiveBinding,
48 | content: { context.content }
49 | )
50 | .environmentObject(context)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/SystemNotificationTests/SystemNotificationTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SystemNotification
3 |
4 | final class SystemNotificationTests: XCTestCase {
5 |
6 | func testExample() {}
7 | }
8 |
--------------------------------------------------------------------------------
/package_version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script creates a new project version for the current project.
5 | # You can customize this to fit your project when you copy these scripts.
6 | # You can pass in a custom branch if you don't want to use the default one.
7 |
8 | SCRIPT="scripts/package_version.sh"
9 | chmod +x $SCRIPT
10 | bash $SCRIPT
11 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script builds a for all provided .
5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default.
6 | # You can pass in a list of if you want to customize the build.
7 |
8 | # Usage:
9 | # build.sh [ default:iOS macOS tvOS watchOS xrOS]
10 | # e.g. `bash scripts/build.sh MyTarget iOS macOS`
11 |
12 | # Exit immediately if a command exits with a non-zero status
13 | set -e
14 |
15 | # Verify that all required arguments are provided
16 | if [ $# -eq 0 ]; then
17 | echo "Error: This script requires at least one argument"
18 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]"
19 | echo "For instance: $0 MyTarget iOS macOS"
20 | exit 1
21 | fi
22 |
23 | # Define argument variables
24 | TARGET=$1
25 |
26 | # Remove TARGET from arguments list
27 | shift
28 |
29 | # Define platforms variable
30 | if [ $# -eq 0 ]; then
31 | set -- iOS macOS tvOS watchOS xrOS
32 | fi
33 | PLATFORMS=$@
34 |
35 | # A function that builds $TARGET for a specific platform
36 | build_platform() {
37 |
38 | # Define a local $PLATFORM variable
39 | local PLATFORM=$1
40 |
41 | # Build $TARGET for the $PLATFORM
42 | echo "Building $TARGET for $PLATFORM..."
43 | if ! xcodebuild -scheme $TARGET -derivedDataPath .build -destination generic/platform=$PLATFORM; then
44 | echo "Failed to build $TARGET for $PLATFORM"
45 | return 1
46 | fi
47 |
48 | # Complete successfully
49 | echo "Successfully built $TARGET for $PLATFORM"
50 | }
51 |
52 | # Start script
53 | echo ""
54 | echo "Building $TARGET for [$PLATFORMS]..."
55 | echo ""
56 |
57 | # Loop through all platforms and call the build function
58 | for PLATFORM in $PLATFORMS; do
59 | if ! build_platform "$PLATFORM"; then
60 | exit 1
61 | fi
62 | done
63 |
64 | # Complete successfully
65 | echo ""
66 | echo "Building $TARGET completed successfully!"
67 | echo ""
68 |
--------------------------------------------------------------------------------
/scripts/chmod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script makes all scripts in this folder executable.
5 |
6 | # Usage:
7 | # scripts_chmod.sh
8 | # e.g. `bash scripts/chmod.sh`
9 |
10 | # Exit immediately if a command exits with a non-zero status
11 | set -e
12 |
13 | # Use the script folder to refer to other scripts.
14 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
15 |
16 | # Find all .sh files in the FOLDER except chmod.sh
17 | find "$FOLDER" -name "*.sh" ! -name "chmod.sh" -type f | while read -r script; do
18 | chmod +x "$script"
19 | done
20 |
--------------------------------------------------------------------------------
/scripts/docc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script builds DocC for a and certain .
5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default.
6 | # You can pass in a list of if you want to customize the build.
7 | # The documentation ends up in to .build/docs-.
8 |
9 | # Usage:
10 | # docc.sh [ default:iOS macOS tvOS watchOS xrOS]
11 | # e.g. `bash scripts/docc.sh MyTarget iOS macOS`
12 |
13 | # Exit immediately if a command exits with a non-zero status
14 | set -e
15 |
16 | # Fail if any command in a pipeline fails
17 | set -o pipefail
18 |
19 | # Verify that all required arguments are provided
20 | if [ $# -eq 0 ]; then
21 | echo "Error: This script requires at least one argument"
22 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]"
23 | echo "For instance: $0 MyTarget iOS macOS"
24 | exit 1
25 | fi
26 |
27 | # Define argument variables
28 | TARGET=$1
29 | TARGET_LOWERCASED=$(echo "$1" | tr '[:upper:]' '[:lower:]')
30 |
31 | # Remove TARGET from arguments list
32 | shift
33 |
34 | # Define platforms variable
35 | if [ $# -eq 0 ]; then
36 | set -- iOS macOS tvOS watchOS xrOS
37 | fi
38 | PLATFORMS=$@
39 |
40 | # Prepare the package for DocC
41 | swift package resolve;
42 |
43 | # A function that builds $TARGET for a specific platform
44 | build_platform() {
45 |
46 | # Define a local $PLATFORM variable and set an exit code
47 | local PLATFORM=$1
48 | local EXIT_CODE=0
49 |
50 | # Define the build folder name, based on the $PLATFORM
51 | case $PLATFORM in
52 | "iOS")
53 | DEBUG_PATH="Debug-iphoneos"
54 | ;;
55 | "macOS")
56 | DEBUG_PATH="Debug"
57 | ;;
58 | "tvOS")
59 | DEBUG_PATH="Debug-appletvos"
60 | ;;
61 | "watchOS")
62 | DEBUG_PATH="Debug-watchos"
63 | ;;
64 | "xrOS")
65 | DEBUG_PATH="Debug-xros"
66 | ;;
67 | *)
68 | echo "Error: Unsupported platform '$PLATFORM'"
69 | exit 1
70 | ;;
71 | esac
72 |
73 | # Build $TARGET docs for the $PLATFORM
74 | echo "Building $TARGET docs for $PLATFORM..."
75 | if ! xcodebuild docbuild -scheme $TARGET -derivedDataPath .build/docbuild -destination "generic/platform=$PLATFORM"; then
76 | echo "Error: Failed to build documentation for $PLATFORM" >&2
77 | return 1
78 | fi
79 |
80 | # Transform docs for static hosting
81 | if ! $(xcrun --find docc) process-archive \
82 | transform-for-static-hosting .build/docbuild/Build/Products/$DEBUG_PATH/$TARGET.doccarchive \
83 | --output-path .build/docs-$PLATFORM \
84 | --hosting-base-path "$TARGET"; then
85 | echo "Error: Failed to transform documentation for $PLATFORM" >&2
86 | return 1
87 | fi
88 |
89 | # Inject a root redirect script on the root page
90 | echo "" > .build/docs-$PLATFORM/index.html;
91 |
92 | # Complete successfully
93 | echo "Successfully built $TARGET docs for $PLATFORM"
94 | return 0
95 | }
96 |
97 | # Start script
98 | echo ""
99 | echo "Building $TARGET docs for [$PLATFORMS]..."
100 | echo ""
101 |
102 | # Loop through all platforms and call the build function
103 | for PLATFORM in $PLATFORMS; do
104 | if ! build_platform "$PLATFORM"; then
105 | exit 1
106 | fi
107 | done
108 |
109 | # Complete successfully
110 | echo ""
111 | echo "Building $TARGET docs completed successfully!"
112 | echo ""
113 |
--------------------------------------------------------------------------------
/scripts/framework.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script builds DocC for a and certain .
5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default.
6 | # You can pass in a list of if you want to customize the build.
7 |
8 | # Important:
9 | # This script doesn't work on packages, only on .xcproj projects that generate a framework.
10 |
11 | # Usage:
12 | # framework.sh [ default:iOS macOS tvOS watchOS xrOS]
13 | # e.g. `bash scripts/framework.sh MyTarget iOS macOS`
14 |
15 | # Exit immediately if a command exits with a non-zero status
16 | set -e
17 |
18 | # Verify that all required arguments are provided
19 | if [ $# -eq 0 ]; then
20 | echo "Error: This script requires exactly one argument"
21 | echo "Usage: $0 "
22 | exit 1
23 | fi
24 |
25 | # Define argument variables
26 | TARGET=$1
27 |
28 | # Remove TARGET from arguments list
29 | shift
30 |
31 | # Define platforms variable
32 | if [ $# -eq 0 ]; then
33 | set -- iOS macOS tvOS watchOS xrOS
34 | fi
35 | PLATFORMS=$@
36 |
37 | # Define local variables
38 | BUILD_FOLDER=.build
39 | BUILD_FOLDER_ARCHIVES=.build/framework_archives
40 | BUILD_FILE=$BUILD_FOLDER/$TARGET.xcframework
41 | BUILD_ZIP=$BUILD_FOLDER/$TARGET.zip
42 |
43 | # Start script
44 | echo ""
45 | echo "Building $TARGET XCFramework for [$PLATFORMS]..."
46 | echo ""
47 |
48 | # Delete old builds
49 | echo "Cleaning old builds..."
50 | rm -rf $BUILD_ZIP
51 | rm -rf $BUILD_FILE
52 | rm -rf $BUILD_FOLDER_ARCHIVES
53 |
54 |
55 | # Generate XCArchive files for all platforms
56 | echo "Generating XCArchives..."
57 |
58 | # Initialize the xcframework command
59 | XCFRAMEWORK_CMD="xcodebuild -create-xcframework"
60 |
61 | # Build iOS archives and append to the xcframework command
62 | if [[ " ${PLATFORMS[@]} " =~ " iOS " ]]; then
63 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=iOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-iOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
64 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=iOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-iOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
65 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-iOS.xcarchive/Products/Library/Frameworks/$TARGET.framework"
66 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-iOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework"
67 | fi
68 |
69 | # Build iOS archive and append to the xcframework command
70 | if [[ " ${PLATFORMS[@]} " =~ " macOS " ]]; then
71 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=macOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-macOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
72 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-macOS.xcarchive/Products/Library/Frameworks/$TARGET.framework"
73 | fi
74 |
75 | # Build tvOS archives and append to the xcframework command
76 | if [[ " ${PLATFORMS[@]} " =~ " tvOS " ]]; then
77 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=tvOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
78 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=tvOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
79 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS.xcarchive/Products/Library/Frameworks/$TARGET.framework"
80 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework"
81 | fi
82 |
83 | # Build watchOS archives and append to the xcframework command
84 | if [[ " ${PLATFORMS[@]} " =~ " watchOS " ]]; then
85 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=watchOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
86 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=watchOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
87 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS.xcarchive/Products/Library/Frameworks/$TARGET.framework"
88 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework"
89 | fi
90 |
91 | # Build xrOS archives and append to the xcframework command
92 | if [[ " ${PLATFORMS[@]} " =~ " xrOS " ]]; then
93 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=xrOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
94 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=xrOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
95 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS.xcarchive/Products/Library/Frameworks/$TARGET.framework"
96 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework"
97 | fi
98 |
99 | # Genererate XCFramework
100 | echo "Generating XCFramework..."
101 | XCFRAMEWORK_CMD+=" -output $BUILD_FILE"
102 | eval "$XCFRAMEWORK_CMD"
103 |
104 | # Genererate iOS XCFramework zip
105 | echo "Generating XCFramework zip..."
106 | zip -r $BUILD_ZIP $BUILD_FILE
107 | echo ""
108 | echo "***** CHECKSUM *****"
109 | swift package compute-checksum $BUILD_ZIP
110 | echo "********************"
111 | echo ""
112 |
113 | # Complete successfully
114 | echo ""
115 | echo "$TARGET XCFramework created successfully!"
116 | echo ""
117 |
--------------------------------------------------------------------------------
/scripts/git_default_branch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script echos the default git branch name.
5 |
6 | # Usage:
7 | # git_default_branch.sh
8 | # e.g. `bash scripts/git_default_branch.sh`
9 |
10 | BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')
11 | echo $BRANCH
12 |
--------------------------------------------------------------------------------
/scripts/package_docc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script builds DocC documentation for `Package.swift`.
5 | # This script targets iOS by default, but you can pass in custom .
6 |
7 | # Usage:
8 | # package_docc.sh [ default:iOS]
9 | # e.g. `bash scripts/package_docc.sh iOS macOS`
10 |
11 | # Exit immediately if a command exits with non-zero status
12 | set -e
13 |
14 | # Use the script folder to refer to other scripts.
15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
16 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh"
17 | SCRIPT_DOCC="$FOLDER/docc.sh"
18 |
19 | # Define platforms variable
20 | if [ $# -eq 0 ]; then
21 | set -- iOS
22 | fi
23 | PLATFORMS=$@
24 |
25 | # Get package name
26 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; }
27 |
28 | # Build package documentation
29 | bash $SCRIPT_DOCC $PACKAGE_NAME $PLATFORMS || { echo "DocC script failed"; exit 1; }
30 |
--------------------------------------------------------------------------------
/scripts/package_framework.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script generates an XCFramework for `Package.swift`.
5 | # This script targets iOS by default, but you can pass in custom .
6 |
7 | # Usage:
8 | # package_framework.sh [ default:iOS]
9 | # e.g. `bash scripts/package_framework.sh iOS macOS`
10 |
11 | # Exit immediately if a command exits with non-zero status
12 | set -e
13 |
14 | # Use the script folder to refer to other scripts.
15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
16 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh"
17 | SCRIPT_FRAMEWORK="$FOLDER/framework.sh"
18 |
19 | # Define platforms variable
20 | if [ $# -eq 0 ]; then
21 | set -- iOS
22 | fi
23 | PLATFORMS=$@
24 |
25 | # Get package name
26 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; }
27 |
28 | # Build package framework
29 | bash $SCRIPT_FRAMEWORK $PACKAGE_NAME $PLATFORMS
30 |
--------------------------------------------------------------------------------
/scripts/package_name.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script finds the main target name in `Package.swift`.
5 |
6 | # Usage:
7 | # package_name.sh
8 | # e.g. `bash scripts/package_name.sh`
9 |
10 | # Exit immediately if a command exits with non-zero status
11 | set -e
12 |
13 | # Check that a Package.swift file exists
14 | if [ ! -f "Package.swift" ]; then
15 | echo "Error: Package.swift not found in current directory"
16 | exit 1
17 | fi
18 |
19 | # Using grep and sed to extract the package name
20 | # 1. grep finds the line containing "name:"
21 | # 2. sed extracts the text between quotes
22 | package_name=$(grep -m 1 'name:.*"' Package.swift | sed -n 's/.*name:[[:space:]]*"\([^"]*\)".*/\1/p')
23 |
24 | if [ -z "$package_name" ]; then
25 | echo "Error: Could not find package name in Package.swift"
26 | exit 1
27 | else
28 | echo "$package_name"
29 | fi
30 |
--------------------------------------------------------------------------------
/scripts/package_version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script creates a new version for `Package.swift`.
5 | # You can pass in a to validate any non-main branch.
6 |
7 | # Usage:
8 | # package_version.sh
9 | # e.g. `bash scripts/package_version.sh master`
10 |
11 | # Exit immediately if a command exits with non-zero status
12 | set -e
13 |
14 | # Use the script folder to refer to other scripts.
15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
16 | SCRIPT_BRANCH_NAME="$FOLDER/git_default_branch.sh"
17 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh"
18 | SCRIPT_VERSION="$FOLDER/version.sh"
19 |
20 | # Get branch name
21 | DEFAULT_BRANCH=$("$SCRIPT_BRANCH_NAME") || { echo "Failed to get branch name"; exit 1; }
22 | BRANCH_NAME=${1:-$DEFAULT_BRANCH}
23 |
24 | # Get package name
25 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; }
26 |
27 | # Build package version
28 | bash $SCRIPT_VERSION $PACKAGE_NAME $BRANCH_NAME
29 |
--------------------------------------------------------------------------------
/scripts/sync_from.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script syncs Swift Package Scripts from a .
5 | # This script will overwrite the existing "scripts" folder.
6 | # Only pass in the full path to a Swift Package Scripts root.
7 |
8 | # Usage:
9 | # package_name.sh
10 | # e.g. `bash sync_from.sh ../SwiftPackageScripts`
11 |
12 | # Define argument variables
13 | SOURCE=$1
14 |
15 | # Define variables
16 | FOLDER="scripts/"
17 | SOURCE_FOLDER="$SOURCE/$FOLDER"
18 |
19 | # Start script
20 | echo ""
21 | echo "Syncing scripts from $SOURCE_FOLDER..."
22 | echo ""
23 |
24 | # Remove existing folder
25 | rm -rf $FOLDER
26 |
27 | # Copy folder
28 | cp -r "$SOURCE_FOLDER/" "$FOLDER/"
29 |
30 | # Complete successfully
31 | echo ""
32 | echo "Script syncing from $SOURCE_FOLDER completed successfully!"
33 | echo ""
34 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script tests a for all provided .
5 |
6 | # Usage:
7 | # test.sh [ default:iOS macOS tvOS watchOS xrOS]
8 | # e.g. `bash scripts/test.sh MyTarget iOS macOS`
9 |
10 | # Exit immediately if a command exits with a non-zero status
11 | set -e
12 |
13 | # Verify that all required arguments are provided
14 | if [ $# -eq 0 ]; then
15 | echo "Error: This script requires at least one argument"
16 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]"
17 | echo "For instance: $0 MyTarget iOS macOS"
18 | exit 1
19 | fi
20 |
21 | # Define argument variables
22 | TARGET=$1
23 |
24 | # Remove TARGET from arguments list
25 | shift
26 |
27 | # Define platforms variable
28 | if [ $# -eq 0 ]; then
29 | set -- iOS macOS tvOS watchOS xrOS
30 | fi
31 | PLATFORMS=$@
32 |
33 | # Start script
34 | echo ""
35 | echo "Testing $TARGET for [$PLATFORMS]..."
36 | echo ""
37 |
38 | # A function that gets the latest simulator for a certain OS.
39 | get_latest_simulator() {
40 | local PLATFORM=$1
41 | local SIMULATOR_TYPE
42 |
43 | case $PLATFORM in
44 | "iOS")
45 | SIMULATOR_TYPE="iPhone"
46 | ;;
47 | "tvOS")
48 | SIMULATOR_TYPE="Apple TV"
49 | ;;
50 | "watchOS")
51 | SIMULATOR_TYPE="Apple Watch"
52 | ;;
53 | "xrOS")
54 | SIMULATOR_TYPE="Apple Vision"
55 | ;;
56 | *)
57 | echo "Error: Unsupported platform for simulator '$PLATFORM'"
58 | return 1
59 | ;;
60 | esac
61 |
62 | # Get the latest simulator for the platform
63 | xcrun simctl list devices available | grep "$SIMULATOR_TYPE" | tail -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/'
64 | }
65 |
66 | # A function that tests $TARGET for a specific platform
67 | test_platform() {
68 |
69 | # Define a local $PLATFORM variable
70 | local PLATFORM="${1//_/ }"
71 |
72 | # Define the destination, based on the $PLATFORM
73 | case $PLATFORM in
74 | "iOS"|"tvOS"|"watchOS"|"xrOS")
75 | local SIMULATOR_UDID=$(get_latest_simulator "$PLATFORM")
76 | if [ -z "$SIMULATOR_UDID" ]; then
77 | echo "Error: No simulator found for $PLATFORM"
78 | return 1
79 | fi
80 | DESTINATION="id=$SIMULATOR_UDID"
81 | ;;
82 | "macOS")
83 | DESTINATION="platform=macOS"
84 | ;;
85 | *)
86 | echo "Error: Unsupported platform '$PLATFORM'"
87 | return 1
88 | ;;
89 | esac
90 |
91 | # Test $TARGET for the $DESTINATION
92 | echo "Testing $TARGET for $PLATFORM..."
93 | xcodebuild test -scheme $TARGET -derivedDataPath .build -destination "$DESTINATION" -enableCodeCoverage YES
94 | local TEST_RESULT=$?
95 |
96 | if [[ $TEST_RESULT -ne 0 ]]; then
97 | return $TEST_RESULT
98 | fi
99 |
100 | # Complete successfully
101 | echo "Successfully tested $TARGET for $PLATFORM"
102 | return 0
103 | }
104 |
105 | # Loop through all platforms and call the test function
106 | for PLATFORM in $PLATFORMS; do
107 | if ! test_platform "$PLATFORM"; then
108 | exit 1
109 | fi
110 | done
111 |
112 | # Complete successfully
113 | echo ""
114 | echo "Testing $TARGET completed successfully!"
115 | echo ""
116 |
--------------------------------------------------------------------------------
/scripts/version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script creates a new version for the provided and .
5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default.
6 | # You can pass in a list of if you want to customize the build.
7 |
8 | # Usage:
9 | # version.sh [ default:iOS macOS tvOS watchOS xrOS]"
10 | # e.g. `scripts/version.sh MyTarget master iOS macOS`
11 |
12 | # This script will:
13 | # * Call version_validate_git.sh to validate the git repo.
14 | # * Call version_validate_target to run tests, swiftlint, etc.
15 | # * Call version_bump.sh if all validation steps above passed.
16 |
17 | # Exit immediately if a command exits with a non-zero status
18 | set -e
19 |
20 | # Verify that all required arguments are provided
21 | if [ $# -lt 2 ]; then
22 | echo "Error: This script requires at least two arguments"
23 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]"
24 | echo "For instance: $0 MyTarget master iOS macOS"
25 | exit 1
26 | fi
27 |
28 | # Define argument variables
29 | TARGET=$1
30 | BRANCH=${2:-main}
31 |
32 | # Remove TARGET and BRANCH from arguments list
33 | shift
34 | shift
35 |
36 | # Read platform arguments or use default value
37 | if [ $# -eq 0 ]; then
38 | set -- iOS macOS tvOS watchOS xrOS
39 | fi
40 |
41 | # Use the script folder to refer to other scripts.
42 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
43 | SCRIPT_VALIDATE_GIT="$FOLDER/version_validate_git.sh"
44 | SCRIPT_VALIDATE_TARGET="$FOLDER/version_validate_target.sh"
45 | SCRIPT_VERSION_BUMP="$FOLDER/version_bump.sh"
46 |
47 | # A function that run a certain script and checks for errors
48 | run_script() {
49 | local script="$1"
50 | shift # Remove the first argument (the script path)
51 |
52 | if [ ! -f "$script" ]; then
53 | echo "Error: Script not found: $script"
54 | exit 1
55 | fi
56 |
57 | chmod +x "$script"
58 | if ! "$script" "$@"; then
59 | echo "Error: Script $script failed"
60 | exit 1
61 | fi
62 | }
63 |
64 | # Start script
65 | echo ""
66 | echo "Creating a new version for $TARGET on the $BRANCH branch..."
67 | echo ""
68 |
69 | # Validate git and project
70 | echo "Validating..."
71 | run_script "$SCRIPT_VALIDATE_GIT" "$BRANCH"
72 | run_script "$SCRIPT_VALIDATE_TARGET" "$TARGET"
73 |
74 | # Bump version
75 | echo "Bumping version..."
76 | run_script "$SCRIPT_VERSION_BUMP"
77 |
78 | # Complete successfully
79 | echo ""
80 | echo "Version created successfully!"
81 | echo ""
82 |
--------------------------------------------------------------------------------
/scripts/version_bump.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script bumps the project version number.
5 | # You can append --no-semver to disable semantic version validation.
6 |
7 | # Usage:
8 | # version_bump.sh [--no-semver]
9 | # e.g. `bash scripts/version_bump.sh`
10 | # e.g. `bash scripts/version_bump.sh --no-semver`
11 |
12 | # Exit immediately if a command exits with a non-zero status
13 | set -e
14 |
15 | # Use the script folder to refer to other scripts.
16 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
17 | SCRIPT_VERSION_NUMBER="$FOLDER/version_number.sh"
18 |
19 |
20 | # Parse --no-semver argument
21 | VALIDATE_SEMVER=true
22 | for arg in "$@"; do
23 | case $arg in
24 | --no-semver)
25 | VALIDATE_SEMVER=false
26 | shift # Remove --no-semver from processing
27 | ;;
28 | esac
29 | done
30 |
31 | # Start script
32 | echo ""
33 | echo "Bumping version number..."
34 | echo ""
35 |
36 | # Get the latest version
37 | VERSION=$($SCRIPT_VERSION_NUMBER)
38 | if [ $? -ne 0 ]; then
39 | echo "Failed to get the latest version"
40 | exit 1
41 | fi
42 |
43 | # Print the current version
44 | echo "The current version is: $VERSION"
45 |
46 | # Function to validate semver format, including optional -rc. suffix
47 | validate_semver() {
48 | if [ "$VALIDATE_SEMVER" = false ]; then
49 | return 0
50 | fi
51 |
52 | if [[ $1 =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
53 | return 0
54 | else
55 | return 1
56 | fi
57 | }
58 |
59 | # Prompt user for new version
60 | while true; do
61 | read -p "Enter the new version number: " NEW_VERSION
62 |
63 | # Validate the version number to ensure that it's a semver version
64 | if validate_semver "$NEW_VERSION"; then
65 | break
66 | else
67 | echo "Invalid version format. Please use semver format (e.g., 1.2.3, v1.2.3, 1.2.3-rc.1, etc.)."
68 | exit 1
69 | fi
70 | done
71 |
72 | # Push the new tag
73 | git push -u origin HEAD
74 | git tag $NEW_VERSION
75 | git push --tags
76 |
77 | # Complete successfully
78 | echo ""
79 | echo "Version tag pushed successfully!"
80 | echo ""
81 |
--------------------------------------------------------------------------------
/scripts/version_number.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script returns the latest project version.
5 |
6 | # Usage:
7 | # version_number.sh
8 | # e.g. `bash scripts/version_number.sh`
9 |
10 | # Exit immediately if a command exits with a non-zero status
11 | set -e
12 |
13 | # Check if the current directory is a Git repository
14 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
15 | echo "Error: Not a Git repository"
16 | exit 1
17 | fi
18 |
19 | # Fetch all tags
20 | git fetch --tags > /dev/null 2>&1
21 |
22 | # Get the latest semver tag
23 | latest_version=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
24 |
25 | # Check if we found a version tag
26 | if [ -z "$latest_version" ]; then
27 | echo "Error: No semver tags found in this repository" >&2
28 | exit 1
29 | fi
30 |
31 | # Print the latest version
32 | echo "$latest_version"
33 |
--------------------------------------------------------------------------------
/scripts/version_validate_git.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script validates the Git repository for release.
5 | # You can pass in a to validate any non-main branch.
6 |
7 | # Usage:
8 | # version_validate_git.sh "
9 | # e.g. `bash scripts/version_validate_git.sh master`
10 |
11 | # This script will:
12 | # * Validate that the script is run within a git repository.
13 | # * Validate that the git repository doesn't have any uncommitted changes.
14 | # * Validate that the current git branch matches the provided one.
15 |
16 | # Exit immediately if a command exits with a non-zero status
17 | set -e
18 |
19 | # Verify that all required arguments are provided
20 | if [ $# -eq 0 ]; then
21 | echo "Error: This script requires exactly one argument"
22 | echo "Usage: $0 "
23 | exit 1
24 | fi
25 |
26 | # Create local argument variables.
27 | BRANCH=$1
28 |
29 | # Start script
30 | echo ""
31 | echo "Validating git repository..."
32 | echo ""
33 |
34 | # Check if the current directory is a Git repository
35 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
36 | echo "Error: Not a Git repository"
37 | exit 1
38 | fi
39 |
40 | # Check for uncommitted changes
41 | if [ -n "$(git status --porcelain)" ]; then
42 | echo "Error: Git repository is dirty. There are uncommitted changes."
43 | exit 1
44 | fi
45 |
46 | # Verify that we're on the correct branch
47 | current_branch=$(git rev-parse --abbrev-ref HEAD)
48 | if [ "$current_branch" != "$BRANCH" ]; then
49 | echo "Error: Not on the specified branch. Current branch is $current_branch, expected $1."
50 | exit 1
51 | fi
52 |
53 | # The Git repository validation succeeded.
54 | echo ""
55 | echo "Git repository validated successfully!"
56 | echo ""
57 |
--------------------------------------------------------------------------------
/scripts/version_validate_target.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Documentation:
4 | # This script validates a for release.
5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default.
6 | # You can pass in a list of if you want to customize the build.
7 |
8 | # Usage:
9 | # version_validate_target.sh [ default:iOS macOS tvOS watchOS xrOS]"
10 | # e.g. `bash scripts/version_validate_target.sh iOS macOS`
11 |
12 | # This script will:
13 | # * Validate that swiftlint passes.
14 | # * Validate that all unit tests passes for all .
15 |
16 | # Exit immediately if a command exits with a non-zero status
17 | set -e
18 |
19 | # Verify that all requires at least one argument"
20 | if [ $# -eq 0 ]; then
21 | echo "Error: This script requires at least one argument"
22 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]"
23 | exit 1
24 | fi
25 |
26 | # Create local argument variables.
27 | TARGET=$1
28 |
29 | # Remove TARGET from arguments list
30 | shift
31 |
32 | # Define platforms variable
33 | if [ $# -eq 0 ]; then
34 | set -- iOS macOS tvOS watchOS xrOS
35 | fi
36 | PLATFORMS=$@
37 |
38 | # Use the script folder to refer to other scripts.
39 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
40 | SCRIPT_TEST="$FOLDER/test.sh"
41 |
42 | # A function that run a certain script and checks for errors
43 | run_script() {
44 | local script="$1"
45 | shift # Remove the first argument (script path) from the argument list
46 |
47 | if [ ! -f "$script" ]; then
48 | echo "Error: Script not found: $script"
49 | exit 1
50 | fi
51 |
52 | chmod +x "$script"
53 | if ! "$script" "$@"; then
54 | echo "Error: Script $script failed"
55 | exit 1
56 | fi
57 | }
58 |
59 | # Start script
60 | echo ""
61 | echo "Validating project..."
62 | echo ""
63 |
64 | # Run SwiftLint
65 | echo "Running SwiftLint"
66 | if ! swiftlint --strict; then
67 | echo "Error: SwiftLint failed"
68 | exit 1
69 | fi
70 |
71 | # Run unit tests
72 | echo "Testing..."
73 | run_script "$SCRIPT_TEST" "$TARGET" "$PLATFORMS"
74 |
75 | # Complete successfully
76 | echo ""
77 | echo "Project successfully validated!"
78 | echo ""
79 |
--------------------------------------------------------------------------------