Post

Replies

Boosts

Views

Activity

Reply to Creating Swift Package with binaryTarget that has dependencies
Thanks Ed. To confirm I understand correctly, if you develop a framework project and create an XCFramework to distribute as a closed source SDK, your framework cannot have dependencies on open source code-based packages. Every dependency, if any, must be a closed source XCFramework. It is not possible to create and distribute a Swift Package that is a binary target that has dependencies on other packages that are raw source code. When you do have the requirements to build an XCFramework with dependencies on open and closed source libraries, is the recommendation to use CocoaPods*? Are there plans to support this with Swift Package Manager in the future? *This setup "just works" with CocoaPods. You define a Podfile for your framework project specifying the dependencies with specific version numbers, run pod install, build your XCFramework from the workspace specifying BUILD_LIBRARIES_FOR_DISTRIBUTION=YES and SKIP_INSTALL=NO, create your Podspec specifying the vendored framework and those same dependencies, then add that pod in a new app. When you run pod install for the app, it takes care of downloading and integrating the dependencies appropriately, including open and closed source libraries your framework relies on. Since it can be achieved with CocoaPods, I'm surprised it seemingly can't with SPM.
Oct ’25
Reply to Creating Swift Package with binaryTarget that has dependencies
Thanks Ed! I appreciate the goal of reducing dependencies as much as possible. In this simple example to outline the approach in detail, I chose Kingfisher. Our real SDK is much more complicated, it utilizes a couple dependencies that would be very difficult to build ourselves. For our use case, it is acknowledged that if the app itself adds a dependency that our SDK relies on, it should be the same version number or an otherwise compatible version. Our dependencies are not super popular so that is fairly rare, though it does happen. And our SDK is not widely integrated in apps either, it happens to be quite niche. Note our SDK is already in use distributed via CocoaPods and we haven't had an issue thus far, we are now trying to understand how to distribute it via SPM instead. I'm interested to understand why I see the duplicate class warnings with the example I put together, given the app itself did not add Kingfisher as a dependency, its only dependency is the sample WallpaperKitDist SDK. Perhaps because I didn't configure the framework or package correctly so Kingfisher unintentionally got included in the framework and the app? Which is a good segue to your next comment. If you do want to keep the dependency for your XCFramework library, one thing you can sometimes do is set up a package to vend your XCFramework, and declare a dependency in that package so that consumers of the XCFramework also get a resolved version of the library dependency. The build of your framework would need to be configured so that this dependency doesn't wind up built in to your library binary. This way, the build of the client app provides a resolved version of the dependency. This sounds perfect. Given the steps outlined in my post to create an example framework, what do I need to do differently? Thanks!
Oct ’25
Reply to BGContinuedProcessingTask launchHandler invocation
Thanks Kevin! Moving performCountWork into the launchHandler (instead of before register) fixed it, simply because the call to set the totalUnitCount did not happen because currentBackgroundContinuedProcessingTask is nil at that time. Makes sense why it was indeterminate, it was never given a total! 😄 performCountWork still doesn't have to be called in the launchHandler, as long as I have the data to be able to set the total unit count when the task gets created at any time during the already in-flight operation, that approach works too. If I did that though, that brings up a question, what if the operation completes before the launchHandler is invoked (if I were to use the queue strategy)? A task would start for work that's already completed and thus wouldn't get progress updates and setTaskCompleted wouldn't be invoked. It seems much more straightforward to use the fail strategy and perform the work in the launchHandler (and if it the submit request fails perform the work without a BGContinuedProcessingTask as we do on iOS 18), at least for my simple use case. The only odd thing I'm seeing now is it animates indeterminately until completedUnitCount is set to a new value. In this example, that happens after just 1 second, so the switch from indeterminate to determinate looks janky. I set total to 100 and completed to 0 when the task starts, but it animates indeterminately until completedUnitCount is set to 1. If I set completed to 1 from the beginning, it animates indeterminately until completed is set to 2 (even if it gets set to 1 again). Any ideas to prevent the indeterminate state always show determinate progress?
Sep ’25
Reply to BGContinuedProcessingTask launchHandler invocation
I'm not sure what's going on, but I think this is a bug in your app, probably caused by this: [line with Task.sleep] Unless I've misread something, that means you're trying to update the UI and progress display 100x per second and the entire process completed in 1s The UILabel text and task's progress is updated once every 1 billion nanoseconds (1 second). This code sleeps for 1s to simulate a long running process that updates progress once per second and takes 100 seconds to complete. If you place a print statement after line 70, it is executed 100 times total after 1 minute and 40 seconds. I'd have to look at progress implementation, but it's possible/likely that it has some logic in it that's keeping it indeterminate because you're not on screen very long and the value is constantly changing. The indeterminate progress bar is visible (from the get-go and throughout the operation) even when the app stays in the foreground the whole time on iPad. There are details I could argue about in a "real" application I would be interested to hear! I do plan to put this in a real app with a slightly different long-running operation ha (notably you're manipulating UIKit from a background thread) I don't believe that's the case, setting breakpoints before the while block, in it, and after indicates it's on com.apple.main-thread.
Sep ’25
Reply to BGContinuedProcessingTask launchHandler invocation
the launchHandler is there to "tell" you the task has started and provide you the BGTask object you use to manage that work. No task type actually requires the work to occur inside the task. Thanks Kevin! So I need to think about this a little differently then. Instead of performing the work once you get a BGContinuedProcessingTask, just begin performing the work (the exact same way you do in iOS 18), if a BGContinuedProcessingTask starts persist it and update it as the work completes, and reset state when the work finishes or gets canceled. Basically, something like this: 1-6 I put together code to demonstrate this approach. Did I understand correctly, this should be all good with this simple counting demo? class ViewController: UIViewController { private let taskIdentifier = "\(Bundle.main.bundleIdentifier!).count_task" private let countToNumber: Int64 = 100 private var currentBackgroundContinuedProcessingTask: BGContinuedProcessingTask? private var shouldStopCounting = false @IBOutlet var progressLabel: UILabel! @IBAction func startCountingTapped(_ sender: UIButton) { performCountWork() // Register to support background continued processing // Ensure this is only registered once otherwise app crashes with exception: Launch handler for task with identifier has already been registered if !(UIApplication.shared.delegate as! AppDelegate).didRegisterCountBackgroundContinuedTask { (UIApplication.shared.delegate as! AppDelegate).didRegisterCountBackgroundContinuedTask = true BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: .main) { task in guard let task = task as? BGContinuedProcessingTask else { return } // The background continued processing task has started, use this task to manage the ongoing work self.currentBackgroundContinuedProcessingTask = task task.expirationHandler = { self.shouldStopCounting = true } } } // Request a background continued processing task start let request = BGContinuedProcessingTaskRequest( identifier: taskIdentifier, title: "Demo Task", subtitle: progressText(completedUnitCount: 0, totalUnitCount: countToNumber) ) request.strategy = .fail // Start the task immediately and fail if it cannot do { try BGTaskScheduler.shared.submit(request) } catch { // This should approximately never happen because the app doesn't submit any more tasks to exceed the system limit // But no big deal if it does not start, the user will just have to keep the app open to make progress print("Failed to submit task: \(error.localizedDescription)") } } @IBAction func stopCountingTapped(_ sender: UIButton) { shouldStopCounting = true } private func performCountWork() { Task { var countedToNumber: Int64 = 0 progressLabel.text = progressText(completedUnitCount: countedToNumber, totalUnitCount: countToNumber) currentBackgroundContinuedProcessingTask?.progress.completedUnitCount = countedToNumber currentBackgroundContinuedProcessingTask?.progress.totalUnitCount = Int64(countToNumber) while !shouldStopCounting && countedToNumber < countToNumber { try! await Task.sleep(nanoseconds: 1_000_000_000) countedToNumber += 1 progressLabel.text = progressText(completedUnitCount: countedToNumber, totalUnitCount: countToNumber) if let task = currentBackgroundContinuedProcessingTask { task.progress.completedUnitCount = countedToNumber task.updateTitle(task.title, subtitle: progressText(completedUnitCount: countedToNumber, totalUnitCount: countToNumber)) } } progressLabel.text = shouldStopCounting ? "Canceled" : "Success!" currentBackgroundContinuedProcessingTask?.setTaskCompleted(success: !shouldStopCounting) currentBackgroundContinuedProcessingTask = nil shouldStopCounting = false } } private func progressText(completedUnitCount: Int64, totalUnitCount: Int64) -> String { return "\(completedUnitCount) / \(totalUnitCount)" } } This seems to work, minus some behavior I did not expect: The Live Activity appears upon Start Counting while the app is in the foreground, I expected it to not appear until the user backgrounds the app. I'm using iPad Pro 26.0 (23A341). But on iPhone Air with 26.1 (23B5044l) the Live Activity does not appear until I close the app. Perhaps a bug that got resolved? The circular progress bar is spinning indeterminately in the Live Activity, not filling the circle as a percentage of completion (on both devices). the two strategies are "fail" (the default) and "queue" BGContinuedProcessingTaskRequest.strategy docs state queue is the default 😅
Sep ’25
Reply to SwiftUI's List backed by CoreData using @FetchRequest fails to update on iOS 26 when compiled with Xcode 26
I’m experiencing this issue with @FetchRequest in my app as well (using LazyVGrid instead of List). The above sample code replicates on iOS 26 beta 8 and 9 using the latest Xcode beta, though it seems to not replicate the first time you run the app only subsequent runs fail to update the UI upon changing a managed object a second time. I filed FB20029616 with a sysdiagnose and screen recording from when it replicated in my app.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Sep ’25
Reply to How to reduce cell height (vertical margins) when using UIListContentConfiguration
Thanks, I should note I want to reduce the vertical margins in the cell as opposed to setting an absolute fixed height dimension, in order to properly support dynamic type text sizes. This is the compositional layout I'm using: let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment) }
Topic: UI Frameworks SubTopic: UIKit Tags:
Aug ’25