diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index fbb1d35e..bf4b51f7 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -82,7 +82,6 @@ 69ACD6F32AFD133A0021127B /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6122AFD10BE00C8E8D9 /* UIImageExtensions.swift */; }; 69ACD6F42AFD133A0021127B /* UILabelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6192AFD10BE00C8E8D9 /* UILabelExtensions.swift */; }; 69ACD6F52AFD133A0021127B /* UINavigationControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6132AFD10BE00C8E8D9 /* UINavigationControllerExtensions.swift */; }; - 69ACD6F62AFD133A0021127B /* UISearchBarExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */; }; 69ACD6F72AFD133A0021127B /* UIStackViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A5FB2AFD10BE00C8E8D9 /* UIStackViewExtensions.swift */; }; 69ACD6F82AFD133A0021127B /* UIView+Spacer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A6092AFD10BE00C8E8D9 /* UIView+Spacer.swift */; }; 69ACD6F92AFD133A0021127B /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E0A60E2AFD10BE00C8E8D9 /* UIViewController+Children.swift */; }; @@ -139,6 +138,8 @@ 69FA5FC223C8690A00B44BCD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 69FA5FC123C8690A00B44BCD /* LaunchScreen.storyboard */; }; 6A72B2222B1A15AC00A59EDD /* BackGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A72B2212B1A15AC00A59EDD /* BackGesture.swift */; }; 88EDD90425B8252E00207987 /* GradientViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88EDD90325B8252E00207987 /* GradientViewController.swift */; }; + A2035B6FA7B449CD92033D8B /* View+FrameSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3212C27C546A38A962094 /* View+FrameSize.swift */; }; + C3417AD121F141AA8F873477 /* SwiftUILayoutGuides.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0AE9942CD0E454B948FF15A /* SwiftUILayoutGuides.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -271,7 +272,6 @@ 69DF225A2B459CC50025C555 /* PushNotifications.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PushNotifications.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 69DF22792B459D0D0025C555 /* PushManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushManager.swift; sourceTree = ""; }; 69DF227A2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationSettingsExtensions.swift; sourceTree = ""; }; - 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISearchBarExtensions.swift; sourceTree = ""; }; 69E0A5F72AFD10BE00C8E8D9 /* UserDefault.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; }; 69E0A5F82AFD10BE00C8E8D9 /* ArrayExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; 69E0A5F92AFD10BE00C8E8D9 /* ACKategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ACKategories.h; sourceTree = ""; }; @@ -359,6 +359,8 @@ 69FA5FC123C8690A00B44BCD /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 6A72B2212B1A15AC00A59EDD /* BackGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackGesture.swift; sourceTree = ""; }; 88EDD90325B8252E00207987 /* GradientViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientViewController.swift; sourceTree = ""; }; + D0E3212C27C546A38A962094 /* View+FrameSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+FrameSize.swift"; sourceTree = ""; }; + E0AE9942CD0E454B948FF15A /* SwiftUILayoutGuides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUILayoutGuides.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -533,7 +535,6 @@ 69E0A61F2AFD10BE00C8E8D9 /* UIApplicationExtensions.swift */, 69E0A6192AFD10BE00C8E8D9 /* UILabelExtensions.swift */, 69E0A6132AFD10BE00C8E8D9 /* UINavigationControllerExtensions.swift */, - 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */, 69E0A5FB2AFD10BE00C8E8D9 /* UIStackViewExtensions.swift */, 69E0A6092AFD10BE00C8E8D9 /* UIView+Spacer.swift */, 69E0A60E2AFD10BE00C8E8D9 /* UIViewController+Children.swift */, @@ -654,6 +655,8 @@ isa = PBXGroup; children = ( 69BC15942CF8A42700A4B55F /* View+Frame.swift */, + D0E3212C27C546A38A962094 /* View+FrameSize.swift */, + E0AE9942CD0E454B948FF15A /* SwiftUILayoutGuides.swift */, 69246CFA2BDFE77400AB31A1 /* SwiftUIColorsTheme.swift */, 69E0A6042AFD10BE00C8E8D9 /* FontModifier.swift */, 693B39B62BF2359B00DF7C5E /* ACKHostingController.swift */, @@ -1296,9 +1299,10 @@ 69ACD6F22AFD133A0021127B /* UIDeviceExtensions.swift in Sources */, 69ACD6F32AFD133A0021127B /* UIImageExtensions.swift in Sources */, 69BC15952CF8A42C00A4B55F /* View+Frame.swift in Sources */, + A2035B6FA7B449CD92033D8B /* View+FrameSize.swift in Sources */, + C3417AD121F141AA8F873477 /* SwiftUILayoutGuides.swift in Sources */, 69ACD6F42AFD133A0021127B /* UILabelExtensions.swift in Sources */, 69ACD6F52AFD133A0021127B /* UINavigationControllerExtensions.swift in Sources */, - 69ACD6F62AFD133A0021127B /* UISearchBarExtensions.swift in Sources */, 69ACD6F72AFD133A0021127B /* UIStackViewExtensions.swift in Sources */, 69ACD6F82AFD133A0021127B /* UIView+Spacer.swift in Sources */, 693B39B72BF2359B00DF7C5E /* ACKHostingController.swift in Sources */, diff --git a/ACKategoriesExample/Screens/UIControl blocks/UIControlBlocksViewController.swift b/ACKategoriesExample/Screens/UIControl blocks/UIControlBlocksViewController.swift index 557ed65d..7d79c59f 100644 --- a/ACKategoriesExample/Screens/UIControl blocks/UIControlBlocksViewController.swift +++ b/ACKategoriesExample/Screens/UIControl blocks/UIControlBlocksViewController.swift @@ -23,9 +23,7 @@ final class UIControlBlocksViewController: BaseViewControllerNoVM { let button = UIButton(type: .system) button.setTitle("Tap here!", for: .normal) - if #available(iOS 13.0, *) { - button.setBackgroundImage(UIColor.systemGray6.image(), for: .normal) - } + button.setBackgroundImage(UIColor.systemGray6.image(), for: .normal) view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false diff --git a/CHANGELOG.md b/CHANGELOG.md index dba6307b..4d3b324e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ ## Next +- Add `SwiftUILayoutGuides.swift` and `View+FrameSize.swift` to the Xcode project ([#159](https://github.com/AckeeCZ/ACKategories/pull/159), kudos to @Jidoml) + - Both files had been SPM-only since 6.16.0, where the changelog announced them. The Carthage xcframework therefore shipped without `WithLayoutMargins`, `fitToReadableContentWidth()`, `fitToLayoutMarginsWidth()`, `measureLayoutMargins()`, the `layoutMarginsInsets`/`readableContentInsets` environment values and `View.frame(size:)` — and no `xcodebuild` job ever compiled them for iOS, macOS, watchOS or tvOS + - Fix the 10 DocC warnings this exposed in `SwiftUILayoutGuides.swift` — they only appeared because the documentation build had never seen the file. The two environment values become plain code spans rather than symbol links, since DocC resolves extensions of SwiftUI types under an internal module name that Apple may rename; and the notes pointing at `FitReadableContentWidth`/`FitLayoutMarginsWidth` are gone, as neither type ever existed +- Remove deprecated `UISearchBar.textField` ([#159](https://github.com/AckeeCZ/ACKategories/pull/159), kudos to @Jidoml) + - ⚠️ **BREAKING CHANGE:** use `UISearchBar.searchTextField` instead — it has been the recommended replacement since iOS 13 and gives you `UISearchTextField` rather than a plain `UITextField` +- Remove availability checks made dead by the iOS 15 floor ([#159](https://github.com/AckeeCZ/ACKategories/pull/159), kudos to @Jidoml) + - Drop 52 `@available` attributes and unwrap 5 `if #available` runtime branches + - Back `UserDefault.subject` with a stored property seeded in `init` instead of `objc_getAssociatedObject` + - The associated-object accessor was a check-then-act race: two threads racing the first access each created a subject, the loser's instance was dropped from the association table and its subscribers were silently orphaned. The subject is now created exactly once, so that initialization race is gone — `UserDefault` itself is still not synchronized for concurrent access + - `$value` now snapshots the persisted value when the wrapper is initialized rather than on first subscription or write. Writes to the same key made outside the wrapper between `init` and the first subscription are no longer reflected in the publisher's replayed value — they never were after it. `wrappedValue` still reads `UserDefaults` on every access + - Decode errors on corrupted data are therefore also reported to `errorLogger` during initialization + - Stop publishing `UserDefault` values whose write to `UserDefaults` failed + - When encoding threw, the setter logged the error and sent the new value anyway, so subscribers held a value `wrappedValue` would never return and that vanished on the next launch. `$value` now stays silent when nothing was persisted - Raise deployment targets to iOS 15, macOS 12, tvOS 15 and watchOS 9, required by Xcode 27 ([#158](https://github.com/AckeeCZ/ACKategories/pull/158), kudos to @Jidoml) - Xcode 27 refuses to build below these versions; note the watchOS floor is 9.0, not 8.0 - Repair CI after `macos-latest` runner image drift ([#158](https://github.com/AckeeCZ/ACKategories/pull/158), kudos to @Jidoml) @@ -28,7 +41,7 @@ - Add SwiftUI extension `View+FrameSize` ([#153](https://github.com/AckeeCZ/ACKategories/pull/153), kudos to @lukashromadnik) - Add `readSize` and `readFrame` to SwiftUI views ([#150](https://github.com/AckeeCZ/ACKategories/pull/150), kudos to @olejnjak) -- Add `WithLayoutMargins` to SwiftUI extensions ([#150](https://github.com/AckeeCZ/ACKategories/pull/151), kudos to @komkovla) +- Add `WithLayoutMargins` to SwiftUI extensions ([#151](https://github.com/AckeeCZ/ACKategories/pull/151), kudos to @komkovla) ## 6.15.0 diff --git a/README.md b/README.md index f486f972..0914d2b0 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ button.on(.touchUpInside) { sender in ... } ``` + You can also omit the event to use the primary action (`.primaryActionTriggered`): `button.on { sender in ... }`. ### String diff --git a/Sources/ACKategories/Combine+Concurrency.swift b/Sources/ACKategories/Combine+Concurrency.swift index eda736d1..41c50480 100644 --- a/Sources/ACKategories/Combine+Concurrency.swift +++ b/Sources/ACKategories/Combine+Concurrency.swift @@ -1,6 +1,5 @@ import Combine -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public extension Future { /// Run async operation as Publisher /// - Parameter operation: Operation to be run @@ -33,7 +32,6 @@ public extension Future { } } -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public extension AnyPublisher { /// Run async operation as Publisher /// - Parameter operation: Operation to be run diff --git a/Sources/ACKategories/EdgeInsetsExtensions.swift b/Sources/ACKategories/EdgeInsetsExtensions.swift index ed0cc495..cad07909 100644 --- a/Sources/ACKategories/EdgeInsetsExtensions.swift +++ b/Sources/ACKategories/EdgeInsetsExtensions.swift @@ -1,6 +1,5 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) public extension SwiftUI.EdgeInsets { static var zero: Self { .init(0) } diff --git a/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index 3d9af9bd..1ca21219 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -1,10 +1,6 @@ import Combine import Foundation -private enum Keys { - static var subject = UInt8(0) -} - /// A type safe property wrapper to set and get values from UserDefaults with support for defaults values. /// /// Usage: @@ -18,14 +14,15 @@ private enum Keys { public final class UserDefault { private let key: String private let defaultValue: Value - private var userDefaults: UserDefaults + private let userDefaults: UserDefaults private let errorLogger: ((Error) -> Void)? + private let subject: CurrentValueSubject /// - Parameters: /// - key: Key for which the value should be saved /// - default: Default value to be used /// - userDefaults: `UserDefaults` where value should be saved into. Default is `UserDefaults.standard` - /// - errorLogger: Closure that is triggered with error from encoding/decoding values from setter/getter + /// - errorLogger: Closure that is triggered with error from encoding/decoding values from setter/getter and from the initial read in `init` public init( _ key: String, `default`: Value, @@ -36,29 +33,17 @@ public final class UserDefault { self.defaultValue = `default` self.userDefaults = userDefaults self.errorLogger = errorLogger + self.subject = CurrentValueSubject( + Self.storedValue(forKey: key, in: userDefaults, default: `default`, errorLogger: errorLogger) + ) } public var wrappedValue: Value { get { - // Check if `Value` is supported by default by `UserDefaults` - if Value.self as? PropertyListValue.Type != nil { - return userDefaults.object(forKey: key) as? Value ?? defaultValue - } else { - guard let data = userDefaults.object(forKey: key) as? Data else { return defaultValue } - let decoder = JSONDecoder() - // Encoding root-level `singleValueContainer` fails on iOS <= 12.0 - // Thus we always encode/decode it into array, so it has a root object - // Related issue: https://github.com/AckeeCZ/ACKategories/issues/89 - do { - return try decoder.decode([Value].self, from: data).first ?? defaultValue - } catch { - errorLogger?(error) - return defaultValue - } - } + Self.storedValue(forKey: key, in: userDefaults, default: defaultValue, errorLogger: errorLogger) } set { - if Value.self as? PropertyListValue.Type != nil { + if Value.self is PropertyListValue.Type { userDefaults.set(newValue, forKey: key) } else { let encoder = JSONEncoder() @@ -67,30 +52,45 @@ public final class UserDefault { userDefaults.set(data, forKey: key) } catch { errorLogger?(error) + // Nothing was persisted, so publishing would leave subscribers + // holding a value `wrappedValue` never returns + return } } - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { - subject.send(newValue) - } + subject.send(newValue) } } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public var projectedValue: AnyPublisher { subject.eraseToAnyPublisher() } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) - // cannot have stored property with limited availability, cannot be lazy since Xcode 14 - private var subject: CurrentValueSubject { - if let subject = objc_getAssociatedObject(self, &Keys.subject) as? CurrentValueSubject { - return subject + /// Reads the persisted value for `key`, falling back to `default` when it is missing or cannot be decoded. + /// + /// Static so `init` can seed `subject` before `self` is fully initialized. + private static func storedValue( + forKey key: String, + in userDefaults: UserDefaults, + `default` defaultValue: Value, + errorLogger: ((Error) -> Void)? + ) -> Value { + // Check if `Value` is supported by default by `UserDefaults` + if Value.self is PropertyListValue.Type { + return userDefaults.object(forKey: key) as? Value ?? defaultValue + } else { + guard let data = userDefaults.object(forKey: key) as? Data else { return defaultValue } + let decoder = JSONDecoder() + // Values are wrapped in an array so the JSON has a root object. + // Do not unwrap this — it would break decoding of everything already + // persisted by earlier versions. See https://github.com/AckeeCZ/ACKategories/issues/89 + do { + return try decoder.decode([Value].self, from: data).first ?? defaultValue + } catch { + errorLogger?(error) + return defaultValue + } } - - let subject = CurrentValueSubject(wrappedValue) - objc_setAssociatedObject(self, &Keys.subject, subject, .OBJC_ASSOCIATION_RETAIN) - return subject } } diff --git a/Sources/ACKategories/PublisherExtensions.swift b/Sources/ACKategories/PublisherExtensions.swift index ac384ff4..cbc571be 100644 --- a/Sources/ACKategories/PublisherExtensions.swift +++ b/Sources/ACKategories/PublisherExtensions.swift @@ -1,6 +1,5 @@ import Combine -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Publisher where Failure == Never { /// Weakifies `Root` object and assigns `Output` to `keyPath` on `Root`. public func weakAssign(to keyPath: ReferenceWritableKeyPath, on object: Root) -> AnyCancellable { @@ -10,14 +9,12 @@ extension Publisher where Failure == Never { } } -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Publisher { public func flatMapLatest(_ transform: @escaping (Self.Output) -> T) -> Publishers.SwitchToLatest> { map(transform).switchToLatest() } } -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Publishers { /// A publisher created by applying the zip function to an arbitrary number of upstream publishers. public struct ZipMany: Publisher where Upstream: Publisher { diff --git a/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift b/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift index 65f029c4..7b22c5a4 100644 --- a/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift +++ b/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift @@ -2,7 +2,6 @@ import os.log import SwiftUI -@available(iOS 13.0, tvOS 13.0, *) open class ACKHostingController: UIHostingController { /// Navigation bar is shown/hidden in viewWillAppear according to this flag public var hasNavigationBar = true diff --git a/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift b/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift index be6e1611..5ea4f7fb 100644 --- a/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift +++ b/Sources/ACKategories/SwiftUIExtensions/FontModifier.swift @@ -2,7 +2,6 @@ import SwiftUI import UIKit -@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) public extension View { /// Sets font and line height for text in this view. /// diff --git a/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift b/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift index eb008c9b..b7c6e1aa 100644 --- a/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift +++ b/Sources/ACKategories/SwiftUIExtensions/SwiftUIColorsTheme.swift @@ -16,7 +16,6 @@ import UIKit /// ``` /// /// Then you can use `Color.theme.primary` in SwiftUI -@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) @dynamicMemberLookup public struct SwiftUIColorsTheme { public subscript(dynamicMember keyPath: KeyPath, UIColor>) -> SwiftUI.Color { @@ -25,7 +24,6 @@ public struct SwiftUIColorsTheme { } } -@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) public extension Color { /// Namespace for bridged ``Theme`` colors from `Theme` extension, see ``SwiftUIColorsTheme``. static let theme = SwiftUIColorsTheme() diff --git a/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift b/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift index 17ed5f47..fda01254 100644 --- a/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift +++ b/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift @@ -1,13 +1,12 @@ import SwiftUI // Source: https://github.com/tgrapperon/swiftui-layout-guides -/// This view populates its content's ``layoutMarginsInsets`` and ``readableContentInsets``. -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +/// This view populates its content's `layoutMarginsInsets` and `readableContentInsets`. public struct WithLayoutMargins: View where Content: View { let content: (EdgeInsets) -> Content - /// Initialize a ``WithLayoutMargins`` view, populating its content's ``layoutMarginsInsets`` - /// and ``readableContentInsets``. + /// Initialize a ``WithLayoutMargins`` view, populating its content's `layoutMarginsInsets` + /// and `readableContentInsets`. /// /// - Parameter content: A closure that builds a `Content` view from the layout /// margins provided in the form of an `EdgeInsets` argument. @@ -15,8 +14,8 @@ public struct WithLayoutMargins: View where Content: View { self.content = content } - /// Initialize a ``WithLayoutMargins`` view, populating its content's ``layoutMarginsInsets`` - /// and ``readableContentInsets``. + /// Initialize a ``WithLayoutMargins`` view, populating its content's `layoutMarginsInsets` + /// and `readableContentInsets`. /// /// - Parameter content: A closure that builds a `Content` view. public init(@ViewBuilder content: @escaping () -> Content) { @@ -37,7 +36,6 @@ public struct WithLayoutMargins: View where Content: View { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) internal struct FitLayoutGuidesWidth: ViewModifier { enum Kind { case layoutMargins @@ -81,15 +79,12 @@ internal struct FitLayoutGuidesWidth: ViewModifier { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) extension View { /// Use this modifier to make the view fit the readable content width. /// /// - Parameter alignment: The `Alignment` to use when the view is smaller than /// the readable content width. /// - Note: You don't have to wrap this view inside a ``WithLayoutMargins`` view. - /// - Note: This modifier is equivalent to wrapping the view inside a - /// ``FitReadableContentWidth`` view. public func fitToReadableContentWidth(alignment: Alignment = .center) -> some View { self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .readableContent)) } @@ -99,12 +94,10 @@ extension View { /// - Parameter alignment: The `Alignment` to use when the view is smaller than /// the readable content width. /// - Note: You don't have to wrap this view inside a ``WithLayoutMargins`` view. - /// - Note: This modifier is equivalent to wrapping the view inside a - /// ``FitLayoutMarginsWidth`` view. public func fitToLayoutMarginsWidth(alignment: Alignment = .center) -> some View { self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .layoutMargins)) } - /// Use this modifier to populate the ``layoutMarginsInsets`` and ``readableContentInsets`` + /// Use this modifier to populate the `layoutMarginsInsets` and `readableContentInsets` /// for the target view. /// /// - Note: You don't have to wrap this view inside a ``WithLayoutMargins`` view. @@ -113,17 +106,14 @@ extension View { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) private struct LayoutMarginsGuidesKey: EnvironmentKey { static var defaultValue: EdgeInsets { .init() } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) private struct ReadableContentGuidesKey: EnvironmentKey { static var defaultValue: EdgeInsets { .init() } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) extension EnvironmentValues { /// The `EdgeInsets` corresponding to the layout margins of the nearest /// ``WithLayoutMargins``'s content. @@ -140,7 +130,6 @@ extension EnvironmentValues { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) internal struct LayoutGuidesModifier: ViewModifier { @State var layoutMarginsInsets: EdgeInsets = .init() @State var readableContentInsets: EdgeInsets = .init() @@ -167,7 +156,6 @@ internal struct LayoutGuidesModifier: ViewModifier { #if os(iOS) || os(tvOS) import UIKit -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) internal struct LayoutGuides: UIViewRepresentable { let onLayoutMarginsGuideChange: (EdgeInsets) -> Void let onReadableContentGuideChange: (EdgeInsets) -> Void @@ -258,7 +246,6 @@ internal struct LayoutGuides: UIViewRepresentable { #endif #if DEBUG -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) private struct Cell: View { var value: String var body: some View { @@ -273,7 +260,6 @@ private struct Cell: View { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) private struct ListTest: View { var body: some View { List { @@ -284,7 +270,6 @@ private struct ListTest: View { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) private struct ScrollViewTest: View { var body: some View { ScrollView { @@ -298,7 +283,7 @@ private struct ScrollViewTest: View { } #if os(iOS) -@available(iOS 16.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) +@available(iOS 16.0, *) internal struct SwiftUILayoutGuides_Previews: PreviewProvider { static func sample(_ title: String, _ content: () -> Content) -> some View where Content: View { diff --git a/Sources/ACKategories/SwiftUIExtensions/View+Frame.swift b/Sources/ACKategories/SwiftUIExtensions/View+Frame.swift index 83f4714d..7676b952 100644 --- a/Sources/ACKategories/SwiftUIExtensions/View+Frame.swift +++ b/Sources/ACKategories/SwiftUIExtensions/View+Frame.swift @@ -1,6 +1,5 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) public extension View { func readSize( onChange: @escaping (CGSize) -> Void @@ -27,7 +26,6 @@ private struct FramePreferenceKey: PreferenceKey { static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) #Preview { Text("Hello, World!") .readSize { diff --git a/Sources/ACKategories/SwiftUIExtensions/View+FrameSize.swift b/Sources/ACKategories/SwiftUIExtensions/View+FrameSize.swift index e1433aa3..ccc1455a 100644 --- a/Sources/ACKategories/SwiftUIExtensions/View+FrameSize.swift +++ b/Sources/ACKategories/SwiftUIExtensions/View+FrameSize.swift @@ -1,6 +1,5 @@ import SwiftUI -@available(iOS 13, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension View { public func frame(size: CGSize) -> some View { diff --git a/Sources/ACKategories/UI/UISearchBarExtensions.swift b/Sources/ACKategories/UI/UISearchBarExtensions.swift deleted file mode 100644 index 5a7ede16..00000000 --- a/Sources/ACKategories/UI/UISearchBarExtensions.swift +++ /dev/null @@ -1,17 +0,0 @@ -#if canImport(UIKit) && !os(watchOS) && !os(tvOS) -import UIKit - -public extension UISearchBar { - /// Shorthand for contained textField - /// - /// On iOS 13+ this just calls `searchTextField`, and if you're targeting iOS 13+, - /// you should use that as you get `UISearchTextField` instead of just `UITextField`. - @available(iOS, deprecated: 13, renamed: "searchTextField") - var textField: UITextField! { - if #available(iOS 13, *) { - return searchTextField - } - return value(forKey: "searchField") as? UITextField - } -} -#endif diff --git a/Sources/ACKategories/UIColorExtensions.swift b/Sources/ACKategories/UIColorExtensions.swift index 41f7e4bd..b3f61b20 100644 --- a/Sources/ACKategories/UIColorExtensions.swift +++ b/Sources/ACKategories/UIColorExtensions.swift @@ -110,27 +110,19 @@ public extension UIColor { UIGraphicsBeginImageContext(size) let context = UIGraphicsGetCurrentContext() - if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) { - // if self has alpha < 1 then this alpha is correctly applied on content - // during context rendering, which is right behavior. But after that in - // `.withTintColor(self)` below, this alpha is applied again and resulting - // image is more transparent then it should be. So use any non-transparent - // color here to draw the content and set resulting color with alpha only below. - context?.setFillColor(UIColor.white.cgColor) - } else { - context?.setFillColor(self.cgColor) - } + // if self has alpha < 1 then this alpha is correctly applied on content + // during context rendering, which is right behavior. But after that in + // `.withTintColor(self)` below, this alpha is applied again and resulting + // image is more transparent then it should be. So use any non-transparent + // color here to draw the content and set resulting color with alpha only below. + context?.setFillColor(UIColor.white.cgColor) context?.fill(rect) // swiftlint:disable:next force_unwrapping let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() - if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) { - return image.withTintColor(self) - } else { - return image - } + return image.withTintColor(self) } } #endif diff --git a/Sources/ACKategories/UIDeviceExtensions.swift b/Sources/ACKategories/UIDeviceExtensions.swift index 3520985e..74d7f46f 100644 --- a/Sources/ACKategories/UIDeviceExtensions.swift +++ b/Sources/ACKategories/UIDeviceExtensions.swift @@ -13,7 +13,6 @@ extension UIDevice { } /// Return **true** if device is Mac (Catalyst) - @available(iOS 14.0, tvOS 14.0, *) public var isMac: Bool { userInterfaceIdiom == .mac } diff --git a/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift b/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift index 954e6582..b24376c0 100644 --- a/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift +++ b/Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift @@ -1,6 +1,5 @@ import Foundation -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public protocol MinBuildNumberFetcher { var minBuildNumber: Int { get async throws } } diff --git a/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift b/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift index 4fbee4aa..b5c7a10b 100644 --- a/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift +++ b/Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift @@ -1,11 +1,9 @@ import Foundation -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public protocol VersionUpdateManaging { var updateRequired: Bool { get async } } -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public final class VersionUpdateManager: VersionUpdateManaging { public var updateRequired: Bool { get async { diff --git a/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift b/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift index 202941aa..21233647 100644 --- a/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift +++ b/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift @@ -1,7 +1,6 @@ import Foundation import Networking -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public final class APIService_Mock: APIServicing { public init() { diff --git a/Sources/ACKategoriesTesting/Networking/NetworkMock.swift b/Sources/ACKategoriesTesting/Networking/NetworkMock.swift index 7aaeefc0..4a7b41c6 100644 --- a/Sources/ACKategoriesTesting/Networking/NetworkMock.swift +++ b/Sources/ACKategoriesTesting/Networking/NetworkMock.swift @@ -1,7 +1,6 @@ import Foundation import Networking -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public final class Network_Mock: Network { public var requestBody: (URLRequest) async throws -> HTTPResponse = { _ in .test() } diff --git a/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift b/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift index 7892c4ca..e82b36c2 100644 --- a/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift +++ b/Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift @@ -1,6 +1,5 @@ import ACKategories -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public final class VersionUpdateFetcher_Mock: MinBuildNumberFetcher { public var minBuildNumber = 0 diff --git a/Sources/Networking/APIService.swift b/Sources/Networking/APIService.swift index 3f5170cb..6e18c9e0 100644 --- a/Sources/Networking/APIService.swift +++ b/Sources/Networking/APIService.swift @@ -1,6 +1,5 @@ import Foundation -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public final class APIService: APIServicing { /// Error thrown when response status code is unexpected public struct UnexpectedStatusCodeError: Error { diff --git a/Sources/Networking/APIServicing.swift b/Sources/Networking/APIServicing.swift index ead9797f..00e4d18c 100644 --- a/Sources/Networking/APIServicing.swift +++ b/Sources/Networking/APIServicing.swift @@ -1,7 +1,6 @@ import Foundation /// Protocol wrapping objects that perform network requests -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public protocol APIServicing { /// Send given `URLRequest` /// - Parameter request: Request to be sent @@ -26,7 +25,6 @@ public protocol APIServicing { } /// Protocol wrapping interceptors that can modify requests before they are sent -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public protocol RequestInterceptor { /// Intercept request sent by service /// - Parameters: @@ -39,7 +37,6 @@ public protocol RequestInterceptor { } /// Protocol wrapping interceptors that can modify responses before they are returned from API service -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public protocol ResponseInterceptor { /// Intercept response that was returned to service /// - Parameters: diff --git a/Sources/Networking/Networking.swift b/Sources/Networking/Networking.swift index 6e6e53ad..f2c6f806 100644 --- a/Sources/Networking/Networking.swift +++ b/Sources/Networking/Networking.swift @@ -1,7 +1,6 @@ import Foundation /// Protocol wrapping raw network requests, basically URLSession -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public protocol Network { /// Send given request /// - Parameter request: Request to be sent @@ -9,7 +8,6 @@ public protocol Network { func request(_ request: URLRequest) async throws -> HTTPResponse } -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 12.0, *) extension URLSession: Network { public func request(_ request: URLRequest) async throws -> HTTPResponse { let (data, response) = try await data(for: request) diff --git a/Sources/Networking/OAuthInterceptor.swift b/Sources/Networking/OAuthInterceptor.swift index 1c4bae25..789778fa 100644 --- a/Sources/Networking/OAuthInterceptor.swift +++ b/Sources/Networking/OAuthInterceptor.swift @@ -1,7 +1,6 @@ import Foundation /// Interceptor that is ready to solve token refresh -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) public final actor OAuthInterceptor: ResponseInterceptor { /// Enum for results when checking if request current auth data public enum UsedCurrentAuthData { diff --git a/Sources/PushNotifications/PushManager.swift b/Sources/PushNotifications/PushManager.swift index 0d4b12c5..7023ebb5 100644 --- a/Sources/PushNotifications/PushManager.swift +++ b/Sources/PushNotifications/PushManager.swift @@ -1,7 +1,6 @@ import Foundation import UserNotifications -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) public protocol PushManaging { var actions: PushManagingActions { get } @@ -9,7 +8,6 @@ public protocol PushManaging { var currentNotificationSettings: UNNotificationSettings? { get } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) public protocol PushManagingActions { func start() func requestPermission(options: UNAuthorizationOptions) async @@ -23,12 +21,10 @@ public protocol PushManagingActions { #endif } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) public extension PushManaging where Self: PushManagingActions { var actions: PushManagingActions { self } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) @objc(ACKPushManager) public final class PushManager: NSObject, PushManaging, PushManagingActions { public private(set) lazy var notificationSettings = AsyncStream { continuation in @@ -108,7 +104,6 @@ public final class PushManager: NSObject, PushManaging, PushManagingActions { #endif } -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) extension PushManager: UNUserNotificationCenterDelegate { public func userNotificationCenter( _ center: UNUserNotificationCenter, diff --git a/Sources/PushNotifications/UNNotificationSettingsExtensions.swift b/Sources/PushNotifications/UNNotificationSettingsExtensions.swift index 07492ae8..184c2541 100644 --- a/Sources/PushNotifications/UNNotificationSettingsExtensions.swift +++ b/Sources/PushNotifications/UNNotificationSettingsExtensions.swift @@ -1,6 +1,5 @@ import UserNotifications -@available(iOS 13.0, macOS 10.15, *) public extension UNNotificationSettings { var allowedPresentationOptions: UNNotificationPresentationOptions { var options = [(UNNotificationSetting, UNNotificationPresentationOptions)]() diff --git a/Tests/ACKategoriesTests/EdgeInsetsTests.swift b/Tests/ACKategoriesTests/EdgeInsetsTests.swift index 554e710b..9516013b 100644 --- a/Tests/ACKategoriesTests/EdgeInsetsTests.swift +++ b/Tests/ACKategoriesTests/EdgeInsetsTests.swift @@ -2,7 +2,6 @@ import ACKategories import SwiftUI import XCTest -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) final class EdgeInsetsTests: XCTestCase { func test_init_size() { XCTAssertEqual( diff --git a/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift b/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift index 992d739e..9289ea23 100644 --- a/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift +++ b/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift @@ -3,7 +3,6 @@ import Foundation import XCTest import ACKategories -@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) final class UserDefaultTests: XCTestCase { private var subject: MyUserDefaultProvider! private var userDefaults: UserDefaults! @@ -74,6 +73,27 @@ final class UserDefaultTests: XCTestCase { subject.hasSeen = true XCTAssertEqual([false, true], values) } + + func testFailedWriteIsNotPublished() { + // Given + let persisted = FloatingValue(ratio: 0.5) + subject.ratio = persisted + + var published = [FloatingValue?]() + subject.$ratio.sink { published.append($0) }.store(in: &cancellables) + + // When – `JSONEncoder` rejects non-conforming floats, so this write cannot reach `UserDefaults` + subject.ratio = FloatingValue(ratio: .infinity) + + // Then – nothing was published, and the getter still agrees with the publisher + XCTAssertEqual(published, [persisted]) + XCTAssertEqual(subject.ratio, persisted) + + // And a write that does succeed is still published + let next = FloatingValue(ratio: 0.75) + subject.ratio = next + XCTAssertEqual(published, [persisted, next]) + } } private struct MyUserDefaultProvider { @@ -85,9 +105,16 @@ private struct MyUserDefaultProvider { @UserDefault("codable_value", userDefaults: UserDefaults(suiteName: "my_user_default")!) var codableValue: CodableValue? + + @UserDefault("ratio", userDefaults: UserDefaults(suiteName: "my_user_default")!) + var ratio: FloatingValue? } private struct CodableValue: Codable, Equatable { let stringValue: String let intValue: Int } + +private struct FloatingValue: Codable, Equatable { + let ratio: Double +} diff --git a/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift index 99e423e5..087edbc2 100644 --- a/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift +++ b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift @@ -2,7 +2,6 @@ import ACKategories import ACKategoriesTesting import XCTest -@available(tvOS 13.0, iOS 13.0, watchOS 6.0, macOS 10.15, *) final class VersionUpdateManager_Tests: XCTestCase { func test_minBuildNumber_lower() async throws { let fetcher = VersionUpdateFetcher_Mock() diff --git a/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift b/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift index e5a599cb..c8f74089 100644 --- a/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift +++ b/Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift @@ -3,7 +3,6 @@ import Networking import XCTest @MainActor -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) final class APIService_OAuthInterceptor_IntegrationTests: XCTestCase { private var network: Network_Mock! diff --git a/Tests/NetworkingTests/APIService_Tests.swift b/Tests/NetworkingTests/APIService_Tests.swift index 0cb95bce..5b310636 100644 --- a/Tests/NetworkingTests/APIService_Tests.swift +++ b/Tests/NetworkingTests/APIService_Tests.swift @@ -3,7 +3,6 @@ import Networking import XCTest @MainActor -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) final class APIService_Tests: XCTestCase { private var network: Network_Mock! diff --git a/Tests/NetworkingTests/OAuthInterceptor_Tests.swift b/Tests/NetworkingTests/OAuthInterceptor_Tests.swift index ec49d091..88795290 100644 --- a/Tests/NetworkingTests/OAuthInterceptor_Tests.swift +++ b/Tests/NetworkingTests/OAuthInterceptor_Tests.swift @@ -3,7 +3,6 @@ import Networking import XCTest @MainActor -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) final class OAuthInterceptor_Tests: XCTestCase { private final class UnexpectedCall: Error { init() { XCTFail("Should not be called") }