Remove unneeded #available and @available.

This commit is contained in:
Brent Simmons 2024-11-11 22:03:32 -08:00
parent 8a611f4109
commit a8eb75c20f
13 changed files with 204 additions and 536 deletions

View File

@ -106,20 +106,6 @@ final class DetailWebViewController: NSViewController {
view = webView view = webView
// Use the safe area layout guides if they are available.
if #available(OSX 11.0, *) {
// These constraints have been removed as they were unsatisfiable after removing NSBox.
} else {
let constraints = [
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
]
NSLayoutConstraint.activate(constraints)
}
// Hide the web view until the first reload (navigation) is complete (plus some delay) to avoid the awful white flash that happens on the initial display in dark mode. // Hide the web view until the first reload (navigation) is complete (plus some delay) to avoid the awful white flash that happens on the initial display in dark mode.
// See bug #901. // See bug #901.
webView.isHidden = true webView.isHidden = true

View File

@ -102,45 +102,24 @@ struct AddAccountsView: View {
HStack(spacing: 12) { HStack(spacing: 12) {
Spacer() Spacer()
if #available(OSX 11.0, *) { Button(action: {
Button(action: { parent?.dismiss(nil)
parent?.dismiss(nil) }, label: {
}, label: { Text("Cancel")
Text("Cancel") .frame(width: 76)
.frame(width: 76) })
}) .help("Cancel")
.help("Cancel") .keyboardShortcut(.cancelAction)
.keyboardShortcut(.cancelAction)
Button(action: {
} else { addAccountDelegate?.presentSheetForAccount(selectedAccount)
Button(action: { parent?.dismiss(nil)
parent?.dismiss(nil) }, label: {
}, label: { Text("Continue")
Text("Cancel") .frame(width: 76)
.frame(width: 76) })
}) .help("Add Account")
.accessibility(label: Text("Add Account")) .keyboardShortcut(.defaultAction)
}
if #available(OSX 11.0, *) {
Button(action: {
addAccountDelegate?.presentSheetForAccount(selectedAccount)
parent?.dismiss(nil)
}, label: {
Text("Continue")
.frame(width: 76)
})
.help("Add Account")
.keyboardShortcut(.defaultAction)
} else {
Button(action: {
addAccountDelegate?.presentSheetForAccount(selectedAccount)
parent?.dismiss(nil)
}, label: {
Text("Continue")
.frame(width: 76)
})
}
} }
.padding(.top, 12) .padding(.top, 12)
.padding(.bottom, 4) .padding(.bottom, 4)
@ -149,8 +128,8 @@ struct AddAccountsView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.frame(width: 420) .frame(width: 420)
.padding() .padding()
} }
var localAccount: some View { var localAccount: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Local") Text("Local")

View File

@ -10,15 +10,10 @@
import AppKit import AppKit
extension NSAppearance { extension NSAppearance {
@objc(rsIsDarkMode) @objc(rsIsDarkMode)
public var isDarkMode: Bool { public var isDarkMode: Bool {
if #available(macOS 10.14, *) { return self.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return self.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
else {
return false
}
} }
} }
#endif #endif

View File

@ -23,16 +23,14 @@ class RSDarkModeAdaptingToolbarButton: NSButton {
override func layout() { override func layout() {
// Always re-set the NSImage template state based on the current dark mode setting // Always re-set the NSImage template state based on the current dark mode setting
if #available(macOS 10.14, *) { if self.forceTemplateInDarkMode, let targetImage = self.image {
if self.forceTemplateInDarkMode, let targetImage = self.image { var newTemplateState: Bool = self.originalImageTemplateState
var newTemplateState: Bool = self.originalImageTemplateState
if self.effectiveAppearance.isDarkMode { if self.effectiveAppearance.isDarkMode {
newTemplateState = true newTemplateState = true
}
targetImage.isTemplate = newTemplateState
} }
targetImage.isTemplate = newTemplateState
} }
super.layout() super.layout()

View File

@ -7,45 +7,17 @@
// //
import Foundation import Foundation
#if canImport(CryptoKit)
import CryptoKit import CryptoKit
#endif
import CommonCrypto
public extension Data { public extension Data {
/// The MD5 hash of the data.
var md5Hash: Data { var md5Hash: Data {
let digest = Insecure.MD5.hash(data: self)
#if canImport(CryptoKit) return Data(digest)
if #available(macOS 10.15, *) {
let digest = Insecure.MD5.hash(data: self)
return Data(digest)
} else {
return ccMD5Hash
}
#else
return ccMD5Hash
#endif
} }
@available(macOS, deprecated: 10.15)
@available(iOS, deprecated: 13.0)
private var ccMD5Hash: Data {
let len = Int(CC_MD5_DIGEST_LENGTH)
let md = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: len)
let _ = self.withUnsafeBytes {
CC_MD5($0.baseAddress, numericCast($0.count), md)
}
return Data(bytes: md, count: len)
}
/// The MD5 has of the data, as a hexadecimal string.
var md5String: String? { var md5String: String? {
return md5Hash.hexadecimalString md5Hash.hexadecimalString
} }
/// Image signature constants. /// Image signature constants.
@ -174,7 +146,5 @@ public extension Data {
} }
return reduce("") { $0 + String(format: "%02x", $1) } return reduce("") { $0 + String(format: "%02x", $1) }
} }
} }

