Post

Replies

Boosts

Views

Activity

Reply to Called endBackgroundTask but not working
I refactored Eskimo's snippet, trying to fit Swift 6 strict concurrency: // (c) 2023 and onwards Quinn `the Eskimo` from Apple DTS Support. // Ref: https://developer.apple.com/forums/thread/729335 // Refactored by 2025 Shiki Suen for Swfit 6 Strict Concurrency. import Foundation /// Prevents the process from suspending by holding a `ProcessInfo` expiry /// activity assertion. /// /// The assertion is released if: /// /// * You explicitly release the assertion by calling ``release()``. /// * There are no more strong references to the object and so it gets /// deinitialized. /// * The system ‘calls in’ the assertion, in which case it calls the /// ``systemDidReleaseAssertion`` closure, if set. /// /// You should aim to explicitly release the assertion yourself, as soon as /// you’ve completed the work that the assertion covers. /// /// This uses `ProcessInfo.performExpiringActivity(withReason:using:)`, which /// is designed for CPU-bound work but is less ideal for I/O-bound tasks like /// networking (r. 109839489). This implementation avoids blocking a Dispatch /// worker thread by using Swift concurrency, improving efficiency compared to /// the original semaphore-based approach. public final actor BackgroundTaskAsserter { // MARK: Lifecycle /// Creates an assertion with the given name. /// /// The name isn’t used by the system but it does show up in various logs so /// it’s important to choose one that’s meaningful to you. public init(name: String, didReleaseHandler: (@MainActor () -> Void)? = nil) { self.name = name self.state = AssertionState() self.systemDidReleaseAssertion = didReleaseHandler // Start the expiring activity ProcessInfo.processInfo.performExpiringActivity( withReason: name ) { [state = self.state] didExpire in let task = Task { if didExpire { await state.transitionToReleased() await self.runSystemDidReleaseAssertion() } else { _ = await state.currentTask?.result } } Task { await state.transitionToStarted(task: task) } } } deinit { Task { [state = self.state] in await state.cancelTaskIfNeeded() } } // MARK: Public /// Manages the state of the assertion in a thread-safe manner. public actor AssertionState { // MARK: Public public enum State { case starting case started(Task<Void, Never>) case released } public var currentTask: Task<Void, Never>? { guard case let .started(task) = state else { return nil } return task } public var isReleased: Bool { if case .released = state { return true } return false } public func transitionToStarted(task: Task<Void, Never>) { guard case .starting = state else { return } state = .started(task) } public func transitionToReleased() { switch state { case .started, .starting: state = .released case .released: break // Already released, no-op } } public func cancelTaskIfNeeded() { if case let .started(task) = state { task.cancel() } state = .released } // MARK: Private private var state: State = .starting } /// The name used when creating the assertion. public let name: String /// Called when the system releases the assertion itself. /// /// This is called on the main actor. /// /// To help avoid retain cycles, the object sets this to `nil` whenever the /// assertion is released. public var systemDidReleaseAssertion: (@MainActor () -> Void)? public let state: AssertionState /// Release the assertion. /// /// It’s safe to call this redundantly, that is, call it twice in a row or /// call it on an assertion that’s expired. public func release() async { await state.cancelTaskIfNeeded() Task { systemDidReleaseAssertion = nil } } // MARK: Private private func runSystemDidReleaseAssertion() { Task { await systemDidReleaseAssertion?() systemDidReleaseAssertion = nil } } }
Jun ’25
Reply to Is Code-level support still allowing cases dealing with macOS 10.9 Mavericks?
This issue has been solved. Here's the things to beware of when migrating NSStackView things to macOS 10.9: (Not talking about macOS 10.10 - 10.12 due to lack of tests.) macOS 10.9 doesn't draw shadow for opaque windows. In macOS 10.9, even if you set .isOpaque = true and the background color to NSColor.clear, it won't make clear backgrounds like what macOS 10.13 and later behaves. NSStackView in macOS 10.9 bugs, requiring every of its components to be constraint-set in order to make the entire view size looks as big as what behaves on macOS 10.13 and later. The subViews of an NSStackView might be more than the views you actually added into it. Instead of directly enumerating the .subviews of an NSStackView, you create another Swift array to track these subviews before adding them to the NSStackView instance.
Topic: UI Frameworks SubTopic: AppKit Tags:
Feb ’23
Reply to How to get the bundle identifier of the app bundle dragged into an NSTableView?
I solved this. Prerequisites: NSTableView instance must have its DataSource object assigned: myNSTableViewInstance.dataSource = XXX The DataSource object conforms to NSTableViewDataSource protocol, having two necessary functions implemented (will discuss below). Whenever appropriate (e.g.: .windowDidLoad(), .viewDidLoad(), etc.), register the .registerForDraggedTypes for NSTableView instance. In the prerequisite 3, the registration method is: // Constructing the NSPasteboard.PasteboardType. tblClients.registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)]) // Compatible with macOS 10.9 Mavericks. tblClients.registerForDraggedTypes([.fileURL]) // Since macOS 10.13 High Sierra. In the prerequisite 2, the registration method is: // Constructing the NSPasteboard.PasteboardType. .init(rawValue: kUTTypeApplicationBundle as String) We start implementing. Suppose that (in this case): the DataSource object is the WindowController itself. the name of the NSTableView instance is tblClients. Since the two functions mentioned above shares a lot of things, we extract them as a standalone function with lambda expressions: /// See whether the incoming NSDraggingInfo is an array of App Bundles. /// - Parameters: /// - info: the incoming NSDraggingInfo object。 /// - onError: On error perform this given lambda expression. /// - handler: When conditions are met, perform this given lambda expression to process the URL array. private func validatePasteboardForAppBundles( neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: @escaping ([URL]) -> Void? ) { let board = info.draggingPasteboard let type = NSPasteboard.PasteboardType(rawValue: kUTTypeApplicationBundle as String) let options: [NSPasteboard.ReadingOptionKey: Any] = [ .urlReadingFileURLsOnly: true, .urlReadingContentsConformToTypes: [type], ] guard let urls = board.readObjects(forClasses: [NSURL.self], options: options) as? [URL], !urls.isEmpty else { onError() return } handler(urls) } Finally, the two implementations. The one to check whether the dragged file object is the one we need to let the NSTableView response to: func tableView( _: NSTableView, validateDrop info: NSDraggingInfo, proposedRow _: Int, proposedDropOperation _: NSTableView.DropOperation ) -> NSDragOperation { var result = NSDragOperation.copy validatePasteboardForAppBundles( neta: info, onError: { result = .init(rawValue: 0) }, // NSDragOperationNone。 handler: { _ in } ) return result } The handling process once the mouse click releases: func tableView( _: NSTableView, acceptDrop info: NSDraggingInfo, row _: Int, dropOperation _: NSTableView.DropOperation ) -> Bool { var result = true // Outer report: Operation successful or not. validatePasteboardForAppBundles( neta: info, onError: { result = false } // NSDragOperationNone。 ) { theURLs in var dealt = false // Inner report. Will be true if at least one URL entry is processed with successful result. theURLs.forEach { url in // See whether this is an app bundle with valid bundleID: guard let bundle = Bundle(url: url), let bundleID = bundle.bundleIdentifier else { return } // Performing customized operations toward this bundleID string: self.applyNewValue(bundleID, highMitigation: true) // Mark this operation as "successful". dealt = true } // Tell the outer report that whether we did at least one successful operation. result = dealt } defer { if result { self.tblClients.reloadData() } } return result } This should work unless there are other things screwed up. $ EOF.
Topic: UI Frameworks SubTopic: AppKit Tags:
Feb ’23