Skip to content

Commit 2b454aa

Browse files
committed
share a curated SwiftUI gallery across iOS, macOS, and visionOS examples
Replaces the per-platform example views with a single `WaveformGalleryView` (in Example/Shared/) that the three example apps wrap. The gallery is a single scrolling page with sections covering every public rendering surface: - Interactive playground (renderer + style + damping + color picker, all driving one preview) - Renderers section (Linear default / .up / .down / Circular / Ring) - Styles section (filled, outlined, gradient, gradient-outlined, striped) - Channel selection (merged, specific(0), specific(1), stereo) - Custom-shape examples (WaveformView trailing closure, bare WaveformShape) LazyVStack at the outer and per-section level — sections only instantiate when scrolled into view. This keeps the number of concurrent `WaveformView` analyses small, which matters because macOS AVFoundation hangs indefinitely when too many `AVAssetReader` instances target the same audio URL at once. Other changes: - iOS `SwiftUIExampleView` reduced to a Gallery / Live picker, with the live-recording UI rebuilt against `WaveformLiveCanvas` directly. - macOS / visionOS `ContentView`s become thin wrappers around the gallery. - Bundle `example_stereo.m4a` in iOS and visionOS targets (previously macOS-only) so the gallery's stereo demos work everywhere. - Carry the iOS app icon into the macOS (with all 1x/2x size variants generated from the 1024 source) and visionOS (front/middle/back layers) targets so the apps share visual identity. - Project file updated: new `Shared/` group, new shared file referenced by all three targets' Sources phase, additional resource entries for the stereo audio.
1 parent 4198994 commit 2b454aa

19 files changed

Lines changed: 523 additions & 240 deletions

File tree

Example/DSWaveformImageExample-VisionOS/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "Icon.png",
45
"idiom" : "vision",
56
"scale" : "2x"
67
}
Loading

Example/DSWaveformImageExample-VisionOS/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "Icon.png",
45
"idiom" : "vision",
56
"scale" : "2x"
67
}
Loading

Example/DSWaveformImageExample-VisionOS/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "Icon.png",
45
"idiom" : "vision",
56
"scale" : "2x"
67
}
Loading

Example/DSWaveformImageExample-VisionOS/ContentView.swift

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,8 @@ import DSWaveformImage
33
import DSWaveformImageViews
44

55
struct ContentView: View {
6-
private static let colors = [UIColor.systemPink, UIColor.systemBlue, UIColor.systemGreen]
7-
private static var randomColor: UIColor {
8-
colors[Int.random(in: 0..<colors.count)]
9-
}
10-
11-
@State private var audioURL: URL = Bundle.main.url(forResource: "example_sound", withExtension: "m4a")!
12-
13-
@State var configuration: Waveform.Configuration = Waveform.Configuration(
14-
style: .gradient([.red, .green])
15-
)
16-
176
var body: some View {
18-
VStack {
19-
Text("SwiftUI example")
20-
.font(.largeTitle.bold())
21-
22-
Button {
23-
configuration = configuration.with(style: .striped(.init(color: Self.randomColor)))
24-
} label: {
25-
Label("switch color randomly", systemImage: "arrow.triangle.2.circlepath")
26-
}
27-
.font(.body.bold())
28-
.padding()
29-
.background(Color(UIColor.systemGray).opacity(0.6))
30-
.cornerRadius(10)
31-
32-
WaveformView(audioURL: audioURL, configuration: configuration, renderer: CircularWaveformRenderer())
33-
}
7+
WaveformGalleryView()
348
}
359
}
3610

Example/DSWaveformImageExample-iOS/SwiftUIExample/SwiftUIExampleView.swift

Lines changed: 38 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,61 @@ import DSWaveformImageViews
33
import SwiftUI
44