View File

@ -29,37 +29,28 @@ import SystemConfiguration
import Foundation import Foundation
public enum ReachabilityError: Error { public enum ReachabilityError: Error {
case failedToCreateWithAddress(sockaddr, Int32) case failedToCreateWithHostname(String, Int32)
case failedToCreateWithHostname(String, Int32) case unableToGetFlags(Int32)
case unableToSetCallback(Int32)
case unableToSetDispatchQueue(Int32)
case unableToGetFlags(Int32)
}
@available(*, unavailable, renamed: "Notification.Name.reachabilityChanged")
public let ReachabilityChangedNotification = NSNotification.Name("ReachabilityChangedNotification")
public extension Notification.Name {
static let reachabilityChanged = Notification.Name("reachabilityChanged")
} }
public class Reachability { public class Reachability {
/// Returns true if the internet is reachable.
///
/// Uses apple.com as an indicator for the internet at large,
/// since its surely one of the easiest-to-reach domain names.
/// Added 6 April 2024. Not part of Ashley Millss original code.
public static var internetIsReachable: Bool {
guard let reachability = try? Reachability(hostname: "apple.com") else {
return false
}
return reachability.connection != .unavailable
}
public typealias NetworkReachable = (Reachability) -> () public typealias NetworkReachable = (Reachability) -> ()
public typealias NetworkUnreachable = (Reachability) -> () public typealias NetworkUnreachable = (Reachability) -> ()
@available(*, unavailable, renamed: "Connection")
public enum NetworkStatus: CustomStringConvertible {
case notReachable, reachableViaWiFi, reachableViaWWAN
public var description: String {
switch self {
case .reachableViaWWAN: return "Cellular"
case .reachableViaWiFi: return "WiFi"
case .notReachable: return "No Connection"
}
}
}
public enum Connection: CustomStringConvertible { public enum Connection: CustomStringConvertible {
@available(*, deprecated, renamed: "unavailable") @available(*, deprecated, renamed: "unavailable")
case none case none
@ -74,29 +65,10 @@ public class Reachability {
} }
} }
public var whenReachable: NetworkReachable?
public var whenUnreachable: NetworkUnreachable?
@available(*, deprecated, renamed: "allowsCellularConnection")
public let reachableOnWWAN: Bool = true
/// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`) /// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`)
public var allowsCellularConnection: Bool public var allowsCellularConnection: Bool
// The notification center on which "reachability changed" events are being posted public var connection: Connection {
public var notificationCenter: NotificationCenter = NotificationCenter.default
@available(*, deprecated, renamed: "connection.description")
public var currentReachabilityString: String {
return "\(connection)"
}
@available(*, unavailable, renamed: "connection")
public var currentReachabilityStatus: Connection {
return connection
}
public var connection: Connection {
if flags == nil { if flags == nil {
try? setReachabilityFlags() try? setReachabilityFlags()
} }
@ -109,298 +81,127 @@ public class Reachability {
} }
} }
fileprivate var isRunningOnDevice: Bool = {
#if targetEnvironment(simulator)
return false
#else
return true
#endif
}()
fileprivate(set) var notifierRunning = false fileprivate(set) var notifierRunning = false
fileprivate let reachabilityRef: SCNetworkReachability fileprivate let reachabilityRef: SCNetworkReachability
fileprivate let reachabilitySerialQueue: DispatchQueue fileprivate let reachabilitySerialQueue: DispatchQueue
fileprivate let notificationQueue: DispatchQueue? fileprivate(set) var flags: SCNetworkReachabilityFlags?
fileprivate(set) var flags: SCNetworkReachabilityFlags? {
didSet {
guard flags != oldValue else { return }
notifyReachabilityChanged()
}
}
required public init(reachabilityRef: SCNetworkReachability, required public init(reachabilityRef: SCNetworkReachability,
queueQoS: DispatchQoS = .default, queueQoS: DispatchQoS = .default,
targetQueue: DispatchQueue? = nil, targetQueue: DispatchQueue? = nil) {
notificationQueue: DispatchQueue? = .main) { self.allowsCellularConnection = true
self.allowsCellularConnection = true self.reachabilityRef = reachabilityRef
self.reachabilityRef = reachabilityRef self.reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability", qos: queueQoS, target: targetQueue)
self.reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability", qos: queueQoS, target: targetQueue) }
self.notificationQueue = notificationQueue
}
public convenience init(hostname: String, public convenience init(hostname: String,
queueQoS: DispatchQoS = .default, queueQoS: DispatchQoS = .default,
targetQueue: DispatchQueue? = nil, targetQueue: DispatchQueue? = nil) throws {
notificationQueue: DispatchQueue? = .main) throws { guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else {
guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { throw ReachabilityError.failedToCreateWithHostname(hostname, SCError())
throw ReachabilityError.failedToCreateWithHostname(hostname, SCError()) }
} self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue)
self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue, notificationQueue: notificationQueue) }
}
public convenience init(queueQoS: DispatchQoS = .default,
targetQueue: DispatchQueue? = nil,
notificationQueue: DispatchQueue? = .main) throws {
var zeroAddress = sockaddr()
zeroAddress.sa_len = UInt8(MemoryLayout<sockaddr>.size)
zeroAddress.sa_family = sa_family_t(AF_INET)
guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else {
throw ReachabilityError.failedToCreateWithAddress(zeroAddress, SCError())
}
self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue, notificationQueue: notificationQueue)
}
deinit {
stopNotifier()
}
} }
public extension Reachability { public extension Reachability {
// MARK: - *** Notifier methods *** var description: String {
func startNotifier() throws { return flags?.description ?? "unavailable flags"
guard !notifierRunning else { return } }
let callback: SCNetworkReachabilityCallBack = { (reachability, flags, info) in
guard let info = info else { return }
// `weakifiedReachability` is guaranteed to exist by virtue of our
// retain/release callbacks which we provided to the `SCNetworkReachabilityContext`.
let weakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info).takeUnretainedValue()
// The weak `reachability` _may_ no longer exist if the `Reachability`
// object has since been deallocated but a callback was already in flight.
weakifiedReachability.reachability?.flags = flags
}
let weakifiedReachability = ReachabilityWeakifier(reachability: self)
let opaqueWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.passUnretained(weakifiedReachability).toOpaque()
var context = SCNetworkReachabilityContext(
version: 0,
info: UnsafeMutableRawPointer(opaqueWeakifiedReachability),
retain: { (info: UnsafeRawPointer) -> UnsafeRawPointer in
let unmanagedWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info)
_ = unmanagedWeakifiedReachability.retain()
return UnsafeRawPointer(unmanagedWeakifiedReachability.toOpaque())
},
release: { (info: UnsafeRawPointer) -> Void in
let unmanagedWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info)
unmanagedWeakifiedReachability.release()
},
copyDescription: { (info: UnsafeRawPointer) -> Unmanaged<CFString> in
let unmanagedWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info)
let weakifiedReachability = unmanagedWeakifiedReachability.takeUnretainedValue()
let description = weakifiedReachability.reachability?.description ?? "nil"
return Unmanaged.passRetained(description as CFString)
}
)
if !SCNetworkReachabilitySetCallback(reachabilityRef, callback, &context) {
stopNotifier()
throw ReachabilityError.unableToSetCallback(SCError())
}
if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef, reachabilitySerialQueue) {
stopNotifier()
throw ReachabilityError.unableToSetDispatchQueue(SCError())
}
// Perform an initial check
try setReachabilityFlags()
notifierRunning = true
}
func stopNotifier() {
defer { notifierRunning = false }
SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil)
SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil)
}
// MARK: - *** Connection test methods ***
@available(*, deprecated, message: "Please use `connection != .none`")
var isReachable: Bool {
return connection != .unavailable
}
@available(*, deprecated, message: "Please use `connection == .cellular`")
var isReachableViaWWAN: Bool {
// Check we're not on the simulator, we're REACHABLE and check we're on WWAN
return connection == .cellular
}
@available(*, deprecated, message: "Please use `connection == .wifi`")
var isReachableViaWiFi: Bool {
return connection == .wifi
}
var description: String {
return flags?.description ?? "unavailable flags"
}
} }
fileprivate extension Reachability { fileprivate extension Reachability {
func setReachabilityFlags() throws { func setReachabilityFlags() throws {
try reachabilitySerialQueue.sync { [unowned self] in try reachabilitySerialQueue.sync { [unowned self] in
var flags = SCNetworkReachabilityFlags() var flags = SCNetworkReachabilityFlags()
if !SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags) { if !SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags) {
self.stopNotifier() throw ReachabilityError.unableToGetFlags(SCError())
throw ReachabilityError.unableToGetFlags(SCError()) }
}
self.flags = flags
}
}
func notifyReachabilityChanged() { self.flags = flags
let notify = { [weak self] in }
guard let self = self else { return } }
self.connection != .unavailable ? self.whenReachable?(self) : self.whenUnreachable?(self)
self.notificationCenter.post(name: .reachabilityChanged, object: self)
}
// notify on the configured `notificationQueue`, or the caller's (i.e. `reachabilitySerialQueue`)
notificationQueue?.async(execute: notify) ?? notify()
}
} }
extension SCNetworkReachabilityFlags { extension SCNetworkReachabilityFlags {
typealias Connection = Reachability.Connection typealias Connection = Reachability.Connection
var connection: Connection { var connection: Connection {
guard isReachableFlagSet else { return .unavailable } guard isReachableFlagSet else { return .unavailable }
// If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi // If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
return .wifi return .wifi
#else #else
var connection = Connection.unavailable var connection = Connection.unavailable
if !isConnectionRequiredFlagSet { if !isConnectionRequiredFlagSet {
connection = .wifi connection = .wifi
} }
if isConnectionOnTrafficOrDemandFlagSet { if isConnectionOnTrafficOrDemandFlagSet {
if !isInterventionRequiredFlagSet { if !isInterventionRequiredFlagSet {
connection = .wifi connection = .wifi
} }
} }
if isOnWWANFlagSet { if isOnWWANFlagSet {
connection = .cellular connection = .cellular
} }
return connection return connection
#endif #endif
} }
var isOnWWANFlagSet: Bool { var isOnWWANFlagSet: Bool {
#if os(iOS) #if os(iOS)
return contains(.isWWAN) return contains(.isWWAN)
#else #else
return false return false
#endif #endif
} }
var isReachableFlagSet: Bool { var isReachableFlagSet: Bool {
return contains(.reachable) return contains(.reachable)
} }
var isConnectionRequiredFlagSet: Bool { var isConnectionRequiredFlagSet: Bool {
return contains(.connectionRequired) return contains(.connectionRequired)
} }
var isInterventionRequiredFlagSet: Bool { var isInterventionRequiredFlagSet: Bool {
return contains(.interventionRequired) return contains(.interventionRequired)
} }
var isConnectionOnTrafficFlagSet: Bool { var isConnectionOnTrafficFlagSet: Bool {
return contains(.connectionOnTraffic) return contains(.connectionOnTraffic)
} }
var isConnectionOnDemandFlagSet: Bool { var isConnectionOnDemandFlagSet: Bool {
return contains(.connectionOnDemand) return contains(.connectionOnDemand)
} }
var isConnectionOnTrafficOrDemandFlagSet: Bool { var isConnectionOnTrafficOrDemandFlagSet: Bool {
return !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty return !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty
} }
var isTransientConnectionFlagSet: Bool { var isTransientConnectionFlagSet: Bool {
return contains(.transientConnection) return contains(.transientConnection)
} }
var isLocalAddressFlagSet: Bool { var isLocalAddressFlagSet: Bool {
return contains(.isLocalAddress) return contains(.isLocalAddress)
} }
var isDirectFlagSet: Bool { var isDirectFlagSet: Bool {
return contains(.isDirect) return contains(.isDirect)
} }
var isConnectionRequiredAndTransientFlagSet: Bool {
return intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection]
}
var description: String { var description: String {
let W = isOnWWANFlagSet ? "W" : "-" let W = isOnWWANFlagSet ? "W" : "-"
let R = isReachableFlagSet ? "R" : "-" let R = isReachableFlagSet ? "R" : "-"
let c = isConnectionRequiredFlagSet ? "c" : "-" let c = isConnectionRequiredFlagSet ? "c" : "-"
let t = isTransientConnectionFlagSet ? "t" : "-" let t = isTransientConnectionFlagSet ? "t" : "-"
let i = isInterventionRequiredFlagSet ? "i" : "-" let i = isInterventionRequiredFlagSet ? "i" : "-"
let C = isConnectionOnTrafficFlagSet ? "C" : "-" let C = isConnectionOnTrafficFlagSet ? "C" : "-"
let D = isConnectionOnDemandFlagSet ? "D" : "-" let D = isConnectionOnDemandFlagSet ? "D" : "-"
let l = isLocalAddressFlagSet ? "l" : "-" let l = isLocalAddressFlagSet ? "l" : "-"
let d = isDirectFlagSet ? "d" : "-" let d = isDirectFlagSet ? "d" : "-"
return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)"
} }
}
/**
`ReachabilityWeakifier` weakly wraps the `Reachability` class
in order to break retain cycles when interacting with CoreFoundation.
CoreFoundation callbacks expect a pair of retain/release whenever an
opaque `info` parameter is provided. These callbacks exist to guard
against memory management race conditions when invoking the callbacks.
#### Race Condition
If we passed `SCNetworkReachabilitySetCallback` a direct reference to our
`Reachability` class without also providing corresponding retain/release
callbacks, then a race condition can lead to crashes when:
- `Reachability` is deallocated on thread X
- A `SCNetworkReachability` callback(s) is already in flight on thread Y
#### Retain Cycle
If we pass `Reachability` to CoreFoundtion while also providing retain/
release callbacks, we would create a retain cycle once CoreFoundation
retains our `Reachability` class. This fixes the crashes and his how
CoreFoundation expects the API to be used, but doesn't play nicely with
Swift/ARC. This cycle would only be broken after manually calling
`stopNotifier()` `deinit` would never be called.
#### ReachabilityWeakifier
By providing both retain/release callbacks and wrapping `Reachability` in
a weak wrapper, we:
- interact correctly with CoreFoundation, thereby avoiding a crash.
See "Memory Management Programming Guide for Core Foundation".
- don't alter the public API of `Reachability.swift` in any way
- still allow for automatic stopping of the notifier on `deinit`.
*/
private class ReachabilityWeakifier {
weak var reachability: Reachability?
init(reachability: Reachability) {
self.reachability = reachability
}
} }

