Skip to content

Commit d37704d

Browse files
authored
Feather: support catalyst targets (#588)
1 parent a91e49c commit d37704d

6 files changed

Lines changed: 91 additions & 46 deletions

File tree

Feather.xcodeproj/project.pbxproj

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,6 @@
390390
GENERATE_INFOPLIST_FILE = YES;
391391
INFOPLIST_FILE = Feather/Resources/Info.plist;
392392
INFOPLIST_KEY_CFBundleDisplayName = Feather;
393-
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO;
394393
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Samara M";
395394
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
396395
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
@@ -418,15 +417,15 @@
418417
SDKROOT = auto;
419418
STRIP_INSTALLED_PRODUCT = NO;
420419
STRIP_PNG_TEXT = NO;
421-
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
422-
SUPPORTS_MACCATALYST = NO;
423-
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
420+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
421+
SUPPORTS_MACCATALYST = YES;
422+
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
424423
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
425424
SWIFT_EMIT_LOC_STRINGS = YES;
426425
SWIFT_OBJC_BRIDGING_HEADER = "Feather/Supporting Files/Feather-Bridging-Header.h";
427426
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
428427
SWIFT_VERSION = 5.0;
429-
TARGETED_DEVICE_FAMILY = "1,2,3";
428+
TARGETED_DEVICE_FAMILY = "1,2";
430429
TVOS_DEPLOYMENT_TARGET = 16.0;
431430
XROS_DEPLOYMENT_TARGET = 2.2;
432431
};
@@ -454,7 +453,6 @@
454453
GENERATE_INFOPLIST_FILE = YES;
455454
INFOPLIST_FILE = Feather/Resources/Info.plist;
456455
INFOPLIST_KEY_CFBundleDisplayName = Feather;
457-
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO;
458456
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Samara M";
459457
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
460458
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
@@ -482,14 +480,14 @@
482480
SDKROOT = auto;
483481
STRIPFLAGS = "-rSTx";
484482
STRIP_PNG_TEXT = NO;
485-
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
486-
SUPPORTS_MACCATALYST = NO;
487-
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
483+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
484+
SUPPORTS_MACCATALYST = YES;
485+
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
488486
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
489487
SWIFT_EMIT_LOC_STRINGS = YES;
490488
SWIFT_OBJC_BRIDGING_HEADER = "Feather/Supporting Files/Feather-Bridging-Header.h";
491489
SWIFT_VERSION = 5.0;
492-
TARGETED_DEVICE_FAMILY = "1,2,3";
490+
TARGETED_DEVICE_FAMILY = "1,2";
493491
TVOS_DEPLOYMENT_TARGET = 16.0;
494492
XROS_DEPLOYMENT_TARGET = 2.2;
495493
};

