From f3232f9f20789763aea6039e9407d83b716f2467 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Mon, 7 Sep 2026 23:43:53 +0200 Subject: [PATCH 01/18] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20SPM=20platfor?= =?UTF-8?q?ms=20to=20iOS=2015,=20macOS=2012,=20tvOS=2015,=20watchOS=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index e6f3765..d8211c7 100644 --- a/Package.swift +++ b/Package.swift @@ -4,10 +4,10 @@ import PackageDescription let package = Package( name: "ACKategories", platforms: [ - .iOS(.v12), - .macOS(.v10_13), - .watchOS(.v5), - .tvOS(.v12), + .iOS(.v15), + .macOS(.v12), + .watchOS(.v9), + .tvOS(.v15), ], products: [ .library(name: "ACKategories", targets: ["ACKategories"]), From 103f746f452ce4e6ac16c2969d2fc504aadc1a18 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Mon, 7 Sep 2026 23:45:17 +0200 Subject: [PATCH 02/18] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Xcode=20proje?= =?UTF-8?q?ct=20deployment=20targets=20to=20match=20SPM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ACKategories.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index 78f25e2..72b2c2f 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -2242,15 +2242,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MACOSX_DEPLOYMENT_TARGET = 10.13; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 6.16.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = auto; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + TVOS_DEPLOYMENT_TARGET = 15.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -2288,15 +2288,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MACOSX_DEPLOYMENT_TARGET = 10.13; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 6.16.0; SDKROOT = auto; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + TVOS_DEPLOYMENT_TARGET = 15.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; From b9c3ecf239e6fab124b81071b44e19580622b137 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Mon, 7 Sep 2026 23:45:33 +0200 Subject: [PATCH 03/18] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20example=20app?= =?UTF-8?q?=20deployment=20target=20to=20iOS=2015?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ACKategories.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index 72b2c2f..fbb1d35 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -1745,7 +1745,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ACKategoriesExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1813,7 +1813,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ACKategoriesExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 33eaf7f1e6bc912827799582c7f574de420f31bf Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Mon, 7 Sep 2026 23:46:00 +0200 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=93=9D=20Document=20supported=20pla?= =?UTF-8?q?tform=20minimums?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3a58ef..d50d736 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,13 @@ A bunch of tools, cocoa subclasses and extensions we created and use at Ackee. +## Requirements + +- iOS 15+ +- macOS 12+ +- tvOS 15+ +- watchOS 9+ + ## Installation ### Carthage @@ -52,7 +59,7 @@ button.on(.touchUpInside) { sender in ... } ``` -If running on iOS 9 or later you can use implicit parameter `UIControl.primaryActionTriggered`. +You can also use implicit parameter `UIControl.primaryActionTriggered`. ### String - trim strings easily From 09259e173c470c4587ffe690ff4cf8059abdbda8 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 00:00:33 +0200 Subject: [PATCH 05/18] =?UTF-8?q?=F0=9F=93=9D=20Add=20CHANGELOG=20entry=20?= =?UTF-8?q?for=20deployment=20target=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e8fdc6..6eb5c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ## Next +- 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 - Rename `Base.ViewController` to `BaseViewController` ([#155](https://github.com/AckeeCZ/ACKategories/pull/155), kudos to @komkovla) - Add Objective-C name annotation `@objc(ACKBaseViewController)` for better Objective-C interoperability - Add deprecated typealias `Base.ViewController` for backward compatibility From 65c11be941c2e348fbcb5646f4a6f3deac9c2e62 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 10:55:02 +0200 Subject: [PATCH 06/18] =?UTF-8?q?=F0=9F=94=A7=20Repair=20CI=20after=20maco?= =?UTF-8?q?s-latest=20runner=20image=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 2 +- .github/workflows/docbuild.yml | 2 +- .github/workflows/tests.yml | 10 ++++++---- CHANGELOG.md | 3 +++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9284bed..c6644fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: - main env: - DEVELOPER_DIR: /Applications/Xcode_26.1.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer jobs: carthage: diff --git a/.github/workflows/docbuild.yml b/.github/workflows/docbuild.yml index f21bf9f..af9fdb9 100644 --- a/.github/workflows/docbuild.yml +++ b/.github/workflows/docbuild.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - DEVELOPER_DIR: /Applications/Xcode_26.1.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer jobs: # Single deploy job since we're just deploying diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe4c6bc..4ef8fe8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,14 +3,16 @@ name: Tests on: [workflow_call] env: - DEVELOPER_DIR: /Applications/Xcode_26.1.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer jobs: xcodebuild: name: Xcodebuild runs-on: macos-latest env: - IOS_DEVICE: iPhone 16 Pro + IOS_DEVICE: iPhone 17 Pro + WATCHOS_DEVICE: Apple Watch Ultra 3 (49mm) + TVOS_DEVICE: Apple TV 4K (3rd generation) steps: - uses: actions/checkout@v4 - name: iOS tests @@ -35,14 +37,14 @@ jobs: name: Tests-macOS.xcresult path: Tests-macOS.xcresult - name: watchOS tests - run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-watchOS.xcresult -sdk watchsimulator -destination "platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm),OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty + run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-watchOS.xcresult -sdk watchsimulator -destination "platform=watchOS Simulator,name=$WATCHOS_DEVICE,OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - uses: actions/upload-artifact@v4 if: failure() with: name: Tests-watchOS.xcresult path: Tests-watchOS.xcresult - name: tvOS tests - run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-tvOS.xcresult -sdk appletvsimulator -destination "platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty + run: set -o pipefail && xcodebuild test -scheme ACKategories -resultBundlePath Tests-tvOS.xcresult -sdk appletvsimulator -destination "platform=tvOS Simulator,name=$TVOS_DEVICE,OS=latest" ONLY_ACTIVE_ARCH=YES | xcpretty - uses: actions/upload-artifact@v4 if: failure() with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb5c35..dba6307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ - 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) + - Bump the pinned Xcode to 26.6 — the image no longer ships the iOS/tvOS/watchOS 26.1 simulator runtimes that 26.1.1 needs + - Refresh simulator destinations to devices that still exist (`iPhone 17 Pro`, `Apple Watch Ultra 3 (49mm)`) - Rename `Base.ViewController` to `BaseViewController` ([#155](https://github.com/AckeeCZ/ACKategories/pull/155), kudos to @komkovla) - Add Objective-C name annotation `@objc(ACKBaseViewController)` for better Objective-C interoperability - Add deprecated typealias `Base.ViewController` for backward compatibility From 3068b697163dec4191002db67e1cf70f567c2554 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:31:08 +0200 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=94=A5=20Drop=20dead=20@available?= =?UTF-8?q?=20from=20Networking=20and=20PushNotifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/ACKategoriesTesting/Networking/APIServiceMock.swift | 1 - Sources/ACKategoriesTesting/Networking/NetworkMock.swift | 1 - Sources/ACKategoriesTesting/VersionUpdateFetcher_Mock.swift | 1 - Sources/Networking/APIService.swift | 1 - Sources/Networking/APIServicing.swift | 3 --- Sources/Networking/Networking.swift | 2 -- Sources/Networking/OAuthInterceptor.swift | 1 - Sources/PushNotifications/PushManager.swift | 5 ----- .../PushNotifications/UNNotificationSettingsExtensions.swift | 1 - 9 files changed, 16 deletions(-) diff --git a/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift b/Sources/ACKategoriesTesting/Networking/APIServiceMock.swift index 202941a..2123364 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 7aaeefc..4a7b41c 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 7892c4c..e82b36c 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 3f5170c..6e18c9e 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 ead9797..00e4d18 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 6e6e53a..f2c6f80 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 1c4bae2..789778f 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 0d4b12c..7023ebb 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 07492ae..184c254 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)]() From 593e2cb973e786eb278b564ee13f9220a07cbea2 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:32:30 +0200 Subject: [PATCH 08/18] =?UTF-8?q?=F0=9F=94=A5=20Drop=20dead=20@available?= =?UTF-8?q?=20from=20SwiftUI=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/ACKategories/EdgeInsetsExtensions.swift | 1 - .../SwiftUIExtensions/ACKHostingController.swift | 1 - .../SwiftUIExtensions/FontModifier.swift | 1 - .../SwiftUIExtensions/SwiftUIColorsTheme.swift | 2 -- .../SwiftUIExtensions/SwiftUILayoutGuides.swift | 13 +------------ .../ACKategories/SwiftUIExtensions/View+Frame.swift | 2 -- .../SwiftUIExtensions/View+FrameSize.swift | 1 - 7 files changed, 1 insertion(+), 20 deletions(-) diff --git a/Sources/ACKategories/EdgeInsetsExtensions.swift b/Sources/ACKategories/EdgeInsetsExtensions.swift index ed0cc49..cad0790 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/SwiftUIExtensions/ACKHostingController.swift b/Sources/ACKategories/SwiftUIExtensions/ACKHostingController.swift index 65f029c..7b22c5a 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 be6e161..5ea4f7f 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 eb008c9..b7c6e1a 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 17ed5f4..6757637 100644 --- a/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift +++ b/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift @@ -2,7 +2,6 @@ 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, *) public struct WithLayoutMargins: View where Content: View { let content: (EdgeInsets) -> 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,7 +79,6 @@ 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. /// @@ -113,17 +110,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 +134,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 +160,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 +250,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 +264,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 +274,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 +287,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 83f4714..7676b95 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 e1433aa..ccc1455 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 { From 6d22a5a33f0d82abe1276293881813b45ea1814b Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:33:29 +0200 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=94=A5=20Drop=20dead=20@available?= =?UTF-8?q?=20from=20Combine=20and=20Publisher=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/ACKategories/Combine+Concurrency.swift | 2 -- Sources/ACKategories/PropertyWrappers/UserDefault.swift | 2 -- Sources/ACKategories/PublisherExtensions.swift | 3 --- Sources/ACKategories/UIDeviceExtensions.swift | 1 - Sources/ACKategories/VersionUpdate/MinBuildNumberFetcher.swift | 1 - Sources/ACKategories/VersionUpdate/VersionUpdateManager.swift | 2 -- 6 files changed, 11 deletions(-) diff --git a/Sources/ACKategories/Combine+Concurrency.swift b/Sources/ACKategories/Combine+Concurrency.swift index eda736d..41c5048 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/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index 3d9af9b..4c26f0f 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -76,12 +76,10 @@ public final class UserDefault { } } - @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 { diff --git a/Sources/ACKategories/PublisherExtensions.swift b/Sources/ACKategories/PublisherExtensions.swift index ac384ff..cbc571b 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/UIDeviceExtensions.swift b/Sources/ACKategories/UIDeviceExtensions.swift index 3520985..74d7f46 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 954e658..b24376c 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 4fbee4a..b5c7a10 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 { From fbd6c1c65ef96f7c9fe3afcd8bc04970ac38bfb5 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:34:21 +0200 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=94=A5=20Unwrap=20availability=20ch?= =?UTF-8?q?ecks=20made=20dead=20by=20the=20iOS=2015=20floor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UIControlBlocksViewController.swift | 4 +--- .../PropertyWrappers/UserDefault.swift | 4 +--- .../UI/UISearchBarExtensions.swift | 5 +---- Sources/ACKategories/UIColorExtensions.swift | 22 ++++++------------- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/ACKategoriesExample/Screens/UIControl blocks/UIControlBlocksViewController.swift b/ACKategoriesExample/Screens/UIControl blocks/UIControlBlocksViewController.swift index 557ed65..7d79c59 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/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index 4c26f0f..865580c 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -70,9 +70,7 @@ public final class UserDefault { } } - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { - subject.send(newValue) - } + subject.send(newValue) } } diff --git a/Sources/ACKategories/UI/UISearchBarExtensions.swift b/Sources/ACKategories/UI/UISearchBarExtensions.swift index 5a7ede1..21c8eaf 100644 --- a/Sources/ACKategories/UI/UISearchBarExtensions.swift +++ b/Sources/ACKategories/UI/UISearchBarExtensions.swift @@ -8,10 +8,7 @@ public extension UISearchBar { /// 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 + return searchTextField } } #endif diff --git a/Sources/ACKategories/UIColorExtensions.swift b/Sources/ACKategories/UIColorExtensions.swift index 41f7e4b..b3f61b2 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 From da81e04b22555b062d88d5389cbce7a3ae3adc7a Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:36:13 +0200 Subject: [PATCH 11/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20UserDefau?= =?UTF-8?q?lt.subject=20to=20a=20stored=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PropertyWrappers/UserDefault.swift | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index 865580c..4be7a20 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: @@ -78,16 +74,8 @@ public final class UserDefault { subject.eraseToAnyPublisher() } - // 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 - } - - let subject = CurrentValueSubject(wrappedValue) - objc_setAssociatedObject(self, &Keys.subject, subject, .OBJC_ASSOCIATION_RETAIN) - return subject - } + // lazy so the subject starts with the value stored at the time of first use + private lazy var subject = CurrentValueSubject(wrappedValue) } public extension UserDefault { From 2d48cdeb0602aae040492b3de936eae14e2ec5b5 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:37:27 +0200 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=94=A5=20Drop=20dead=20@available?= =?UTF-8?q?=20from=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/ACKategoriesTests/EdgeInsetsTests.swift | 1 - Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift | 1 - .../VersionUpdate/VersionUpdateManager_Tests.swift | 1 - Tests/NetworkingTests/APIService+OAuthInterceptor_Tests.swift | 1 - Tests/NetworkingTests/APIService_Tests.swift | 1 - Tests/NetworkingTests/OAuthInterceptor_Tests.swift | 1 - 6 files changed, 6 deletions(-) diff --git a/Tests/ACKategoriesTests/EdgeInsetsTests.swift b/Tests/ACKategoriesTests/EdgeInsetsTests.swift index 554e710..9516013 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 992d739..4595e90 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! diff --git a/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift b/Tests/ACKategoriesTests/VersionUpdate/VersionUpdateManager_Tests.swift index 99e423e..087edbc 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 e5a599c..c8f7408 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 0cb95bc..5b31063 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 ec49d09..8879529 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") } From 5fb6406312e0d78ef8b9d5b61d3ef0873b504125 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 12:48:29 +0200 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=93=9D=20Add=20CHANGELOG=20entry=20?= =?UTF-8?q?for=20availability=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dba6307..d1743b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ## Next +- 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 `lazy` stored property instead of `objc_getAssociatedObject` - 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) From 0098f41bca0c8021a99e64386d3b4f128ddc5c95 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 13:24:57 +0200 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=93=9D=20Document=20that=20UserDefa?= =?UTF-8?q?ult.subject=20is=20not=20thread-safe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + Sources/ACKategories/PropertyWrappers/UserDefault.swift | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1743b2..2f8e9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - 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 `lazy` stored property instead of `objc_getAssociatedObject` + - The associated-object storage was lock-protected by the ObjC runtime, so a concurrent *first* access is now a data race rather than merely racy. Neither form was usable concurrently, but the failure mode changed - 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) diff --git a/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index 4be7a20..f50e85f 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -74,7 +74,10 @@ public final class UserDefault { subject.eraseToAnyPublisher() } - // lazy so the subject starts with the value stored at the time of first use + // `lazy` so the subject starts with the value stored at the time of first use, + // matching the associated-object implementation this replaced. + // Not thread-safe: the first access must be serialised, otherwise the lazy + // initialisation is a data race. private lazy var subject = CurrentValueSubject(wrappedValue) } From e81bb3e08958f3d879a2cc68960d311e4cda33a7 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Tue, 8 Sep 2026 17:14:34 +0200 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=94=A5=20Remove=20UISearchBar.textF?= =?UTF-8?q?ield=20and=20create=20UserDefault.subject=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ACKategories.xcodeproj/project.pbxproj | 4 ---- CHANGELOG.md | 6 ++++-- .../PropertyWrappers/UserDefault.swift | 15 ++++++--------- .../ACKategories/UI/UISearchBarExtensions.swift | 14 -------------- 4 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 Sources/ACKategories/UI/UISearchBarExtensions.swift diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index fbb1d35..30cfe16 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 */; }; @@ -271,7 +270,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 = ""; }; @@ -533,7 +531,6 @@ 69E0A61F2AFD10BE00C8E8D9 /* UIApplicationExtensions.swift */, 69E0A6192AFD10BE00C8E8D9 /* UILabelExtensions.swift */, 69E0A6132AFD10BE00C8E8D9 /* UINavigationControllerExtensions.swift */, - 69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */, 69E0A5FB2AFD10BE00C8E8D9 /* UIStackViewExtensions.swift */, 69E0A6092AFD10BE00C8E8D9 /* UIView+Spacer.swift */, 69E0A60E2AFD10BE00C8E8D9 /* UIViewController+Children.swift */, @@ -1298,7 +1295,6 @@ 69BC15952CF8A42C00A4B55F /* View+Frame.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/CHANGELOG.md b/CHANGELOG.md index 2f8e9a4..90d179e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ ## Next +- 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 `lazy` stored property instead of `objc_getAssociatedObject` - - The associated-object storage was lock-protected by the ObjC runtime, so a concurrent *first* access is now a data race rather than merely racy. Neither form was usable concurrently, but the failure mode changed + - 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 `UserDefault` no longer has racy state - 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) diff --git a/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index f50e85f..d3a1005 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -14,8 +14,9 @@ import Foundation 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 @@ -32,12 +33,14 @@ public final class UserDefault { self.defaultValue = `default` self.userDefaults = userDefaults self.errorLogger = errorLogger + self.subject = CurrentValueSubject(`default`) + subject.send(wrappedValue) } public var wrappedValue: Value { get { // Check if `Value` is supported by default by `UserDefaults` - if Value.self as? PropertyListValue.Type != nil { + 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 } @@ -54,7 +57,7 @@ public final class UserDefault { } } set { - if Value.self as? PropertyListValue.Type != nil { + if Value.self is PropertyListValue.Type { userDefaults.set(newValue, forKey: key) } else { let encoder = JSONEncoder() @@ -73,12 +76,6 @@ public final class UserDefault { public var projectedValue: AnyPublisher { subject.eraseToAnyPublisher() } - - // `lazy` so the subject starts with the value stored at the time of first use, - // matching the associated-object implementation this replaced. - // Not thread-safe: the first access must be serialised, otherwise the lazy - // initialisation is a data race. - private lazy var subject = CurrentValueSubject(wrappedValue) } public extension UserDefault { diff --git a/Sources/ACKategories/UI/UISearchBarExtensions.swift b/Sources/ACKategories/UI/UISearchBarExtensions.swift deleted file mode 100644 index 21c8eaf..0000000 --- a/Sources/ACKategories/UI/UISearchBarExtensions.swift +++ /dev/null @@ -1,14 +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! { - return searchTextField - } -} -#endif From 135b5135bbe33212e536e4997f966d49ea34b575 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Wed, 9 Sep 2026 16:58:21 +0200 Subject: [PATCH 16/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Seed=20UserDefault.s?= =?UTF-8?q?ubject=20from=20disk=20in=20one=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `init` used to build the subject with the default value and then send the persisted one into it, because Swift forbids touching `self` before every stored property is assigned. Extract the getter's body into a static `storedValue(forKey:in:default:errorLogger:)` so `init` can read the store before `subject` exists, and drop the trailing `send`. The read logic now lives in exactly one place. Also rewrite the array-wrapping comment. It justified the workaround with an iOS 12 encoder limitation, two deployment-target floors out of date, so as written it invited the next reader to delete the workaround — which would break decoding of everything already persisted. It now states the prohibition and its cost instead. No behavior change: the existing tests pass unedited, including the one asserting the publisher replays the default followed by the new value. --- CHANGELOG.md | 4 +- .../PropertyWrappers/UserDefault.swift | 51 ++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d179e..009b3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,9 @@ - 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 `UserDefault` no longer has racy state + - 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 - 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) diff --git a/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index d3a1005..81c4152 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -22,7 +22,7 @@ public final class UserDefault { /// - 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, @@ -33,28 +33,14 @@ public final class UserDefault { self.defaultValue = `default` self.userDefaults = userDefaults self.errorLogger = errorLogger - self.subject = CurrentValueSubject(`default`) - subject.send(wrappedValue) + 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 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() - // 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 is PropertyListValue.Type { @@ -76,6 +62,33 @@ public final class UserDefault { public var projectedValue: AnyPublisher { subject.eraseToAnyPublisher() } + + /// 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 + } + } + } } public extension UserDefault { From 3903791d7d425998269269cdcf051b1dbdd53649 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Wed, 9 Sep 2026 16:59:42 +0200 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=90=9B=20Don't=20publish=20UserDefa?= =?UTF-8?q?ult=20values=20that=20failed=20to=20persist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setter sent `newValue` into the subject unconditionally, outside the `do/catch` around encoding. When `JSONEncoder` threw, the error was logged, nothing reached `UserDefaults`, and subscribers were still told the value had changed — so `wrappedValue` and `$value` disagreed permanently, and the value subscribers held disappeared on the next launch. Not hypothetical: `JSONEncoder` rejects non-conforming floats by default, so any `Codable` model carrying a computed ratio can hit it. The new test covers exactly that, and asserts a later successful write is still published. --- CHANGELOG.md | 2 ++ .../PropertyWrappers/UserDefault.swift | 3 ++ .../PropertyWrappers/UserDefaultTests.swift | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 009b3cd..854bc9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - 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) diff --git a/Sources/ACKategories/PropertyWrappers/UserDefault.swift b/Sources/ACKategories/PropertyWrappers/UserDefault.swift index 81c4152..1ca2121 100644 --- a/Sources/ACKategories/PropertyWrappers/UserDefault.swift +++ b/Sources/ACKategories/PropertyWrappers/UserDefault.swift @@ -52,6 +52,9 @@ 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 } } diff --git a/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift b/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift index 4595e90..9289ea2 100644 --- a/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift +++ b/Tests/ACKategoriesTests/PropertyWrappers/UserDefaultTests.swift @@ -73,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 { @@ -84,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 +} From dfdae2bf25b8685e51e7c938d1defd20b5c55708 Mon Sep 17 00:00:00 2001 From: Jindrich Dolezal Date: Wed, 9 Sep 2026 17:05:34 +0200 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=94=A7=20Ship=20SwiftUILayoutGuides?= =?UTF-8?q?=20and=20View+FrameSize=20through=20the=20Xcode=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These were the only two files under `Sources/` with no reference in `project.pbxproj`. SPM globs the tree so package consumers have had them since 6.16.0, where the CHANGELOG announced both — but the Carthage xcframework shipped without them and no `xcodebuild` job ever compiled them for any platform. Added to the framework target only, unmodified. Verified: Debug builds for iOS, macOS, watchOS and tvOS simulators all succeed with no diagnostics citing either file, and `xcodebuild docbuild` succeeds clean. The DocC link fixes ride along because they surfaced only now, when the documentation build first saw this file. Environment values become plain code spans instead of symbol links — DocC files extensions of SwiftUI types under `SwiftUICore`, an internal module name not worth hardcoding — and the notes referencing `FitReadableContentWidth`/`FitLayoutMarginsWidth` are dropped, since neither type has ever existed here. Also corrects the 6.16.0 entry for `WithLayoutMargins`, whose link text said #150 while pointing at pull request 151. --- ACKategories.xcodeproj/project.pbxproj | 8 ++++++++ CHANGELOG.md | 5 ++++- .../SwiftUIExtensions/SwiftUILayoutGuides.swift | 16 ++++++---------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index 30cfe16..bf4b51f 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -138,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 */ @@ -357,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 */ @@ -651,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 */, @@ -1293,6 +1299,8 @@ 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 */, 69ACD6F72AFD133A0021127B /* UIStackViewExtensions.swift in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 854bc9a..4d3b324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ## 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) @@ -38,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/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift b/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift index 6757637..fda0125 100644 --- a/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift +++ b/Sources/ACKategories/SwiftUIExtensions/SwiftUILayoutGuides.swift @@ -1,12 +1,12 @@ import SwiftUI // Source: https://github.com/tgrapperon/swiftui-layout-guides -/// This view populates its content's ``layoutMarginsInsets`` and ``readableContentInsets``. +/// 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. @@ -14,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) { @@ -85,8 +85,6 @@ 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 - /// ``FitReadableContentWidth`` view. public func fitToReadableContentWidth(alignment: Alignment = .center) -> some View { self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .readableContent)) } @@ -96,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.