View File

@ -34,40 +34,37 @@ public final class WidgetDataEncoder {
} }
func encode() { func encode() {
if #available(iOS 14, *) { isRunning = true
isRunning = true
flushSharedContainer()
flushSharedContainer() os_log(.debug, log: log, "Starting encoding widget data.")
os_log(.debug, log: log, "Starting encoding widget data.")
DispatchQueue.main.async {
DispatchQueue.main.async { self.encodeWidgetData() { latestData in
self.encodeWidgetData() { latestData in guard let latestData = latestData else {
guard let latestData = latestData else {
self.isRunning = false
return
}
let encodedData = try? JSONEncoder().encode(latestData)
os_log(.debug, log: self.log, "Finished encoding widget data.")
if self.fileExists() {
try? FileManager.default.removeItem(at: self.dataURL!)
os_log(.debug, log: self.log, "Removed widget data from container.")
}
if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) {
os_log(.debug, log: self.log, "Wrote widget data to container.")
WidgetCenter.shared.reloadAllTimelines()
}
self.isRunning = false self.isRunning = false
return
} }
let encodedData = try? JSONEncoder().encode(latestData)
os_log(.debug, log: self.log, "Finished encoding widget data.")
if self.fileExists() {
try? FileManager.default.removeItem(at: self.dataURL!)
os_log(.debug, log: self.log, "Removed widget data from container.")
}
if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) {
os_log(.debug, log: self.log, "Wrote widget data to container.")
WidgetCenter.shared.reloadAllTimelines()
}
self.isRunning = false
} }
} }
} }
@available(iOS 14, *)
private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) { private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) {
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
var groupError: Error? = nil var groupError: Error? = nil

View File

@ -48,9 +48,7 @@ class KeyboardManager {
static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand { static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand {
let selector = NSSelectorFromString(action) let selector = NSSelectorFromString(action)
let keyCommand = UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on) let keyCommand = UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on)
if #available(iOS 15.0, *) { keyCommand.wantsPriorityOverSystemBehavior = true
keyCommand.wantsPriorityOverSystemBehavior = true
}
return keyCommand return keyCommand
} }
@ -67,9 +65,7 @@ private extension KeyboardManager {
return KeyboardManager.createKeyCommand(title: title, action: action, input: input, modifiers: modifiers) return KeyboardManager.createKeyCommand(title: title, action: action, input: input, modifiers: modifiers)
} else { } else {
let keyCommand = UIKeyCommand(input: input, modifierFlags: modifiers, action: NSSelectorFromString(action)) let keyCommand = UIKeyCommand(input: input, modifierFlags: modifiers, action: NSSelectorFromString(action))
if #available(iOS 15.0, *) { keyCommand.wantsPriorityOverSystemBehavior = true
keyCommand.wantsPriorityOverSystemBehavior = true
}
return keyCommand return keyCommand
} }
} }