Feather/Backend/Observable/DownloadManager.swift

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class Download: Identifiable, @unchecked Sendable {
1818

1919
var overallProgress: Double {
2020
onlyArchiving
21-
? unpackageProgress
22-
: (0.3 * unpackageProgress) + (0.7 * progress)
21+
? unpackageProgress
22+
: (0.3 * unpackageProgress) + (0.7 * progress)
2323
}
2424

2525
var task: URLSessionDownloadTask?
@@ -29,7 +29,7 @@ class Download: Identifiable, @unchecked Sendable {
2929
let url: URL
3030
let fileName: String
3131
let onlyArchiving: Bool
32-
32+
3333
init(
3434
id: String,
3535
url: URL,
@@ -52,7 +52,8 @@ class DownloadManager: NSObject, ObservableObject {
5252
}
5353

5454
private var _session: URLSession!
55-
55+
56+
#if !targetEnvironment(macCatalyst)
5657
private func _updateBackgroundAudioState() {
5758
if #unavailable(iOS 26.0){
5859
if !downloads.isEmpty {
@@ -62,13 +63,14 @@ class DownloadManager: NSObject, ObservableObject {
6263
}
6364
}
6465
}
65-
66+
#endif
67+
6668
override init() {
6769
super.init()
6870
let configuration = URLSessionConfiguration.default
6971
_session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
7072
}
71-
73+
7274
func startDownload(
7375
from url: URL,
7476
id: String = UUID().uuidString
@@ -77,19 +79,23 @@ class DownloadManager: NSObject, ObservableObject {
7779
resumeDownload(existingDownload)
7880
return existingDownload
7981
}
80-
82+
8183
let download = Download(id: id, url: url)
82-
84+
8385
let task = _session.downloadTask(with: url)
8486
download.task = task
8587
task.resume()
86-
88+
8789
downloads.append(download)
90+
91+
#if !targetEnvironment(macCatalyst)
8892
if #available(iOS 26.0, *) {
8993
BackgroundTaskManager.shared.startTask(for: id, filename: url.lastPathComponent)
9094
} else {
9195
_updateBackgroundAudioState()
9296
}
97+
#endif
98+
9399
return download
94100
}
95101

@@ -99,36 +105,50 @@ class DownloadManager: NSObject, ObservableObject {
99105
) -> Download {
100106
let download = Download(id: id, url: url, onlyArchiving: true)
101107
downloads.append(download)
108+
109+
#if !targetEnvironment(macCatalyst)
102110
_updateBackgroundAudioState()
111+
#endif
112+
103113
return download
104114
}
105-
115+
106116
func resumeDownload(_ download: Download) {
107117
if let resumeData = download.resumeData {
108118
let task = _session.downloadTask(withResumeData: resumeData)
109119
download.task = task
110120
task.resume()
121+
122+
#if !targetEnvironment(macCatalyst)
111123
_updateBackgroundAudioState()
124+
#endif
112125
} else if let url = download.task?.originalRequest?.url {
113126
let task = _session.downloadTask(with: url)
114127
download.task = task
115128
task.resume()
129+
130+
#if !targetEnvironment(macCatalyst)
116131
_updateBackgroundAudioState()
132+
#endif
117133
}
118134
}
119-
135+
120136
func cancelDownload(_ download: Download) {
121137
download.task?.cancel()
122-
138+
123139
if let index = downloads.firstIndex(where: { $0.id == download.id }) {
124140
downloads.remove(at: index)
141+
142+
#if !targetEnvironment(macCatalyst)
125143
_updateBackgroundAudioState()
144+
126145
if #available(iOS 26.0, *) {
127146
BackgroundTaskManager.shared.stopTask(for: download.id, success: false)
128147
}
148+
#endif
129149
}
130150
}
131-
151+
132152
func isManualDownload(_ string: String) -> Bool {
133153
return string.contains("FeatherManualDownload")
134154
}
@@ -158,10 +178,14 @@ extension DownloadManager: URLSessionDownloadDelegate {
158178
DispatchQueue.main.async {
159179
if let index = DownloadManager.shared.getDownloadIndex(by: dl.id) {
160180
DownloadManager.shared.downloads.remove(at: index)
181+
182+
#if !targetEnvironment(macCatalyst)
161183
if #available(iOS 26.0, *) {
162184
BackgroundTaskManager.shared.updateProgress(for: dl.id, progress: 1.0)
163185
}
186+
164187
self._updateBackgroundAudioState()
188+
#endif
165189
}
166190
}
167191
}
@@ -188,22 +212,25 @@ extension DownloadManager: URLSessionDownloadDelegate {
188212
print("Error handling downloaded file: \(error.localizedDescription)")
189213
}
190214
}
191-
215+
192216
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
193217
guard let download = getDownloadTask(by: downloadTask) else { return }
194-
218+
195219
DispatchQueue.main.async {
196220
download.progress = totalBytesExpectedToWrite > 0
197-
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
198-
: 0
221+
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
222+
: 0
199223
download.bytesDownloaded = totalBytesWritten
200224
download.totalBytes = totalBytesExpectedToWrite
225+
226+
#if !targetEnvironment(macCatalyst)
201227
if #available(iOS 26.0, *) {
202228
BackgroundTaskManager.shared.updateProgress(for: download.id, progress: download.overallProgress)
203229
}
230+
#endif
204231
}
205232
}
206-
233+
207234
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
208235
guard
209236
let _ = error,

Feather/Utilities/BackgroundAudioManager.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
// Created by Nagata Asami on 12/10/25.
66
//
77

8+
#if !targetEnvironment(macCatalyst)
9+
810
import AVFoundation
911

