Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f3232f9
⬆️ Bump SPM platforms to iOS 15, macOS 12, tvOS 15, watchOS 9
Sep 7, 2026
103f746
⬆️ Bump Xcode project deployment targets to match SPM
Sep 7, 2026
b9c3ecf
⬆️ Bump example app deployment target to iOS 15
Sep 7, 2026
33eaf7f
πŸ“ Document supported platform minimums
Sep 7, 2026
09259e1
πŸ“ Add CHANGELOG entry for deployment target bump
Sep 7, 2026
65c11be
πŸ”§ Repair CI after macos-latest runner image drift
Sep 8, 2026
3068b69
πŸ”₯ Drop dead @available from Networking and PushNotifications
Sep 8, 2026
593e2cb
πŸ”₯ Drop dead @available from SwiftUI extensions
Sep 8, 2026
6d22a5a
πŸ”₯ Drop dead @available from Combine and Publisher extensions
Sep 8, 2026
fbd6c1c
πŸ”₯ Unwrap availability checks made dead by the iOS 15 floor
Sep 8, 2026
da81e04
♻️ Simplify UserDefault.subject to a stored property
Sep 8, 2026
2d48cde
πŸ”₯ Drop dead @available from tests
Sep 8, 2026
5fb6406
πŸ“ Add CHANGELOG entry for availability cleanup
Sep 8, 2026
0098f41
πŸ“ Document that UserDefault.subject is not thread-safe
Sep 8, 2026
e32d886
Merge branch 'main' into drop-dead-availability
olejnjak Sep 8, 2026
e81bb3e
πŸ”₯ Remove UISearchBar.textField and create UserDefault.subject once
Sep 8, 2026
135b513
♻️ Seed UserDefault.subject from disk in one step
Sep 9, 2026
3903791
πŸ› Don't publish UserDefault values that failed to persist
Sep 9, 2026
dfdae2b
πŸ”§ Ship SwiftUILayoutGuides and View+FrameSize through the Xcode project
Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions ACKategories.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 = "<group>"; };
69DF227A2B459D0D0025C555 /* UNNotificationSettingsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationSettingsExtensions.swift; sourceTree = "<group>"; };
69E0A5F52AFD10BE00C8E8D9 /* UISearchBarExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISearchBarExtensions.swift; sourceTree = "<group>"; };
69E0A5F72AFD10BE00C8E8D9 /* UserDefault.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
69E0A5F82AFD10BE00C8E8D9 /* ArrayExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = "<group>"; };
69E0A5F92AFD10BE00C8E8D9 /* ACKategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ACKategories.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -359,6 +359,8 @@
69FA5FC123C8690A00B44BCD /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
6A72B2212B1A15AC00A59EDD /* BackGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackGesture.swift; sourceTree = "<group>"; };
88EDD90325B8252E00207987 /* GradientViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientViewController.swift; sourceTree = "<group>"; };
D0E3212C27C546A38A962094 /* View+FrameSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+FrameSize.swift"; sourceTree = "<group>"; };
E0AE9942CD0E454B948FF15A /* SwiftUILayoutGuides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUILayoutGuides.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
olejnjak marked this conversation as resolved.
- 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)
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions Sources/ACKategories/Combine+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion Sources/ACKategories/EdgeInsetsExtensions.swift
Original file line number Diff line number Diff line change
@@ -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) }

Expand Down
72 changes: 36 additions & 36 deletions Sources/ACKategories/PropertyWrappers/UserDefault.swift
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,14 +14,15 @@ private enum Keys {
public final class UserDefault<Value: Codable> {
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<Value, Never>

/// - 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,
Expand All @@ -36,29 +33,17 @@ public final class UserDefault<Value: Codable> {
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()
Expand All @@ -67,30 +52,45 @@ public final class UserDefault<Value: Codable> {
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<Value, Never> {
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<Value, Never> {
if let subject = objc_getAssociatedObject(self, &Keys.subject) as? CurrentValueSubject<Value, Never> {
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<Value, Never>(wrappedValue)
objc_setAssociatedObject(self, &Keys.subject, subject, .OBJC_ASSOCIATION_RETAIN)
return subject
}
}

Expand Down
3 changes: 0 additions & 3 deletions Sources/ACKategories/PublisherExtensions.swift
Original file line number Diff line number Diff line change
@@ -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<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Output>, on object: Root) -> AnyCancellable {
Expand All @@ -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<T: Publisher>(_ transform: @escaping (Self.Output) -> T) -> Publishers.SwitchToLatest<T, Publishers.Map<Self, T>> {
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<Upstream>: Publisher where Upstream: Publisher {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import os.log
import SwiftUI

@available(iOS 13.0, tvOS 13.0, *)
open class ACKHostingController<RootView: View>: UIHostingController<RootView> {
/// Navigation bar is shown/hidden in viewWillAppear according to this flag
public var hasNavigationBar = true
Expand Down
1 change: 0 additions & 1 deletion Sources/ACKategories/SwiftUIExtensions/FontModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Theme<UIColor>, UIColor>) -> SwiftUI.Color {
Expand All @@ -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<UIColor>` extension, see ``SwiftUIColorsTheme``.
static let theme = SwiftUIColorsTheme()
Expand Down
Loading
Loading