View File

@ -197,9 +197,7 @@ private extension MasterFeedTableViewCell {
disclosureButton?.tintColor = AppAssets.controlBackgroundColor disclosureButton?.tintColor = AppAssets.controlBackgroundColor
disclosureButton?.imageView?.contentMode = .center disclosureButton?.imageView?.contentMode = .center
disclosureButton?.imageView?.clipsToBounds = false disclosureButton?.imageView?.clipsToBounds = false
if #available(iOS 13.4, *) { disclosureButton?.addInteraction(UIPointerInteraction())
disclosureButton?.addInteraction(UIPointerInteraction())
}
addSubviewAtInit(disclosureButton!) addSubviewAtInit(disclosureButton!)
} }

View File

@ -88,9 +88,7 @@ class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView {
button.tintColor = UIColor.tertiaryLabel button.tintColor = UIColor.tertiaryLabel
button.setImage(AppAssets.disclosureImage, for: .normal) button.setImage(AppAssets.disclosureImage, for: .normal)
button.contentMode = .center button.contentMode = .center
if #available(iOS 13.4, *) { button.addInteraction(UIPointerInteraction())
button.addInteraction(UIPointerInteraction())
}
button.addTarget(self, action: #selector(toggleDisclosure), for: .touchUpInside) button.addTarget(self, action: #selector(toggleDisclosure), for: .touchUpInside)
return button return button
}() }()