55
struct SwiftUIExampleView: View {
6-
private enum ActiveTab: Hashable {
7-
case recorder, shape, overview
6+
private enum Tab: Hashable {
7+
case gallery
8+
case recorder
89
}
910

10-
private static let colors = [UIColor.systemPink, UIColor.systemBlue, UIColor.systemGreen]
11-
private static var randomColor: UIColor { colors.randomElement()! }
12-
13-
private static var audioURLs: [URL?] = [
14-
Bundle.main.url(forResource: "example_sound", withExtension: "m4a"),
15-
Bundle.main.url(forResource: "example_sound_2", withExtension: "m4a")
16-
]
17-
private static func randomURL(_ current: URL?) -> URL? { audioURLs.filter { $0 != current }.randomElement()! }
18-
19-
@StateObject private var audioRecorder: AudioRecorder = AudioRecorder()
20-
21-
@State private var configuration: Waveform.Configuration = Waveform.Configuration(
22-
style: .striped(Waveform.Style.StripeConfig(color: Self.randomColor, width: 3, lineCap: .round)),
23-
verticalScalingFactor: 0.9
24-
)
25-
26-
@State private var liveConfiguration: Waveform.Configuration = Waveform.Configuration(
27-
style: .striped(.init(color: randomColor, width: 3, spacing: 3))
28-
)
29-
30-
@State private var audioURL: URL? = audioURLs.first!
31-
@State private var samples: [Float] = []
32-
@State private var silence: Bool = true
33-
@State private var selection: ActiveTab = .overview
11+
@State private var tab: Tab = .gallery
3412

3513
var body: some View {
36-
VStack {
37-
Text("SwiftUI examples")
38-
.font(.largeTitle.bold())
39-
40-
Picker("Hey", selection: $selection) {
41-
Text("Recorder").tag(ActiveTab.recorder)
42-
Text("Shape").tag(ActiveTab.shape)
43-
Text("Overview").tag(ActiveTab.overview)
14+
VStack(spacing: 0) {
15+
Picker("Section", selection: $tab) {
16+
Label("Gallery", systemImage: "rectangle.grid.2x2").tag(Tab.gallery)
17+
Label("Live", systemImage: "mic.circle.fill").tag(Tab.recorder)
4418
}
4519
.pickerStyle(.segmented)
20+
.labelsHidden()
4621
.padding(.horizontal)
22+
.padding(.top, 12)
4723

48-
switch selection {
49-
case .recorder: recordingExample
50-
case .shape: shape
51-
case .overview: overview
24+
switch tab {
25+
case .gallery: WaveformGalleryView()
26+
case .recorder: LiveRecordingTab()
5227
}
5328
}
54-
.padding(.vertical, 20)
5529
}
30+
}
31+
32+
private struct LiveRecordingTab: View {
33+
@StateObject private var audioRecorder = AudioRecorder()
34+
@State private var silence: Bool = true
35+
@State private var configuration: Waveform.Configuration = Waveform.Configuration(
36+
style: .striped(.init(color: .systemIndigo, width: 3, spacing: 3))
37+
)
38+
39+
var body: some View {
40+
VStack(spacing: 16) {
41+
VStack(alignment: .leading, spacing: 4) {
42+
Text("Live recording")
43+
.font(.title2.weight(.semibold))
44+
Text("WaveformLiveCanvas streams microphone amplitude into a circular renderer.")
45+
.font(.callout)
46+
.foregroundStyle(.secondary)
47+
}
48+
.frame(maxWidth: .infinity, alignment: .leading)
49+
.padding(.horizontal)
50+
.padding(.top, 16)
5651

57-
@ViewBuilder
58-
private var recordingExample: some View {
59-
VStack {
6052
WaveformLiveCanvas(
6153
samples: audioRecorder.samples,
62-
configuration: liveConfiguration,
54+
configuration: configuration,
6355
renderer: CircularWaveformRenderer(kind: .circle),
6456
shouldDrawSilencePadding: silence
6557
)
58+
.padding(.horizontal)
6659

67-
Toggle("draw silence", isOn: $silence)
60+
Toggle("Pad silence", isOn: $silence)
6861
.controlSize(.mini)
6962
.padding(.horizontal)
7063

@@ -75,136 +68,8 @@ struct SwiftUIExampleView: View {
7568
isRecording: $audioRecorder.isRecording
7669
)
7770
.padding(.horizontal)
78-
}
79-
}
80-
81-
@ViewBuilder
82-
private var shape: some View {
83-
VStack {
84-
Text("WaveformView").font(.monospaced(.title.bold())())
8571

86-
HStack {
87-
Button {
88-
configuration = configuration.with(style: .striped(Waveform.Style.StripeConfig(color: Self.randomColor, width: 3, lineCap: .round)))
89-
liveConfiguration = liveConfiguration.with(style: .striped(.init(color: Self.randomColor, width: 3, spacing: 3)))
90-
} label: {
91-
Label("color", systemImage: "dice")
92-
.frame(maxWidth: .infinity)
93-
}
94-
.font(.body.bold())
95-
.padding(8)
96-
.background(Color(.systemGray6))
97-
.cornerRadius(10)
98-
99-
Button {
100-
audioURL = Self.randomURL(audioURL)
101-
print("will draw \(audioURL!)")
102-
} label: {
103-
Label("waveform", systemImage: "dice")
104-
.frame(maxWidth: .infinity)
105-
}
106-
.font(.body.bold())
107-
.padding(8)
108-
.background(Color(.systemGray6))
109-
.cornerRadius(10)
110-
}
111-
.padding(.horizontal)
112-
113-
// the if let is left here intentionally to illustrate how to deal with optional URLs
114-
// as this was asked in an older GitHub issue
115-
if let audioURL {
116-
WaveformView(audioURL: audioURL, configuration: configuration)
117-
118-
WaveformView(
119-
audioURL: audioURL,
120-
configuration: configuration,
121-
renderer: CircularWaveformRenderer(kind: .ring(0.7))
122-
) { shape in
123-
// you may completely override the shape styling this way
124-
shape
125-
.stroke(
126-
LinearGradient(colors: [.red, Color(Self.randomColor)], startPoint: .zero, endPoint: .topTrailing),
127-
style: StrokeStyle(lineWidth: 3, lineCap: .round))
128-
}
129-
130-
Divider()
131-
Text("WaveformShape").font(.monospaced(.title.bold())())
132-
133-
/// **Note:** It's possible, but discouraged to use WaveformShape directly.
134-
/// As Shapes should not do any expensive computations, the analyzing should happen outside,
135-
/// hence making the API a tiny bit clumsy if used directly, since we do require to know the size,
136-
/// even though the Shape of course intrinsically knows its size already.
137-
GeometryReader { geometry in
138-
WaveformShape(samples: samples)
139-
.fill(Color.orange)
140-
.task {
141-
do {
142-
let samplesNeeded = Int(geometry.size.width * configuration.scale)
143-
let samples = try await WaveformAnalyzer().samples(fromAudioAt: audioURL, count: samplesNeeded)
144-
await MainActor.run { self.samples = samples }
145-
} catch {
146-
assertionFailure(error.localizedDescription)
147-
}
148-
}
149-
}
150-
}
151-
}
152-
}
153-
154-
@ViewBuilder
155-
private var overview: some View {
156-
if let audioURL {
157-
HStack {
158-
VStack {
159-
WaveformView(audioURL: audioURL, configuration: .init(style: .filled(.red)))
160-
WaveformView(audioURL: audioURL, configuration: .init(style: .outlined(.blue, 0.5)))
161-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradient([.yellow, .orange])))
162-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradientOutlined([.yellow, .orange], 1)))
163-
WaveformView(audioURL: audioURL, configuration: .init(style: .striped(.init(color: .red, width: 2, spacing: 1))))
164-
165-
// Multi-channel examples
166-
WaveformView(audioURL: audioURL, configuration: .init(style: .filled(.blue)), renderer: LinearWaveformRenderer(channelSelection: .specific(0)))
167-
WaveformView(audioURL: audioURL, configuration: .init(style: .filled(.red)), renderer: LinearWaveformRenderer(channelSelection: .specific(1)))
168-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradient([.blue, .cyan])), renderer: LinearWaveformRenderer.stereo)
169-
170-
WaveformView(audioURL: audioURL, configuration: .init(style: .striped(.init(color: .black)))) { shape in
171-
shape // override the shape styling
172-
.stroke(LinearGradient(colors: [.blue, .pink], startPoint: .bottom, endPoint: .top), lineWidth: 3)
173-
} placeholder: {
174-
ProgressView()
175-
}
176-
}
177-
178-
VStack {
179-
WaveformView(audioURL: audioURL, configuration: .init(style: .filled(.red)), renderer: CircularWaveformRenderer())
180-
WaveformView(audioURL: audioURL, configuration: .init(style: .outlined(.blue, 0.5)), renderer: CircularWaveformRenderer())
181-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradient([.yellow, .orange])), renderer: CircularWaveformRenderer())
182-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradientOutlined([.yellow, .orange], 1)), renderer: CircularWaveformRenderer())
183-
WaveformView(audioURL: audioURL, configuration: .init(style: .striped(.init(color: .red, width: 2, spacing: 2))), renderer: CircularWaveformRenderer())
184-
185-
WaveformView(audioURL: audioURL, configuration: .init(style: .striped(.init(color: .black))), renderer: CircularWaveformRenderer()) { shape in
186-
shape // override the shape styling
187-
.stroke(LinearGradient(colors: [.blue, .pink], startPoint: .bottom, endPoint: .top), lineWidth: 3)
188-
} placeholder: {
189-
ProgressView()
190-
}
191-
}
192-
193-
VStack {
194-
WaveformView(audioURL: audioURL, configuration: .init(style: .filled(.red)), renderer: CircularWaveformRenderer(kind: .ring(0.5)))
195-
WaveformView(audioURL: audioURL, configuration: .init(style: .outlined(.blue, 0.5)), renderer: CircularWaveformRenderer(kind: .ring(0.5)))
196-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradient([.yellow, .orange])), renderer: CircularWaveformRenderer(kind: .ring(0.5)))
197-
WaveformView(audioURL: audioURL, configuration: .init(style: .gradientOutlined([.yellow, .orange], 1)), renderer: CircularWaveformRenderer(kind: .ring(0.5)))
198-
WaveformView(audioURL: audioURL, configuration: .init(style: .striped(.init(color: .red, width: 2, spacing: 2))), renderer: CircularWaveformRenderer(kind: .ring(0.5)))
199-
200-
WaveformView(audioURL: audioURL, configuration: .init(style: .striped(.init(color: .black))), renderer: CircularWaveformRenderer(kind: .ring(0.5))) { shape in
201-
shape // override the shape styling
202-
.stroke(LinearGradient(colors: [.blue, .pink], startPoint: .bottom, endPoint: .top), lineWidth: 3)
203-
} placeholder: {
204-
ProgressView()
205-
}
206-
}
207-
}
72+
Spacer(minLength: 0)
20873
}
20974
}
21075
}
@@ -229,9 +94,7 @@ private class AudioRecorder: NSObject, ObservableObject, RecordingDelegate {
22994

23095
override init() {
23196
audioManager = SCAudioManager()
232-
23397
super.init()
234-
23598
audioManager.prepareAudioRecording()
23699
audioManager.recordingDelegate = self
237100
}
@@ -250,16 +113,13 @@ private class AudioRecorder: NSObject, ObservableObject, RecordingDelegate {
250113
// MARK: - RecordingDelegate
251114

252115
func audioManager(_ manager: SCAudioManager!, didAllowRecording flag: Bool) {}
253-
254116
func audioManager(_ manager: SCAudioManager!, didFinishRecordingSuccessfully flag: Bool) {}
255117

256118
func audioManager(_ manager: SCAudioManager!, didUpdateRecordProgress progress: CGFloat) {
257119
let linear = 1 - pow(10, manager.lastAveragePower() / 20)
258-
259120
// Here we add the same sample 3 times to speed up the animation.
260121
// Usually you'd just add the sample once.
261122
recordingTime = audioManager.currentRecordingTime
262123
samples += [linear, linear, linear]
263124
}
264125
}
265-

Example/DSWaveformImageExample-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,61 @@
11
{
22
"images" : [
33
{
4+
"filename" : "Icon-16.png",
45
"idiom" : "mac",
56
"scale" : "1x",
67
"size" : "16x16"
78
},
89
{
10+
"filename" : "Icon-32.png",
911
"idiom" : "mac",
1012
"scale" : "2x",
1113
"size" : "16x16"
1214
},
1315
{
16+
"filename" : "Icon-32.png",
1417
"idiom" : "mac",
1518
"scale" : "1x",
1619
"size" : "32x32"
1720
},
1821
{
22+
"filename" : "Icon-64.png",
1923
"idiom" : "mac",
2024
"scale" : "2x",
2125
"size" : "32x32"
2226
},
2327
{
28+
"filename" : "Icon-128.png",
2429
"idiom" : "mac",
2530
"scale" : "1x",
2631
"size" : "128x128"
2732
},
2833
{
34+
"filename" : "Icon-256.png",
2935
"idiom" : "mac",
3036
"scale" : "2x",
3137
"size" : "128x128"
3238
},
3339
{
40+
"filename" : "Icon-256.png",
3441
"idiom" : "mac",
3542
"scale" : "1x",
3643
"size" : "256x256"
3744
},
3845
{
46+
"filename" : "Icon-512.png",
3947
"idiom" : "mac",
4048
"scale" : "2x",
4149
"size" : "256x256"
4250
},
4351
{
52+
"filename" : "Icon-512.png",
4453
"idiom" : "mac",
4554
"scale" : "1x",
4655
"size" : "512x512"
4756
},
4857
{
58+
"filename" : "Icon-1024.png",
4959
"idiom" : "mac",
5060
"scale" : "2x",
5161
"size" : "512x512"
308 KB
Loading

0 commit comments

Comments
 (0)