1012
class BackgroundAudioManager {
1113
static let shared = BackgroundAudioManager()
1214
private let _engine = AVAudioEngine()
13-
15+
1416

1517
private init() {}
16-
18+
1719
func start() {
1820
do {
1921
let session = AVAudioSession.sharedInstance()
20-
22+
2123
try session.setCategory(.playback, options: [.mixWithOthers])
2224
try session.setActive(true)
2325
let silence = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in
@@ -27,17 +29,19 @@ class BackgroundAudioManager {
2729
}
2830
return noErr
2931
}
30-
32+
3133
_engine.attach(silence)
3234
_engine.connect(silence, to: _engine.mainMixerNode, format: nil)
3335
try _engine.start()
3436
} catch {
3537
print("failed to start engine:", error)
3638
}
3739
}
38-
40+
3941
func stop() {
4042
_engine.stop()
4143
try? AVAudioSession.sharedInstance().setActive(false)
4244
}
4345
}
46+
47+
#endif

Feather/Utilities/BackgroundTaskManager.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,29 @@
55
// Created by Nagata Asami on 4/1/26.
66
//
77

8+
#if !targetEnvironment(macCatalyst)
9+
810
import Foundation
911
import BackgroundTasks
1012
import CryptoKit
1113

1214
@available(iOS 26.0, *)
1315
class BackgroundTaskManager: ObservableObject {
1416
static let shared = BackgroundTaskManager()
15-
17+
1618
private let baseId = "\(Bundle.main.bundleIdentifier!).userTask"
17-
19+
1820
private var activeTasks: [String: BGContinuedProcessingTask] = [:]
1921
private var registeredTasks: Set<String> = []
20-
22+
2123
func startTask(for downloadId: String, filename: String) {
2224
let taskIdentifier = "\(baseId).\(downloadId.md5)"
23-
25+
2426
if !registeredTasks.contains(taskIdentifier) {
2527
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
2628
guard let task = task as? BGContinuedProcessingTask else { return }
2729
self.activeTasks[task.identifier] = task
28-
30+
2931
task.expirationHandler = {
3032
if let download = DownloadManager.shared.getDownload(by: downloadId) {
3133
DownloadManager.shared.cancelDownload(download)
@@ -35,7 +37,7 @@ class BackgroundTaskManager: ObservableObject {
3537
}
3638
self.registeredTasks.insert(taskIdentifier)
3739
}
38-
40+
3941
let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, title: filename, subtitle: .localized("Downloading"))
4042
request.strategy = .queue
4143
do {
@@ -44,25 +46,25 @@ class BackgroundTaskManager: ObservableObject {
4446
print(error)
4547
}
4648
}
47-
49+
4850
func updateProgress(for downloadId: String, progress: Double) {
4951
let taskIdentifier = "\(baseId).\(downloadId.md5)"
50-
52+
5153
guard let task = activeTasks[taskIdentifier] else { return }
5254
task.progress.totalUnitCount = 100
5355
task.progress.completedUnitCount = Int64(progress * 100)
54-
56+
5557
task.updateTitle(task.title, subtitle: "\(Int(progress * 100))%")
56-
58+
5759
if task.progress.completedUnitCount == task.progress.totalUnitCount {
5860
stopTask(for: downloadId, success: true)
5961
}
6062
}
61-
63+
6264
func stopTask(for downloadId: String, success: Bool) {
6365
let taskIdentifier = "\(baseId).\(downloadId.md5)"
6466
guard let task = activeTasks[taskIdentifier] else { return }
65-
67+
6668
task.setTaskCompleted(success: success)
6769
activeTasks.removeValue(forKey: taskIdentifier)
6870
}
@@ -73,3 +75,5 @@ extension String {
7375
Insecure.MD5.hash(data: Data(self.utf8)).map { String(format: "%02hhx", $0) }.joined()
7476
}
7577
}
78+
79+
#endif

Feather/Utilities/Handlers/AppFileHandler.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,12 @@ final class AppFileHandler: NSObject, @unchecked Sendable {
6969
if let download = download {
7070
DispatchQueue.main.async {
7171
download.unpackageProgress = progress
72+
73+
#if !targetEnvironment(macCatalyst)
7274
if #available(iOS 26.0, *) {
7375
BackgroundTaskManager.shared.updateProgress(for: download.id, progress: download.overallProgress)
7476
}
77+
#endif
7578
}
7679
}
7780
}

0 commit comments

Comments
 (0)