View File

@ -19,11 +19,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
private var refreshProgressView: RefreshProgressView? private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var addNewItemButton: UIBarButtonItem! { @IBOutlet weak var addNewItemButton: UIBarButtonItem! {
didSet { didSet {
if #available(iOS 14, *) { addNewItemButton.primaryAction = nil
addNewItemButton.primaryAction = nil
} else {
addNewItemButton.action = #selector(MasterFeedViewController.add(_:))
}
} }
} }
@ -428,37 +424,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
@IBAction func add(_ sender: UIBarButtonItem) { @IBAction func add(_ sender: UIBarButtonItem) {
if #available(iOS 14, *) {
} else {
let title = NSLocalizedString("Add Item", comment: "Add Item")
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
let addFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
let addFeedAction = UIAlertAction(title: addFeedActionTitle, style: .default) { _ in
self.coordinator.showAddFeed()
}
let addWebFolderdActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
let addWebFolderAction = UIAlertAction(title: addWebFolderdActionTitle, style: .default) { _ in
self.coordinator.showAddFolder()
}
alertController.addAction(addFeedAction)
alertController.addAction(addWebFolderAction)
alertController.addAction(cancelAction)
alertController.popoverPresentationController?.barButtonItem = sender
present(alertController, animated: true)
}
} }
@objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) { @objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) {
@ -638,35 +603,32 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
@objc @objc
func configureContextMenu(_: Any? = nil) { func configureContextMenu(_: Any? = nil) {
if #available(iOS 14.0, *) { /*
Context Menu Order:
/* 1. Add Feed
Context Menu Order: 3. Add Folder
1. Add Feed */
3. Add Folder
*/ var menuItems: [UIAction] = []
var menuItems: [UIAction] = [] let addFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
let addFeedAction = UIAction(title: addFeedActionTitle, image: AppAssets.plus) { _ in
let addFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed") self.coordinator.showAddFeed()
let addFeedAction = UIAction(title: addFeedActionTitle, image: AppAssets.plus) { _ in
self.coordinator.showAddFeed()
}
menuItems.append(addFeedAction)
let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
let addWebFolderAction = UIAction(title: addWebFolderActionTitle, image: AppAssets.folderOutlinePlus) { _ in
self.coordinator.showAddFolder()
}
menuItems.append(addWebFolderAction)
let contextMenu = UIMenu(title: NSLocalizedString("Add Item", comment: "Add Item"), image: nil, identifier: nil, options: [], children: menuItems.reversed())
self.addNewItemButton.menu = contextMenu
} }
menuItems.append(addFeedAction)
let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
let addWebFolderAction = UIAction(title: addWebFolderActionTitle, image: AppAssets.folderOutlinePlus) { _ in
self.coordinator.showAddFolder()
}
menuItems.append(addWebFolderAction)
let contextMenu = UIMenu(title: NSLocalizedString("Add Item", comment: "Add Item"), image: nil, identifier: nil, options: [], children: menuItems.reversed())
self.addNewItemButton.menu = contextMenu
} }
func focus() { func focus() {
becomeFirstResponder() becomeFirstResponder()
} }

