diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index cc2585691..c4ca44916 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -7,7 +7,7 @@ // import AppKit -import WebKit +@preconcurrency import WebKit import RSCore import RSWeb import Articles diff --git a/Mac/MainWindow/NNW3/NNW3ImportController.swift b/Mac/MainWindow/NNW3/NNW3ImportController.swift index 32084490e..c54a77c76 100644 --- a/Mac/MainWindow/NNW3/NNW3ImportController.swift +++ b/Mac/MainWindow/NNW3/NNW3ImportController.swift @@ -69,7 +69,7 @@ private extension NNW3ImportController { panel.canChooseDirectories = false panel.resolvesAliases = true panel.directoryURL = NNW3ImportController.defaultFileURL - panel.allowedFileTypes = ["plist"] + panel.allowedContentTypes = [.propertyList] panel.allowsOtherFileTypes = false panel.accessoryView = accessoryViewController.view panel.isAccessoryViewDisclosed = true diff --git a/Mac/MainWindow/OPML/ExportOPMLWindowController.swift b/Mac/MainWindow/OPML/ExportOPMLWindowController.swift index 4b2ce20a6..2d2ac30cf 100644 --- a/Mac/MainWindow/OPML/ExportOPMLWindowController.swift +++ b/Mac/MainWindow/OPML/ExportOPMLWindowController.swift @@ -8,6 +8,7 @@ import AppKit import Account +import UniformTypeIdentifiers class ExportOPMLWindowController: NSWindowController { @@ -75,7 +76,7 @@ class ExportOPMLWindowController: NSWindowController { func exportOPML(account: Account) { let panel = NSSavePanel() - panel.allowedFileTypes = ["opml"] + panel.allowedContentTypes = [UTType.opml] panel.allowsOtherFileTypes = false panel.prompt = NSLocalizedString("Export OPML", comment: "Export OPML") panel.title = NSLocalizedString("Export OPML", comment: "Export OPML") diff --git a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift index f0737fe93..1db6ca6d2 100644 --- a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift +++ b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift @@ -8,6 +8,7 @@ import AppKit import Account +import UniformTypeIdentifiers class ImportOPMLWindowController: NSWindowController { @@ -85,7 +86,7 @@ class ImportOPMLWindowController: NSWindowController { panel.allowsMultipleSelection = false panel.canChooseDirectories = false panel.resolvesAliases = true - panel.allowedFileTypes = ["opml", "xml"] + panel.allowedContentTypes = [UTType.opml, UTType.xml] panel.allowsOtherFileTypes = false panel.beginSheetModal(for: hostWindow!) { modalResult in diff --git a/Mac/MainWindow/Sidebar/PasteboardFolder.swift b/Mac/MainWindow/Sidebar/PasteboardFolder.swift index 60c42154c..0ffd6c08b 100644 --- a/Mac/MainWindow/Sidebar/PasteboardFolder.swift +++ b/Mac/MainWindow/Sidebar/PasteboardFolder.swift @@ -84,7 +84,7 @@ struct PasteboardFolder: Hashable { } } -extension Folder: PasteboardWriterOwner { +extension Folder: @retroactive PasteboardWriterOwner { public var pasteboardWriter: NSPasteboardWriting { return FolderPasteboardWriter(folder: self) diff --git a/Mac/MainWindow/Sidebar/PasteboardWebFeed.swift b/Mac/MainWindow/Sidebar/PasteboardWebFeed.swift index 31612ded9..f0c838c47 100644 --- a/Mac/MainWindow/Sidebar/PasteboardWebFeed.swift +++ b/Mac/MainWindow/Sidebar/PasteboardWebFeed.swift @@ -146,7 +146,7 @@ struct PasteboardWebFeed: Hashable { } } -extension WebFeed: PasteboardWriterOwner { +extension WebFeed: @retroactive PasteboardWriterOwner { public var pasteboardWriter: NSPasteboardWriting { return WebFeedPasteboardWriter(webFeed: self) diff --git a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift index 4dd5b80d5..29be01571 100644 --- a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -10,7 +10,7 @@ import AppKit import Articles import RSCore -extension Article: PasteboardWriterOwner { +extension Article: @retroactive PasteboardWriterOwner { public var pasteboardWriter: NSPasteboardWriting { return ArticlePasteboardWriter(article: self) } diff --git a/Mac/MainWindow/Timeline/Cell/TimelineCellAppearance.swift b/Mac/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index b288ff911..dbfdf045b 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -67,7 +67,7 @@ struct TimelineCellAppearance: Equatable { } } -extension NSEdgeInsets: Equatable { +extension NSEdgeInsets: @retroactive Equatable { public static func ==(lhs: NSEdgeInsets, rhs: NSEdgeInsets) -> Bool { return lhs.left == rhs.left && lhs.top == rhs.top && lhs.right == rhs.right && lhs.bottom == rhs.bottom diff --git a/Mac/MainWindow/Timeline/NSSharingService+Extension.h b/Mac/MainWindow/Timeline/NSSharingService+Extension.h new file mode 100644 index 000000000..389a58161 --- /dev/null +++ b/Mac/MainWindow/Timeline/NSSharingService+Extension.h @@ -0,0 +1,23 @@ +// +// NSObject+NSSharingService_RSCore.h +// RSCore +// +// Created by Brent Simmons on 11/3/24. +// + +@import AppKit; + +@interface NSSharingService (NoDeprecationWarning) + +// The only way to create custom UI — a Share menu, for instance — +// is to use the unfortunately deprecated +// +[NSSharingService sharingServicesForItems:]. +// This cover method allows us to not generate a warning. +// +// We know it’s deprecated, and we don’t want to be bugged +// about it every time we build. (If anyone from Apple +// is reading this — a replacement would be very welcome!) + ++ (NSArray *)sharingServicesForItems_noDeprecationWarning:(NSArray *)items; + +@end diff --git a/Mac/MainWindow/Timeline/NSSharingService+Extension.m b/Mac/MainWindow/Timeline/NSSharingService+Extension.m new file mode 100644 index 000000000..cf2e71e26 --- /dev/null +++ b/Mac/MainWindow/Timeline/NSSharingService+Extension.m @@ -0,0 +1,22 @@ +// +// NSSharingService+Extension.m +// RSCore +// +// Created by Brent Simmons on 11/3/24. +// + +#import "NSSharingService+Extension.h" + +@implementation NSSharingService (NoDeprecationWarning) + ++ (NSArray *)sharingServicesForItems_noDeprecationWarning:(NSArray *)items { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + return [NSSharingService sharingServicesForItems:items]; + +#pragma clang diagnostic pop +} + +@end diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index dd83afdcb..1cc5d6e3e 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -200,7 +200,7 @@ private extension TimelineViewController { let sortedArticles = articles.sortedByDate(.orderedAscending) let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) } - let standardServices = NSSharingService.sharingServices(forItems: items) + let standardServices = NSSharingService.sharingServices(forItems_noDeprecationWarning: items) as? [NSSharingService] ?? [NSSharingService]() let customServices = SharingServicePickerDelegate.customSharingServices(for: items) let services = standardServices + customServices if services.isEmpty { diff --git a/Mac/NetNewsWire-Bridging-Header.h b/Mac/NetNewsWire-Bridging-Header.h index 3f547bcf1..01bbec4e2 100644 --- a/Mac/NetNewsWire-Bridging-Header.h +++ b/Mac/NetNewsWire-Bridging-Header.h @@ -7,3 +7,4 @@ // #import "WKPreferencesPrivate.h" +#import "NSSharingService+Extension.h" diff --git a/Mac/Preferences/Accounts/AddAccountsView.swift b/Mac/Preferences/Accounts/AddAccountsView.swift index 6fc31957d..c8a496ad2 100644 --- a/Mac/Preferences/Accounts/AddAccountsView.swift +++ b/Mac/Preferences/Accounts/AddAccountsView.swift @@ -222,7 +222,7 @@ struct AddAccountsView: View { .padding(.top, 8) HStack { - ForEach(0.. Bool diff --git a/Modules/RSCore/Sources/RSCore/AppKit/RSAppMovementMonitor.swift b/Modules/RSCore/Sources/RSCore/AppKit/RSAppMovementMonitor.swift index 83c1d49fb..7225937a1 100644 --- a/Modules/RSCore/Sources/RSCore/AppKit/RSAppMovementMonitor.swift +++ b/Modules/RSCore/Sources/RSCore/AppKit/RSAppMovementMonitor.swift @@ -128,8 +128,11 @@ public class RSAppMovementMonitor: NSObject { func relaunchFromURL(_ appURL: URL) { // Relaunching is best achieved by requesting that the system launch the app // at the given URL with the "new instance" option to prevent it simply reactivating us. - let _ = try? NSWorkspace.shared.launchApplication(at: appURL, options: .newInstance, configuration: [:]) - NSApp.terminate(self) + let configuration = NSWorkspace.OpenConfiguration() + configuration.createsNewApplicationInstance = true + NSWorkspace.shared.openApplication(at: appURL, configuration: configuration) { _, _ in + NSApp.terminate(self) + } } func defaultHandler() { diff --git a/Modules/RSCore/Sources/RSCore/AppKit/UserApp.swift b/Modules/RSCore/Sources/RSCore/AppKit/UserApp.swift index 974f82875..eb79a04d6 100644 --- a/Modules/RSCore/Sources/RSCore/AppKit/UserApp.swift +++ b/Modules/RSCore/Sources/RSCore/AppKit/UserApp.swift @@ -63,7 +63,7 @@ public final class UserApp { path = bundleURL.path } else { - path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path } if icon == nil, let path = path { icon = NSWorkspace.shared.icon(forFile: path) @@ -71,7 +71,7 @@ public final class UserApp { return } - path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path if let path = path { if icon == nil { icon = NSWorkspace.shared.icon(forFile: path) @@ -84,7 +84,7 @@ public final class UserApp { } } - public func launchIfNeeded() -> Bool { + public func launchIfNeeded() async -> Bool { // Return true if already running. // Return true if not running and successfully gets launched. @@ -99,20 +99,29 @@ public final class UserApp { } let url = URL(fileURLWithPath: path) - if let app = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) { - runningApplication = app - if app.isFinishedLaunching { - return true - } - Thread.sleep(forTimeInterval: 1.0) // Give the app time to launch. This is ugly. - if app.isFinishedLaunching { - return true - } - Thread.sleep(forTimeInterval: 1.0) // Give it some *more* time. - return true - } - return false + do { + let configuration = NSWorkspace.OpenConfiguration() + configuration.promptsUserIfNeeded = true + + let app = try await NSWorkspace.shared.openApplication(at: url, configuration: configuration) + runningApplication = app + + if app.isFinishedLaunching { + return true + } + + try? await Task.sleep(for: .seconds(1)) // Give the app time to launch. This is ugly. + if app.isFinishedLaunching { + return true + } + + try? await Task.sleep(for: .seconds(1)) // Give it some *more* time. + return true + + } catch { + return false + } } public func bringToFront() -> Bool { diff --git a/Modules/RSCore/Sources/RSCore/CloudKit/CloudKitZone.swift b/Modules/RSCore/Sources/RSCore/CloudKit/CloudKitZone.swift index 10479231d..94dddb695 100644 --- a/Modules/RSCore/Sources/RSCore/CloudKit/CloudKitZone.swift +++ b/Modules/RSCore/Sources/RSCore/CloudKit/CloudKitZone.swift @@ -26,13 +26,13 @@ public enum CloudKitZoneError: LocalizedError { } } -public protocol CloudKitZoneDelegate: class { +public protocol CloudKitZoneDelegate: AnyObject { func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void); } public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: CKRecord.ID) -public protocol CloudKitZone: class { +public protocol CloudKitZone: AnyObject { static var qualityOfService: QualityOfService { get } diff --git a/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperation.swift b/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperation.swift index 5f6fd5d0c..92a4f97c3 100644 --- a/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperation.swift +++ b/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperation.swift @@ -15,7 +15,7 @@ import Foundation /// When it’s canceled, it should do its best to stop /// doing whatever it’s doing. However, it should not /// leave data in an inconsistent state. -public protocol MainThreadOperation: class { +public protocol MainThreadOperation: AnyObject { // These three properties are set by MainThreadOperationQueue. Don’t set them. var isCanceled: Bool { get set } // Check this at appropriate times in case the operation has been canceled. diff --git a/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperationQueue.swift b/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperationQueue.swift index 36455586e..be7d225dd 100644 --- a/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperationQueue.swift +++ b/Modules/RSCore/Sources/RSCore/Shared/MainThreadOperationQueue.swift @@ -8,7 +8,7 @@ import Foundation -public protocol MainThreadOperationDelegate: class { +public protocol MainThreadOperationDelegate: AnyObject { func operationDidComplete(_ operation: MainThreadOperation) func cancelOperation(_ operation: MainThreadOperation) func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) diff --git a/Modules/RSCore/Sources/RSCore/Shared/UndoableCommand.swift b/Modules/RSCore/Sources/RSCore/Shared/UndoableCommand.swift index 1a79051b5..5594151ff 100644 --- a/Modules/RSCore/Sources/RSCore/Shared/UndoableCommand.swift +++ b/Modules/RSCore/Sources/RSCore/Shared/UndoableCommand.swift @@ -8,7 +8,7 @@ import Foundation -public protocol UndoableCommand: class { +public protocol UndoableCommand: AnyObject { var undoActionName: String { get } var redoActionName: String { get } @@ -39,7 +39,7 @@ extension UndoableCommand { // Useful for view controllers. -public protocol UndoableCommandRunner: class { +public protocol UndoableCommandRunner: AnyObject { var undoableCommands: [UndoableCommand] { get set } var undoManager: UndoManager? { get } diff --git a/Modules/RSTree/Sources/RSTree/TreeController.swift b/Modules/RSTree/Sources/RSTree/TreeController.swift index 6475f9a60..84178243e 100644 --- a/Modules/RSTree/Sources/RSTree/TreeController.swift +++ b/Modules/RSTree/Sources/RSTree/TreeController.swift @@ -8,8 +8,8 @@ import Foundation -public protocol TreeControllerDelegate: class { - +public protocol TreeControllerDelegate: AnyObject { + func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]? } diff --git a/Modules/RSWeb/Package.swift b/Modules/RSWeb/Package.swift index 71a511d0e..4b021105e 100644 --- a/Modules/RSWeb/Package.swift +++ b/Modules/RSWeb/Package.swift @@ -20,8 +20,8 @@ let package = Package( dependencies: [ "RSParser", "RSCore" - ] - //swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + ], + swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] ), .testTarget( name: "RSWebTests", diff --git a/Modules/RSWeb/Sources/RSWeb/MacWebBrowser.swift b/Modules/RSWeb/Sources/RSWeb/MacWebBrowser.swift index 74c784169..1e8a1298f 100755 --- a/Modules/RSWeb/Sources/RSWeb/MacWebBrowser.swift +++ b/Modules/RSWeb/Sources/RSWeb/MacWebBrowser.swift @@ -8,6 +8,7 @@ #if os(macOS) import AppKit +import UniformTypeIdentifiers public class MacWebBrowser { @@ -18,34 +19,28 @@ public class MacWebBrowser { return false } - if (inBackground) { - do { - try NSWorkspace.shared.open(preparedURL, options: [.withoutActivation], configuration: [:]) - return true - } - catch { - return false - } + if inBackground { + + let configuration = NSWorkspace.OpenConfiguration() + configuration.activates = false + NSWorkspace.shared.open(url, configuration: configuration, completionHandler: nil) + + return true } - + return NSWorkspace.shared.open(preparedURL) } /// Returns an array of the browsers installed on the system, sorted by name. /// /// "Browsers" are applications that can both handle `https` URLs, and display HTML documents. - public class func sortedBrowsers() -> [MacWebBrowser] { - guard let httpsIDs = LSCopyAllHandlersForURLScheme("https" as CFString)?.takeRetainedValue() as? [String] else { - return [] - } + public static func sortedBrowsers() -> [MacWebBrowser] { - guard let htmlIDs = LSCopyAllRoleHandlersForContentType(kUTTypeHTML, .viewer)?.takeRetainedValue() as? [String] else { - return [] - } + let httpsAppURLs = NSWorkspace.shared.urlsForApplications(toOpen: URL(string: "https://apple.com/")!) + let htmlAppURLs = NSWorkspace.shared.urlsForApplications(toOpen: UTType.html) + let browserAppURLs = Set(httpsAppURLs).intersection(Set(htmlAppURLs)) - let browserIDs = Set(httpsIDs).intersection(Set(htmlIDs)) - - return browserIDs.compactMap { MacWebBrowser(bundleIdentifier: $0) }.sorted { + return browserAppURLs.compactMap { MacWebBrowser(url: $0) }.sorted { if let leftName = $0.name, let rightName = $1.name { return leftName < rightName } @@ -127,15 +122,25 @@ public class MacWebBrowser { /// - url: The URL to open. /// - inBackground: If `true`, attempt to load the URL without bringing the browser to the foreground. @discardableResult public func openURL(_ url: URL, inBackground: Bool = false) -> Bool { + + // TODO: make this function async. + guard let preparedURL = url.preparedForOpeningInBrowser() else { return false } - let options: NSWorkspace.LaunchOptions = inBackground ? [.withoutActivation] : [] + Task { @MainActor in - return NSWorkspace.shared.open([preparedURL], withAppBundleIdentifier: self.bundleIdentifier, options: options, additionalEventParamDescriptor: nil, launchIdentifiers: nil) + let configuration = NSWorkspace.OpenConfiguration() + if inBackground { + configuration.activates = false + } + + NSWorkspace.shared.open([preparedURL], withApplicationAt: self.url, configuration: configuration, completionHandler: nil) + } + + return true } - } extension MacWebBrowser: CustomDebugStringConvertible { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index de5e23df4..82eb74897 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -915,7 +915,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1610; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 176813F22564BB2C00D98635 = { @@ -959,6 +959,7 @@ 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = SHJK2V3AJG; + LastSwiftMigration = 1620; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.HardenedRuntime = { diff --git a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire iOS Share Extension.xcscheme b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire iOS Share Extension.xcscheme index 422da8511..8b374fd88 100644 --- a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire iOS Share Extension.xcscheme +++ b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire iOS Share Extension.xcscheme @@ -1,6 +1,6 @@