diff --git a/.resources/Recordings/AudioPlaybackWidget.gif b/.resources/Recordings/AudioPlaybackWidget.gif
new file mode 100644
index 0000000..1f46e7c
Binary files /dev/null and b/.resources/Recordings/AudioPlaybackWidget.gif differ
diff --git a/.resources/Screenshots/AudioPlaybackWidget.png b/.resources/Screenshots/AudioPlaybackWidget.png
new file mode 100644
index 0000000..f4bf5d7
Binary files /dev/null and b/.resources/Screenshots/AudioPlaybackWidget.png differ
diff --git a/App/App.swift b/App/App.swift
index 14da16f..07d6e69 100644
--- a/App/App.swift
+++ b/App/App.swift
@@ -50,5 +50,6 @@ extension WidgetExamplesApp {
renderer.scale = 10
let filename = URL.documentsDirectory.appending(path: "SharedViewWidget.png")
try? renderer.uiImage?.pngData()?.write(to: filename)
+ print(filename)
}
}
diff --git a/App/Navigation/AppScreen.swift b/App/Navigation/AppScreen.swift
index ce17b10..def2f31 100644
--- a/App/Navigation/AppScreen.swift
+++ b/App/Navigation/AppScreen.swift
@@ -24,6 +24,7 @@ import SwiftUI
enum AppScreen: Hashable, CaseIterable {
case appGroup
+ case audioPlayback
case coreData
case dynamicIntent
case liveActivity
@@ -44,6 +45,8 @@ extension AppScreen {
switch self {
case .appGroup:
"App Group"
+ case .audioPlayback:
+ "Audio Playback"
case .coreData:
"Core Data"
case .dynamicIntent:
@@ -70,6 +73,8 @@ extension AppScreen {
switch self {
case .appGroup:
AppGroupWidgetView()
+ case .audioPlayback:
+ AudioPlaybackWidgetView()
case .coreData:
CoreDataWidgetView()
case .dynamicIntent:
diff --git a/App/Screens/AudioPlayback/AudioPlaybackWidgetView.swift b/App/Screens/AudioPlayback/AudioPlaybackWidgetView.swift
new file mode 100644
index 0000000..3d6f8b4
--- /dev/null
+++ b/App/Screens/AudioPlayback/AudioPlaybackWidgetView.swift
@@ -0,0 +1,80 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftUI
+import WidgetKit
+
+struct AudioPlaybackWidgetView: View {
+ @AppStorage(UserDefaultKey.isAudioPlaying, store: .appGroup)
+ private var isPlaying = false
+
+ var body: some View {
+ List {
+ Section {
+ contentView
+ } header: {
+ headerView
+ }
+ }
+ .onChange(of: isPlaying) {
+ reloadWidgetTimelines()
+ }
+ }
+}
+
+// MARK: - Content
+
+extension AudioPlaybackWidgetView {
+ private var headerView: some View {
+ Text("Audio Playback")
+ }
+
+ @ViewBuilder
+ private var contentView: some View {
+ stateView
+ buttonsView
+ }
+
+ private var stateView: some View {
+ Text(isPlaying ? "Playing..." : "Paused")
+ }
+
+ private var buttonsView: some View {
+ AudioPlaybackWidgetButtonsView(isPlaying: isPlaying)
+ }
+}
+
+// MARK: - Helpers
+
+extension AudioPlaybackWidgetView {
+ private func reloadWidgetTimelines() {
+ WidgetCenter.shared.reloadTimelines(ofKind: WidgetType.audioPlayback.kind)
+ }
+}
+
+// MARK: - Preview
+
+#Preview {
+ NavigationStack {
+ AudioPlaybackWidgetView()
+ }
+}
diff --git a/README.md b/README.md
index 5e24118..1af0b24 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
# Widget Examples
@@ -25,14 +17,15 @@ A demo project showing different types of Widgets created with SwiftUI and Widge
Table of Contents
- 1. [Gallery](#gallery)
- 2. [Unofficial gallery](#unofficial)
+ 1. [Basic widgets](#basic)
+ 2. [Intent widgets](#intent)
+ 2. [Unofficial widgets](#unofficial)
3. [Installation](#installation)
4. [License](#license)
-## Gallery
+## Basic widgets
+
+## Intent widgets
+
+The following widgets use Intents. Please refer to the [documentation](https://developer.apple.com/documentation/appintents/appintent) for a more detailed explanation.
+
+
-## Unofficial gallery
+## Unofficial widgets
-Please be aware that the folowing widgets use private API. This means they not necessarily have to pass Apple Review and you use them at your own risk.
+The folowing widgets use private API. Please bear in mind that they don't necessarily have to pass the Apple review process and you use them at your own risk.
diff --git a/Shared/Components/AudioPlayer.swift b/Shared/Components/AudioPlayer.swift
new file mode 100644
index 0000000..a79e73d
--- /dev/null
+++ b/Shared/Components/AudioPlayer.swift
@@ -0,0 +1,103 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import AVFoundation
+import OSLog
+
+final class AudioPlayer {
+ private enum State {
+ case playing
+ case paused
+ case stopped
+ case disabled
+ }
+
+ static let shared = AudioPlayer()
+
+ private var logger: Logger = .audioPlayer
+ private var player: AVAudioPlayer?
+ private var state: State
+
+ private init() {
+ do {
+ try AVAudioSession.sharedInstance().setCategory(.playback)
+ try AVAudioSession.sharedInstance().setActive(true)
+ state = .stopped
+ } catch {
+ state = .disabled
+ logger.error("AVAudioSession init error: \(error)")
+ }
+ }
+
+ var isEnabled: Bool {
+ state != .disabled
+ }
+
+ var isPlaying: Bool {
+ state == .playing
+ }
+}
+
+// MARK: - Actions
+
+extension AudioPlayer {
+ func play(sound: Sound) {
+ guard isEnabled else {
+ return
+ }
+ player = player ?? createPlayer(for: sound)
+ if let player {
+ state = .playing
+ player.play()
+ }
+ }
+
+ func pause() {
+ guard isEnabled, let player else {
+ return
+ }
+ state = .paused
+ player.pause()
+ }
+
+ func stop() {
+ guard isEnabled else {
+ return
+ }
+ state = .stopped
+ player?.stop()
+ player = nil
+ }
+}
+
+// MARK: - Helpers
+
+extension AudioPlayer {
+ private func createPlayer(for sound: Sound) -> AVAudioPlayer? {
+ do {
+ return try AVAudioPlayer(contentsOf: sound.url)
+ } catch {
+ logger.error("Error creating player: \(error)")
+ return nil
+ }
+ }
+}
diff --git a/Shared/Components/UserDefaultKey.swift b/Shared/Components/UserDefaultKey.swift
index 28ffb15..bb53a5f 100644
--- a/Shared/Components/UserDefaultKey.swift
+++ b/Shared/Components/UserDefaultKey.swift
@@ -26,6 +26,8 @@ enum UserDefaultKey {
static var luckyNumber: String { #function }
static var persons: String { #function }
+ static var isAudioPlaying: String { #function }
+
static func eventCounter(id: Int) -> String {
"eventCounter-\(id)"
}
diff --git a/Shared/Logger.swift b/Shared/Logger.swift
index b43fdbd..cde83d3 100644
--- a/Shared/Logger.swift
+++ b/Shared/Logger.swift
@@ -28,6 +28,7 @@ extension Logger {
static let app = Logger(subsystem: subsystem, category: "App")
static let widgets = Logger(subsystem: subsystem, category: "Widgets")
+ static let audioPlayer = Logger(subsystem: subsystem, category: "AudioPlayer")
static let coreData = Logger(subsystem: subsystem, category: "CoreData")
static let fileManager = Logger(subsystem: subsystem, category: "FileManager")
}
diff --git a/Shared/Models/AudioPlayback/Sound.swift b/Shared/Models/AudioPlayback/Sound.swift
new file mode 100644
index 0000000..5751b4a
--- /dev/null
+++ b/Shared/Models/AudioPlayback/Sound.swift
@@ -0,0 +1,34 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+
+enum Sound: String {
+ /// https://pixabay.com/music/beats-sad-soul-chasing-a-feeling-185750
+ case main
+}
+
+extension Sound {
+ var url: URL {
+ Bundle.main.url(forResource: rawValue, withExtension: "mp3")!
+ }
+}
diff --git a/Shared/WidgetType.swift b/Shared/WidgetType.swift
index 34fdcd1..d416fd1 100644
--- a/Shared/WidgetType.swift
+++ b/Shared/WidgetType.swift
@@ -25,6 +25,7 @@ import Foundation
enum WidgetType: String {
case analogClock
case appGroup
+ case audioPlayback
case coreData
case countdown
case deepLink
diff --git a/Widget Examples.xcodeproj/project.pbxproj b/Widget Examples.xcodeproj/project.pbxproj
index 4748951..d6a67e1 100644
--- a/Widget Examples.xcodeproj/project.pbxproj
+++ b/Widget Examples.xcodeproj/project.pbxproj
@@ -22,6 +22,13 @@
E521D0542A63012000F6749D /* DynamicIntentWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E521D0532A63012000F6749D /* DynamicIntentWidget+EntryView.swift */; };
E521D0562A63012900F6749D /* DynamicIntentWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E521D0552A63012900F6749D /* DynamicIntentWidget+Provider.swift */; };
E521D0582A63013C00F6749D /* DynamicIntentWidget+Intent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E521D0572A63013C00F6749D /* DynamicIntentWidget+Intent.swift */; };
+ E53E0F282BF0FEB900681CA8 /* AudioPlaybackWidget+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */; };
+ E53E0F2B2BF104A300681CA8 /* AudioPlaybackWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53E0F2A2BF104A300681CA8 /* AudioPlaybackWidgetView.swift */; };
+ E53E0F2E2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */; };
+ E53E0F2F2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */; };
+ E53E0F342BF10C8100681CA8 /* WidgetHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5757DB12A6AD4D700764349 /* WidgetHeaderView.swift */; };
+ E53E0F372BF10CF600681CA8 /* AudioPlaybackWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31612BF0F27B00280186 /* AudioPlaybackWidget+EntryView.swift */; };
+ E53E0F382BF10CF900681CA8 /* AudioPlaybackWidget+Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31602BF0F27B00280186 /* AudioPlaybackWidget+Entry.swift */; };
E542135A253879B700CCC9C3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5421359253879B700CCC9C3 /* App.swift */; };
E542135C253879B700CCC9C3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E542135B253879B700CCC9C3 /* ContentView.swift */; };
E54214122538AFEC00CCC9C3 /* SharedViewWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E542140A2538AF7800CCC9C3 /* SharedViewWidgetEntry.swift */; };
@@ -127,6 +134,16 @@
E58BB8F52A62E32100DA8303 /* IntentWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58BB8F42A62E32100DA8303 /* IntentWidget+EntryView.swift */; };
E58BB8F72A62E32700DA8303 /* IntentWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58BB8F62A62E32700DA8303 /* IntentWidget+Provider.swift */; };
E58BB8F92A62E32C00DA8303 /* IntentWidget+Intent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58BB8F82A62E32C00DA8303 /* IntentWidget+Intent.swift */; };
+ E5AE31662BF0F27B00280186 /* AudioPlaybackWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE315F2BF0F27B00280186 /* AudioPlaybackWidget.swift */; };
+ E5AE31692BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */; };
+ E5AE316A2BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31632BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift */; };
+ E5AE316B2BF0F27B00280186 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = E5AE31642BF0F27B00280186 /* README.md */; };
+ E5AE316E2BF0F4F700280186 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE316D2BF0F4F700280186 /* Sound.swift */; };
+ E5AE316F2BF0F4F700280186 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE316D2BF0F4F700280186 /* Sound.swift */; };
+ E5AE31712BF0F67200280186 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31702BF0F67200280186 /* AudioPlayer.swift */; };
+ E5AE31722BF0F67200280186 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31702BF0F67200280186 /* AudioPlayer.swift */; };
+ E5AE31772BF0FCB400280186 /* main.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E5AE31762BF0FCB400280186 /* main.mp3 */; };
+ E5AE31782BF0FCB400280186 /* main.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E5AE31762BF0FCB400280186 /* main.mp3 */; };
E5B345222A67E37D00702B86 /* LiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B345212A67E37D00702B86 /* LiveActivityWidget.swift */; };
E5B345242A67E3E400702B86 /* DeliveryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B345232A67E3E400702B86 /* DeliveryAttributes.swift */; };
E5B345252A67E3E400702B86 /* DeliveryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B345232A67E3E400702B86 /* DeliveryAttributes.swift */; };
@@ -208,6 +225,8 @@
E521D0532A63012000F6749D /* DynamicIntentWidget+EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DynamicIntentWidget+EntryView.swift"; sourceTree = ""; };
E521D0552A63012900F6749D /* DynamicIntentWidget+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DynamicIntentWidget+Provider.swift"; sourceTree = ""; };
E521D0572A63013C00F6749D /* DynamicIntentWidget+Intent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DynamicIntentWidget+Intent.swift"; sourceTree = ""; };
+ E53E0F2A2BF104A300681CA8 /* AudioPlaybackWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackWidgetView.swift; sourceTree = ""; };
+ E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackWidgetButtonsView.swift; sourceTree = ""; };
E5421356253879B700CCC9C3 /* Widget Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Widget Examples.app"; sourceTree = BUILT_PRODUCTS_DIR; };
E5421359253879B700CCC9C3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
E542135B253879B700CCC9C3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
@@ -280,6 +299,15 @@
E58BB8F62A62E32700DA8303 /* IntentWidget+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentWidget+Provider.swift"; sourceTree = ""; };
E58BB8F82A62E32C00DA8303 /* IntentWidget+Intent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentWidget+Intent.swift"; sourceTree = ""; };
E591C5712551666A002044C8 /* IntentWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentWidget.swift; sourceTree = ""; };
+ E5AE315F2BF0F27B00280186 /* AudioPlaybackWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlaybackWidget.swift; sourceTree = ""; };
+ E5AE31602BF0F27B00280186 /* AudioPlaybackWidget+Entry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+Entry.swift"; sourceTree = ""; };
+ E5AE31612BF0F27B00280186 /* AudioPlaybackWidget+EntryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+EntryView.swift"; sourceTree = ""; };
+ E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+Intents.swift"; sourceTree = ""; };
+ E5AE31632BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+Provider.swift"; sourceTree = ""; };
+ E5AE31642BF0F27B00280186 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
+ E5AE316D2BF0F4F700280186 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; };
+ E5AE31702BF0F67200280186 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; };
+ E5AE31762BF0FCB400280186 /* main.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = main.mp3; sourceTree = ""; };
E5B345212A67E37D00702B86 /* LiveActivityWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidget.swift; sourceTree = ""; };
E5B345232A67E3E400702B86 /* DeliveryAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryAttributes.swift; sourceTree = ""; };
E5B345262A67E56900702B86 /* Delivery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delivery.swift; sourceTree = ""; };
@@ -320,6 +348,7 @@
isa = PBXGroup;
children = (
E58246CF2A66C646008180CC /* AppGroup */,
+ E53E0F292BF1049200681CA8 /* AudioPlayback */,
E58246D02A66C64C008180CC /* CoreData */,
E58246CC2A66A65B008180CC /* DynamicIntent */,
E5B345342A67F88E00702B86 /* LiveActivity */,
@@ -352,6 +381,23 @@
path = AnalogClockWidget;
sourceTree = "";
};
+ E53E0F292BF1049200681CA8 /* AudioPlayback */ = {
+ isa = PBXGroup;
+ children = (
+ E53E0F2A2BF104A300681CA8 /* AudioPlaybackWidgetView.swift */,
+ );
+ path = AudioPlayback;
+ sourceTree = "";
+ };
+ E53E0F2C2BF1069300681CA8 /* Shared */ = {
+ isa = PBXGroup;
+ children = (
+ E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */,
+ E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */,
+ );
+ path = Shared;
+ sourceTree = "";
+ };
E542134D253879B700CCC9C3 = {
isa = PBXGroup;
children = (
@@ -402,6 +448,7 @@
E542137425387A0300CCC9C3 /* WidgetBundle.swift */,
E52FE12F2BD552CE00141AC0 /* AnalogClockWidget */,
E54213C7253891CB00CCC9C3 /* AppGroupWidget */,
+ E5AE31652BF0F27B00280186 /* AudioPlaybackWidget */,
E54214182538B57400CCC9C3 /* CoreDataWidget */,
E542139A2538806F00CCC9C3 /* CountdownWidget */,
E54213F42538A9A300CCC9C3 /* DeepLinkWidget */,
@@ -550,6 +597,7 @@
E55498362551D1F40066870B /* Models */ = {
isa = PBXGroup;
children = (
+ E5AE316C2BF0F4E900280186 /* AudioPlayback */,
E56181CD2A69D856002E0CB2 /* CoreData */,
E56181D02A69D8A3002E0CB2 /* DynamicIntent */,
E56181CF2A69D87C002E0CB2 /* LiveActivity */,
@@ -630,6 +678,7 @@
isa = PBXGroup;
children = (
E5757DD32A6AF61F00764349 /* Assets.xcassets */,
+ E5AE31762BF0FCB400280186 /* main.mp3 */,
);
path = Resources;
sourceTree = "";
@@ -637,6 +686,7 @@
E58246742A6435B0008180CC /* Components */ = {
isa = PBXGroup;
children = (
+ E5AE31702BF0F67200280186 /* AudioPlayer.swift */,
E5619C562A6986D900F94132 /* DeepLink.swift */,
E58246722A635A4B008180CC /* Loadable.swift */,
E542141D2538B68200CCC9C3 /* PersistenceController.swift */,
@@ -750,6 +800,27 @@
path = IntentWidget;
sourceTree = "";
};
+ E5AE31652BF0F27B00280186 /* AudioPlaybackWidget */ = {
+ isa = PBXGroup;
+ children = (
+ E53E0F2C2BF1069300681CA8 /* Shared */,
+ E5AE315F2BF0F27B00280186 /* AudioPlaybackWidget.swift */,
+ E5AE31602BF0F27B00280186 /* AudioPlaybackWidget+Entry.swift */,
+ E5AE31612BF0F27B00280186 /* AudioPlaybackWidget+EntryView.swift */,
+ E5AE31632BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift */,
+ E5AE31642BF0F27B00280186 /* README.md */,
+ );
+ path = AudioPlaybackWidget;
+ sourceTree = "";
+ };
+ E5AE316C2BF0F4E900280186 /* AudioPlayback */ = {
+ isa = PBXGroup;
+ children = (
+ E5AE316D2BF0F4F700280186 /* Sound.swift */,
+ );
+ path = AudioPlayback;
+ sourceTree = "";
+ };
E5B345202A67E36A00702B86 /* LiveActivityWidget */ = {
isa = PBXGroup;
children = (
@@ -882,7 +953,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ E5AE316B2BF0F27B00280186 /* README.md in Resources */,
E5757DD42A6AF61F00764349 /* Assets.xcassets in Resources */,
+ E5AE31782BF0FCB400280186 /* main.mp3 in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -891,6 +964,7 @@
buildActionMask = 2147483647;
files = (
E5757DD12A6AF2B500764349 /* Assets.xcassets in Resources */,
+ E5AE31772BF0FCB400280186 /* main.mp3 in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -903,6 +977,7 @@
files = (
E5DCA1942A683E6000C59A50 /* LiveActivityView+Title.swift in Sources */,
E58BB8D02A62D89500DA8303 /* EnvironmentWidget+Provider.swift in Sources */,
+ E5AE31722BF0F67200280186 /* AudioPlayer.swift in Sources */,
E582467B2A643D8E008180CC /* NetworkWidget+Entry.swift in Sources */,
E58BB8ED2A62D90C00DA8303 /* CountryWebRepository.swift in Sources */,
E58BB8F72A62E32700DA8303 /* IntentWidget+Provider.swift in Sources */,
@@ -926,14 +1001,18 @@
E57C3E112A674183002CF144 /* LockScreenWidget+Entry.swift in Sources */,
E580A5D72A6E9A270007BA39 /* Logger.swift in Sources */,
E58BB8F12A62D92700DA8303 /* PersistenceController.swift in Sources */,
+ E5AE31662BF0F27B00280186 /* AudioPlaybackWidget.swift in Sources */,
E58BB8D32A62D89500DA8303 /* EnvironmentWidget+Entry.swift in Sources */,
E542EEDA2BD565CD00964BEB /* AnalogClockWidget+Provider.swift in Sources */,
E58BB8C62A62D88500DA8303 /* CountdownWidget+Provider.swift in Sources */,
E58BB8BF2A62D86D00DA8303 /* AppGroupWidget+EntryView.swift in Sources */,
E542EEDB2BD565CD00964BEB /* AnalogClockWidget.swift in Sources */,
+ E53E0F382BF10CF900681CA8 /* AudioPlaybackWidget+Entry.swift in Sources */,
E5B345282A67E56900702B86 /* Delivery.swift in Sources */,
+ E53E0F372BF10CF600681CA8 /* AudioPlaybackWidget+EntryView.swift in Sources */,
E5DCA18E2A683DFC00C59A50 /* LiveActivityView+Icon.swift in Sources */,
E58246912A64560D008180CC /* InteractiveWidget+EntryView.swift in Sources */,
+ E5AE316A2BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift in Sources */,
E58BB8E92A62D8FB00DA8303 /* WidgetType.swift in Sources */,
E58BB8C12A62D87200DA8303 /* CoreDataWidget+Entry.swift in Sources */,
E5B345222A67E37D00702B86 /* LiveActivityWidget.swift in Sources */,
@@ -958,6 +1037,7 @@
E58BB8E12A62D8B100DA8303 /* DynamicIntentWidget.swift in Sources */,
E5DCA1972A683E8C00C59A50 /* LiveActivityView+Status.swift in Sources */,
E58BB8CB2A62D88A00DA8303 /* DeepLinkWidget+Entry.swift in Sources */,
+ E5AE31692BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift in Sources */,
E58BB8CA2A62D88A00DA8303 /* DeepLinkWidget.swift in Sources */,
E58246B52A65C943008180CC /* Product.swift in Sources */,
E58BB8E72A62D8F500DA8303 /* Shared.swift in Sources */,
@@ -973,6 +1053,7 @@
E58246BA2A65CBA3008180CC /* SwiftDataWidget+EntryView.swift in Sources */,
E57C3DF02A66D33B002CF144 /* Document.swift in Sources */,
E58246B82A65CB9C008180CC /* SwiftDataWidget+Entry.swift in Sources */,
+ E5AE316F2BF0F4F700280186 /* Sound.swift in Sources */,
E58BB8BD2A62D86D00DA8303 /* AppGroupWidget+Provider.swift in Sources */,
E5DCA19A2A6840BD00C59A50 /* DeliveryState.swift in Sources */,
E58BB8BB2A62D86500DA8303 /* WidgetBundle.swift in Sources */,
@@ -992,6 +1073,7 @@
E58BB8EE2A62D90C00DA8303 /* WebRepository.swift in Sources */,
E58BB8F02A62D92400DA8303 /* DataModel.xcdatamodeld in Sources */,
E58BB8DE2A62D8AA00DA8303 /* LockScreenWidget.swift in Sources */,
+ E53E0F2F2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */,
E58BB8C42A62D88500DA8303 /* CountdownWidget+EntryView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1002,9 +1084,11 @@
files = (
E58246C42A6692FE008180CC /* AppScreen.swift in Sources */,
E542135C253879B700CCC9C3 /* ContentView.swift in Sources */,
+ E5AE31712BF0F67200280186 /* AudioPlayer.swift in Sources */,
E5638AEA265D859D00E73B05 /* WidgetType.swift in Sources */,
E517B33A2A61B36F009072A3 /* SharedViewWidgetView.swift in Sources */,
E5DCA1992A6840BD00C59A50 /* DeliveryState.swift in Sources */,
+ E53E0F342BF10C8100681CA8 /* WidgetHeaderView.swift in Sources */,
E57C3DEF2A66D33B002CF144 /* Document.swift in Sources */,
E5DCA18D2A683DFC00C59A50 /* LiveActivityView+Icon.swift in Sources */,
E517B3342A61AF28009072A3 /* CoreDataWidgetView.swift in Sources */,
@@ -1012,6 +1096,7 @@
E56D4CF02A69474E00742DFB /* FileManager+Extensions.swift in Sources */,
E57C3DE92A66C8C2002CF144 /* Array+Extensions.swift in Sources */,
E517B33C2A61E022009072A3 /* UserDefaultKey.swift in Sources */,
+ E53E0F282BF0FEB900681CA8 /* AudioPlaybackWidget+Intents.swift in Sources */,
E517B3412A61E0C6009072A3 /* Shared.swift in Sources */,
E58246CE2A66A66F008180CC /* DynamicIntentWidgetView+Person.swift in Sources */,
E55498382551D2160066870B /* Person.swift in Sources */,
@@ -1020,9 +1105,11 @@
E58246BF2A65CBBB008180CC /* SwiftDataWidgetView.swift in Sources */,
E54214262538B77500CCC9C3 /* DataModel.xcdatamodeld in Sources */,
E58246752A6435C1008180CC /* Loadable.swift in Sources */,
+ E53E0F2E2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */,
E54214152538AFF000CCC9C3 /* SharedViewWidgetEntryView.swift in Sources */,
E5DCA1962A683E8C00C59A50 /* LiveActivityView+Status.swift in Sources */,
E517B3382A61B246009072A3 /* DynamicIntentWidgetView.swift in Sources */,
+ E5AE316E2BF0F4F700280186 /* Sound.swift in Sources */,
E57C3DEC2A66CA11002CF144 /* UserDefaults+Extensions.swift in Sources */,
E54214122538AFEC00CCC9C3 /* SharedViewWidgetEntry.swift in Sources */,
E5B345382A67F9C900702B86 /* LiveActivityWidgetView+Model.swift in Sources */,
@@ -1043,6 +1130,7 @@
E58246C62A669323008180CC /* AppSidebarList.swift in Sources */,
E5B345242A67E3E400702B86 /* DeliveryAttributes.swift in Sources */,
E58246B62A65CB8D008180CC /* Product.swift in Sources */,
+ E53E0F2B2BF104A300681CA8 /* AudioPlaybackWidgetView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Widget Examples.xcodeproj/xcshareddata/xcschemes/Widgets.xcscheme b/Widget Examples.xcodeproj/xcshareddata/xcschemes/Widgets.xcscheme
new file mode 100644
index 0000000..8636aab
--- /dev/null
+++ b/Widget Examples.xcodeproj/xcshareddata/xcschemes/Widgets.xcscheme
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Entry.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Entry.swift
new file mode 100644
index 0000000..0a683c3
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Entry.swift
@@ -0,0 +1,38 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import WidgetKit
+
+extension AudioPlaybackWidget {
+ struct Entry: TimelineEntry {
+ var date: Date = .now
+ var isPlaying: Bool
+ }
+}
+
+// MARK: - Data
+
+extension AudioPlaybackWidget.Entry {
+ static var placeholder: Self {
+ .init(isPlaying: false)
+ }
+}
diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+EntryView.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+EntryView.swift
new file mode 100644
index 0000000..59236e4
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+EntryView.swift
@@ -0,0 +1,38 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftUI
+
+extension AudioPlaybackWidget {
+ struct EntryView: View {
+ let entry: Entry
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ WidgetHeaderView(title: "Audio Playback")
+ Spacer()
+ AudioPlaybackWidgetButtonsView(isPlaying: entry.isPlaying)
+ }
+ .containerBackground(.clear, for: .widget)
+ }
+ }
+}
diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Provider.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Provider.swift
new file mode 100644
index 0000000..dc6b915
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Provider.swift
@@ -0,0 +1,41 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import WidgetKit
+
+extension AudioPlaybackWidget {
+ struct Provider: TimelineProvider {
+ func placeholder(in context: Context) -> Entry {
+ .placeholder
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
+ completion(.placeholder)
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
+ let isPlaying = UserDefaults.appGroup.bool(forKey: UserDefaultKey.isAudioPlaying)
+ let entry = Entry(isPlaying: isPlaying)
+ completion(.init(entries: [entry], policy: .never))
+ }
+ }
+}
diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget.swift
new file mode 100644
index 0000000..9cb6ea7
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget.swift
@@ -0,0 +1,46 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftUI
+import WidgetKit
+
+struct AudioPlaybackWidget: Widget {
+ private let kind: String = WidgetType.audioPlayback.kind
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: Provider()) {
+ EntryView(entry: $0)
+ }
+ .configurationDisplayName("Audio Playback Widget")
+ .description("Play music in the background directly from the Widget.")
+ .supportedFamilies([.systemSmall])
+ }
+}
+
+// MARK: - Preview
+
+#Preview(as: .systemSmall) {
+ AudioPlaybackWidget()
+} timeline: {
+ AudioPlaybackWidget.Entry(isPlaying: false)
+ AudioPlaybackWidget.Entry(isPlaying: true)
+}
diff --git a/Widgets/AudioPlaybackWidget/README.md b/Widgets/AudioPlaybackWidget/README.md
new file mode 100644
index 0000000..958bf8e
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/README.md
@@ -0,0 +1,7 @@
+# Audio Playback Widget
+
+Play music in the background directly from the Widget.
+
+## Preview
+
+![Audio Playback Widget](../../.resources/Recordings/AudioPlaybackWidget.gif)
diff --git a/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidget+Intents.swift b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidget+Intents.swift
new file mode 100644
index 0000000..d34152e
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidget+Intents.swift
@@ -0,0 +1,67 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import AppIntents
+import AVFoundation
+import WidgetKit
+
+// MARK: - PlayIntent
+
+struct AudioPlaybackWidgetPlayIntent: AudioPlaybackIntent {
+ static var title: LocalizedStringResource = "Play Music"
+
+ private let sound: Sound
+
+ init(sound: Sound) {
+ self.sound = sound
+ }
+
+ init() {
+ self.init(sound: .main)
+ }
+
+ func perform() async throws -> some IntentResult {
+ AudioPlayer.shared.play(sound: sound)
+ UserDefaults.appGroup.set(
+ AudioPlayer.shared.isPlaying,
+ forKey: UserDefaultKey.isAudioPlaying
+ )
+ return .result()
+ }
+}
+
+// MARK: - PauseIntent
+
+struct AudioPlaybackWidgetPauseIntent: AudioPlaybackIntent {
+ static var title: LocalizedStringResource = "Pause Music"
+
+ init() {}
+
+ func perform() async throws -> some IntentResult {
+ AudioPlayer.shared.pause()
+ UserDefaults.appGroup.set(
+ AudioPlayer.shared.isPlaying,
+ forKey: UserDefaultKey.isAudioPlaying
+ )
+ return .result()
+ }
+}
diff --git a/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidgetButtonsView.swift b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidgetButtonsView.swift
new file mode 100644
index 0000000..721342d
--- /dev/null
+++ b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidgetButtonsView.swift
@@ -0,0 +1,55 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2024-Present Paweł Wiszenko
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import SwiftUI
+
+struct AudioPlaybackWidgetButtonsView: View {
+ let isPlaying: Bool
+
+ var body: some View {
+ if isPlaying {
+ pauseButton
+ } else {
+ playButton
+ }
+ }
+}
+
+// MARK: - Content
+
+extension AudioPlaybackWidgetButtonsView {
+ private var playButton: some View {
+ Button(intent: AudioPlaybackWidgetPlayIntent(sound: .main)) {
+ Image(systemName: "play")
+ .padding(2)
+ }
+ .buttonStyle(.bordered)
+ }
+
+ private var pauseButton: some View {
+ Button(intent: AudioPlaybackWidgetPauseIntent()) {
+ Image(systemName: "pause")
+ .padding(2)
+ }
+ .buttonStyle(.bordered)
+ }
+}
diff --git a/Widgets/Resources/main.mp3 b/Widgets/Resources/main.mp3
new file mode 100644
index 0000000..99b6ce6
Binary files /dev/null and b/Widgets/Resources/main.mp3 differ
diff --git a/Widgets/WidgetBundle.swift b/Widgets/WidgetBundle.swift
index 7b2ec2d..6f3ccec 100644
--- a/Widgets/WidgetBundle.swift
+++ b/Widgets/WidgetBundle.swift
@@ -35,6 +35,7 @@ struct WidgetBundle1: WidgetBundle {
var body: some Widget {
AnalogClockWidget()
AppGroupWidget()
+ AudioPlaybackWidget()
CoreDataWidget()
CountdownWidget()
DeepLinkWidget()