View File

@ -14,7 +14,6 @@ class MasterTimelineTitleView: UIView {
@IBOutlet weak var label: UILabel! @IBOutlet weak var label: UILabel!
@IBOutlet weak var unreadCountView: MasterTimelineUnreadCountView! @IBOutlet weak var unreadCountView: MasterTimelineUnreadCountView!
@available(iOS 13.4, *)
private lazy var pointerInteraction: UIPointerInteraction = { private lazy var pointerInteraction: UIPointerInteraction = {
UIPointerInteraction(delegate: self) UIPointerInteraction(delegate: self)
}() }()
@ -35,24 +34,18 @@ class MasterTimelineTitleView: UIView {
func buttonize() { func buttonize() {
heightAnchor.constraint(equalToConstant: 40.0).isActive = true heightAnchor.constraint(equalToConstant: 40.0).isActive = true
accessibilityTraits = .button accessibilityTraits = .button
if #available(iOS 13.4, *) { addInteraction(pointerInteraction)
addInteraction(pointerInteraction)
}
} }
func debuttonize() { func debuttonize() {
heightAnchor.constraint(equalToConstant: 40.0).isActive = true heightAnchor.constraint(equalToConstant: 40.0).isActive = true
accessibilityTraits.remove(.button) accessibilityTraits.remove(.button)
if #available(iOS 13.4, *) { removeInteraction(pointerInteraction)
removeInteraction(pointerInteraction)
}
} }
} }
extension MasterTimelineTitleView: UIPointerInteractionDelegate { extension MasterTimelineTitleView: UIPointerInteractionDelegate {
@available(iOS 13.4, *)
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
var rect = self.frame var rect = self.frame
rect.origin.x = rect.origin.x - 10 rect.origin.x = rect.origin.x - 10
@ -60,5 +53,4 @@ extension MasterTimelineTitleView: UIPointerInteractionDelegate {
return UIPointerStyle(effect: .automatic(UITargetedPreview(view: self)), shape: .roundedRect(rect)) return UIPointerStyle(effect: .automatic(UITargetedPreview(view: self)), shape: .roundedRect(rect))
} }
} }

View File

@ -79,9 +79,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// Configure the table // Configure the table
tableView.dataSource = dataSource tableView.dataSource = dataSource
if #available(iOS 15.0, *) { tableView.isPrefetchingEnabled = false
tableView.isPrefetchingEnabled = false
}
numberOfTextLines = AppDefaults.shared.timelineNumberOfLines numberOfTextLines = AppDefaults.shared.timelineNumberOfLines
iconSize = AppDefaults.shared.timelineIconSize iconSize = AppDefaults.shared.timelineIconSize
resetEstimatedRowHeight() resetEstimatedRowHeight()
@ -104,13 +103,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
// Disable swipe back on iPad Mice // Disable swipe back on iPad Mice
if #available(iOS 13.4, *) { guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else {
guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else { return
return
}
gesture.allowedScrollTypesMask = []
} }
gesture.allowedScrollTypesMask = []
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {