Skip to content

Commit 8e7ab7e

Browse files
committed
add visionos support
1 parent 8cc3275 commit 8e7ab7e

File tree

7 files changed

+81
-32
lines changed

7 files changed

+81
-32
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55

66
---
77

8+
## [5.1.1](https://github.com/fulldecent/FDWaveformView/releases/tag/5.1.1)
9+
10+
Released on 2025-12-06.
11+
12+
### Added
13+
14+
- visionOS 1.0+ support with proper screen scale handling
15+
16+
---
17+
818
## [5.1.0](https://github.com/fulldecent/FDWaveformView/releases/tag/5.1.0)
919

1020
Released on 2025-12-06.

Example/Example.xcodeproj/project.pbxproj

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,9 @@
292292
CODE_SIGN_STYLE = Automatic;
293293
CURRENT_PROJECT_VERSION = 1;
294294
DEVELOPMENT_TEAM = 8Q693ZG5RN;
295+
ENABLE_APP_SANDBOX = YES;
296+
ENABLE_HARDENED_RUNTIME = YES;
297+
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
295298
ENABLE_PREVIEWS = YES;
296299
GENERATE_INFOPLIST_FILE = YES;
297300
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -307,9 +310,14 @@
307310
MARKETING_VERSION = 1.0;
308311
PRODUCT_BUNDLE_IDENTIFIER = net.phor.Example;
309312
PRODUCT_NAME = "$(TARGET_NAME)";
313+
REGISTER_APP_GROUPS = YES;
314+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
315+
SUPPORTS_MACCATALYST = NO;
316+
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
317+
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
310318
SWIFT_EMIT_LOC_STRINGS = YES;
311319
SWIFT_VERSION = 5.0;
312-
TARGETED_DEVICE_FAMILY = "1,2";
320+
TARGETED_DEVICE_FAMILY = "1,2,7";
313321
};
314322
name = Debug;
315323
};
@@ -321,6 +329,9 @@
321329
CODE_SIGN_STYLE = Automatic;
322330
CURRENT_PROJECT_VERSION = 1;
323331
DEVELOPMENT_TEAM = 8Q693ZG5RN;
332+
ENABLE_APP_SANDBOX = YES;
333+
ENABLE_HARDENED_RUNTIME = YES;
334+
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
324335
ENABLE_PREVIEWS = YES;
325336
GENERATE_INFOPLIST_FILE = YES;
326337
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -336,9 +347,14 @@
336347
MARKETING_VERSION = 1.0;
337348
PRODUCT_BUNDLE_IDENTIFIER = net.phor.Example;
338349
PRODUCT_NAME = "$(TARGET_NAME)";
350+
REGISTER_APP_GROUPS = YES;
351+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
352+
SUPPORTS_MACCATALYST = NO;
353+
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
354+
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
339355
SWIFT_EMIT_LOC_STRINGS = YES;
340356
SWIFT_VERSION = 5.0;
341-
TARGETED_DEVICE_FAMILY = "1,2";
357+
TARGETED_DEVICE_FAMILY = "1,2,7";
342358
};
343359
name = Release;
344360
};

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import PackageDescription
66
let package = Package(
77
name: "FDWaveformView",
88
platforms: [
9-
.iOS(.v13),
10-
.macOS(.v10_14),
9+
.iOS(.v15),
10+
.visionOS(.v1),
1111
],
1212
products: [
1313
// Products define the executables and libraries a package produces, making them visible to other packages.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ UIView.animate(withDuration: 0.3) {
6565

6666
- Antialiased waveforms draw extra pixels to avoid jagged edges.
6767
- Autolayout-driven size changes trigger re-rendering to prevent pixelation.
68-
- Supports iOS 12+ and Swift 5.
68+
- Supports iOS 15+ and visionOS 1.0+.
6969
- Includes unit tests running on GitHub Actions.
7070

7171
## Installation

Sources/FDWaveformView/FDWaveformAudioDataSource.swift

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,52 @@ import Foundation
99
final class FDWaveformAudioDataSource: FDWaveformDataSource {
1010
private let audioURL: URL
1111
private let asset: AVAsset
12-
private let assetTrack: AVAssetTrack?
13-
private let sampleRate: Int
14-
private let channelCount: Int
15-
private let cachedTotalSamples: Int
12+
private var assetTrack: AVAssetTrack?
13+
private var sampleRate: Int = 44100
14+
private var channelCount: Int = 1
15+
private var cachedTotalSamples: Int = 0
16+
private var isLoaded: Bool = false
1617

1718
init(audioURL: URL) {
1819
self.audioURL = audioURL
1920
self.asset = AVAsset(url: audioURL)
21+
}
2022

21-
// Get the audio track and its properties
22-
let tracks = asset.tracks(withMediaType: .audio)
23-
self.assetTrack = tracks.first
24-
25-
if let track = assetTrack,
26-
let formatDescriptions = track.formatDescriptions as? [CMAudioFormatDescription],
27-
let formatDesc = formatDescriptions.first,
28-
let streamDesc = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc)
29-
{
30-
self.sampleRate = Int(streamDesc.pointee.mSampleRate)
31-
self.channelCount = Int(streamDesc.pointee.mChannelsPerFrame)
32-
33-
// Calculate total samples from duration
34-
let duration = asset.duration
35-
let durationInSeconds = CMTimeGetSeconds(duration)
36-
self.cachedTotalSamples = Int(durationInSeconds * Double(sampleRate))
37-
} else {
38-
self.sampleRate = 44100
39-
self.channelCount = 1
40-
self.cachedTotalSamples = 0
23+
/// Loads audio track properties asynchronously. Called automatically on first access.
24+
private func loadTrackIfNeeded() async {
25+
guard !isLoaded else { return }
26+
isLoaded = true
27+
28+
do {
29+
let tracks = try await asset.loadTracks(withMediaType: .audio)
30+
self.assetTrack = tracks.first
31+
32+
if let track = assetTrack,
33+
let formatDescriptions = try? await track.load(.formatDescriptions)
34+
as? [CMAudioFormatDescription],
35+
let formatDesc = formatDescriptions.first,
36+
let streamDesc = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc)
37+
{
38+
self.sampleRate = Int(streamDesc.pointee.mSampleRate)
39+
self.channelCount = Int(streamDesc.pointee.mChannelsPerFrame)
40+
41+
// Calculate total samples from duration
42+
let duration = try await asset.load(.duration)
43+
let durationInSeconds = CMTimeGetSeconds(duration)
44+
self.cachedTotalSamples = Int(durationInSeconds * Double(sampleRate))
45+
}
46+
} catch {
47+
// Keep default values on error
4148
}
4249
}
4350

4451
func numberOfSamples() async -> Int {
52+
await loadTrackIfNeeded()
4553
return cachedTotalSamples
4654
}
4755

4856
func samples(in range: Range<Int>) async -> [Float] {
57+
await loadTrackIfNeeded()
4958
guard !range.isEmpty, let track = assetTrack else { return [] }
5059

5160
guard let reader = try? AVAssetReader(asset: asset) else { return [] }
@@ -124,6 +133,7 @@ final class FDWaveformAudioDataSource: FDWaveformDataSource {
124133
}
125134

126135
func maximums(from range: Range<Int>, numberOfBins: Int) async -> [Float] {
136+
await loadTrackIfNeeded()
127137
guard !range.isEmpty, numberOfBins > 0, let track = assetTrack else { return [] }
128138

129139
guard let reader = try? AVAssetReader(asset: asset) else { return [] }

Sources/FDWaveformView/FDWaveformRenderOperation.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,23 @@ struct FDWaveformRenderFormat {
1717
/// The scale factor to apply to the rendered image (usually the current screen's scale)
1818
var scale: CGFloat
1919

20+
/// Default scale factor for rendering
21+
private static var defaultScale: CGFloat {
22+
#if os(visionOS)
23+
return 2.0
24+
#else
25+
return UIScreen.main.scale
26+
#endif
27+
}
28+
2029
init(
2130
type: FDWaveformView.WaveformType = .linear,
2231
wavesColor: UIColor = .black,
23-
scale: CGFloat = UIScreen.main.scale
32+
scale: CGFloat? = nil
2433
) {
2534
self.type = type
2635
self.wavesColor = wavesColor
27-
self.scale = scale
36+
self.scale = scale ?? Self.defaultScale
2837
}
2938
}
3039

Sources/FDWaveformView/FDWaveformView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,11 @@ open class FDWaveformView: UIView {
192192

193193
/// Desired scale of image based on window's screen scale
194194
private var desiredImageScale: CGFloat {
195-
return window?.screen.scale ?? UIScreen.main.scale
195+
#if os(visionOS)
196+
return 2.0
197+
#else
198+
return window?.screen.scale ?? UIScreen.main.scale
199+
#endif
196200
}
197201

198202
/// Represents the status of the waveform renderings

0 commit comments

Comments
 (0)