Skip to content

Commit 4903492

Browse files
authored
Add AppStorage support for Date and Date? types (#36)
1 parent 51bcc14 commit 4903492

File tree

8 files changed

+319
-42
lines changed

8 files changed

+319
-42
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ When adding features:
181181

182182
Minimum deployment targets:
183183
- iOS 18.0 / iPadOS 18.0
184-
- macOS 15.0
184+
- macOS 12.0
185185
- tvOS 18.0
186186
- watchOS 11.0
187187
- visionOS 2.0

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ let package = Package(
3232
platforms: [
3333
.iOS(.v18),
3434
.macCatalyst(.v18),
35-
.macOS(.v15),
35+
.macOS(.v12),
3636
.tvOS(.v18),
3737
.visionOS(.v2),
3838
.watchOS(.v11)

Scripts/lint.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ $MINT_CMD bootstrap -m Mintfile || exit 1
4444
if [ -z "$CI" ]; then
4545
$MINT_RUN swift-format format --configuration .swift-format --recursive --parallel --in-place Sources Tests || exit 1
4646
$MINT_RUN swiftlint --autocorrect || exit 1
47-
$PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "RadiantKit"
48-
$PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Tests -c "Leo Dion" -o "BrightDigit" -p "RadiantKit"
47+
$PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "RadiantKit" -y 2025
48+
$PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Tests -c "Leo Dion" -o "BrightDigit" -p "RadiantKit" -y 2025
4949
fi
5050

5151
if [ -z "$FORMAT_ONLY" ]; then

Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+AppStored.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,25 @@
142142
self.init(wrappedValue: wrappedValue, type.key, store: store)
143143
}
144144

145+
/// Initializes an `AppStorage` instance with a wrapped value and an
146+
/// `AppStored` type.
147+
///
148+
/// - Parameters:
149+
/// - wrappedValue: The initial value for the app-stored value.
150+
/// - type: The `AppStored` type to associate with the app-stored value.
151+
/// - store: The `UserDefaults` instance to use for storing the app-stored
152+
/// value. If `nil`, the standard `UserDefaults` will be used.
153+
/// - Requires:
154+
/// - `AppStoredType.Value` must be `Date` and `Value` must be `Date`.
155+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
156+
public init<AppStoredType: AppStored>(
157+
wrappedValue: Value,
158+
for type: AppStoredType.Type,
159+
store: UserDefaults? = nil
160+
) where AppStoredType.Value == Date, Value == Date {
161+
self.init(wrappedValue: wrappedValue, type.key, store: store)
162+
}
163+
145164
/// Initializes an `AppStorage` instance with a wrapped value and an
146165
/// `AppStored` type.
147166
///

Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+ExpressibleByNilLiteral.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@
136136
Value == Data? {
137137
self.init(type.key, store: store)
138138
}
139+
140+
/// Initializes an `AppStorage` property wrapper for a given `AppStored` type
141+
/// with an optional `Date` value.
142+
///
143+
/// - Parameters:
144+
/// - type: The `AppStored` type.
145+
/// - store: The `UserDefaults` instance to use, or `nil` to use the shared
146+
/// `UserDefaults`.
147+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
148+
public init<AppStoredType: AppStored>(
149+
for type: AppStoredType.Type,
150+
store: UserDefaults? = nil
151+
)
152+
where
153+
AppStoredType.Value == Value,
154+
Value == Date? {
155+
self.init(type.key, store: store)
156+
}
139157
}
140158

141159
extension AppStorage {

Sources/RadiantKit/Views/ValueTextBubble.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
public import SwiftUI
3535

3636
/// A SwiftUI view that displays a value with a formatted text bubble.
37-
@available(iOS 15.0, *)
3837
public struct ValueTextBubble<
3938
ShapeStyleType: ShapeStyle,
4039
ValueType: Equatable,
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
//
2+
// AppStorageDateTests.swift
3+
// RadiantKit
4+
//
5+
// Created by Leo Dion.
6+
// Copyright © 2025 BrightDigit.
7+
//
8+
// Permission is hereby granted, free of charge, to any person
9+
// obtaining a copy of this software and associated documentation
10+
// files (the "Software"), to deal in the Software without
11+
// restriction, including without limitation the rights to use,
12+
// copy, modify, merge, publish, distribute, sublicense, and/or
13+
// sell copies of the Software, and to permit persons to whom the
14+
// Software is furnished to do so, subject to the following
15+
// conditions:
16+
//
17+
// The above copyright notice and this permission notice shall be
18+
// included in all copies or substantial portions of the Software.
19+
//
20+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27+
// OTHER DEALINGS IN THE SOFTWARE.
28+
//
29+
30+
#if canImport(SwiftUI)
31+
32+
import Foundation
33+
import RadiantKit
34+
import SwiftUI
35+
import Testing
36+
37+
// MARK: - Test Types
38+
39+
internal enum TestDateStored: AppStored {
40+
internal typealias Value = Date
41+
internal static let keyType: KeyType = .describing
42+
}
43+
44+
internal enum TestOptionalDateStored: AppStored {
45+
internal typealias Value = Date?
46+
internal static let keyType: KeyType = .describing
47+
}
48+
49+
internal enum TestReflectingDateStored: AppStored {
50+
internal typealias Value = Date
51+
internal static let keyType: KeyType = .reflecting
52+
}
53+
54+
internal enum TestReflectingOptionalDateStored: AppStored {
55+
internal typealias Value = Date?
56+
internal static let keyType: KeyType = .reflecting
57+
}
58+
59+
// MARK: - Tests
60+
61+
@MainActor
62+
internal struct AppStorageDateTests {
63+
// MARK: - Non-Optional Date Tests
64+
65+
@Test
66+
internal func testNonOptionalDateStorage() async throws {
67+
#if canImport(SwiftUI)
68+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
69+
guard let store = UserDefaults(suiteName: #function) else {
70+
Issue.record("Failed to create UserDefaults")
71+
return
72+
}
73+
defer { store.removePersistentDomain(forName: #function) }
74+
75+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
76+
let storage = AppStorage(
77+
wrappedValue: testDate,
78+
for: TestDateStored.self,
79+
store: store
80+
)
81+
82+
#expect(storage.wrappedValue == testDate)
83+
} else {
84+
Issue.record("AppStorage Date support requires macOS 15.0+")
85+
}
86+
#else
87+
Issue.record("SwiftUI is not available on this platform")
88+
#endif
89+
}
90+
91+
@Test
92+
internal func testNonOptionalDateUpdate() async throws {
93+
#if canImport(SwiftUI)
94+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
95+
guard let store = UserDefaults(suiteName: #function) else {
96+
Issue.record("Failed to create UserDefaults")
97+
return
98+
}
99+
defer { store.removePersistentDomain(forName: #function) }
100+
101+
let initialDate = Date(timeIntervalSince1970: 1_704_067_200)
102+
let storage = AppStorage(
103+
wrappedValue: initialDate,
104+
for: TestDateStored.self,
105+
store: store
106+
)
107+
108+
#expect(storage.wrappedValue == initialDate)
109+
110+
let newDate = Date(timeIntervalSince1970: 1_735_689_600)
111+
storage.wrappedValue = newDate
112+
113+
#expect(storage.wrappedValue == newDate)
114+
#expect(store.object(forKey: TestDateStored.key) != nil)
115+
} else {
116+
Issue.record("AppStorage Date support requires macOS 15.0+")
117+
}
118+
#else
119+
Issue.record("SwiftUI is not available on this platform")
120+
#endif
121+
}
122+
123+
@Test
124+
internal func testNonOptionalDateWithReflectingKey() async throws {
125+
#if canImport(SwiftUI)
126+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
127+
guard let store = UserDefaults(suiteName: #function) else {
128+
Issue.record("Failed to create UserDefaults")
129+
return
130+
}
131+
defer { store.removePersistentDomain(forName: #function) }
132+
133+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
134+
let storage = AppStorage(
135+
wrappedValue: testDate,
136+
for: TestReflectingDateStored.self,
137+
store: store
138+
)
139+
140+
#expect(storage.wrappedValue == testDate)
141+
142+
let key = TestReflectingDateStored.key
143+
#expect(key.contains("TestReflectingDateStored"))
144+
} else {
145+
Issue.record("AppStorage Date support requires macOS 15.0+")
146+
}
147+
#else
148+
Issue.record("SwiftUI is not available on this platform")
149+
#endif
150+
}
151+
152+
// MARK: - Optional Date Tests
153+
154+
@Test
155+
internal func testOptionalDateStorageWithValue() async throws {
156+
#if canImport(SwiftUI)
157+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
158+
guard let store = UserDefaults(suiteName: #function) else {
159+
Issue.record("Failed to create UserDefaults")
160+
return
161+
}
162+
defer { store.removePersistentDomain(forName: #function) }
163+
164+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
165+
let storage = AppStorage(
166+
for: TestOptionalDateStored.self,
167+
store: store
168+
)
169+
170+
#expect(storage.wrappedValue == nil)
171+
172+
storage.wrappedValue = testDate
173+
174+
#expect(storage.wrappedValue == testDate)
175+
#expect(store.object(forKey: TestOptionalDateStored.key) != nil)
176+
} else {
177+
Issue.record("AppStorage Date support requires macOS 15.0+")
178+
}
179+
#else
180+
Issue.record("SwiftUI is not available on this platform")
181+
#endif
182+
}
183+
184+
@Test
185+
internal func testOptionalDateStorageWithNil() async throws {
186+
#if canImport(SwiftUI)
187+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
188+
guard let store = UserDefaults(suiteName: #function) else {
189+
Issue.record("Failed to create UserDefaults")
190+
return
191+
}
192+
defer { store.removePersistentDomain(forName: #function) }
193+
194+
let storage = AppStorage(
195+
for: TestOptionalDateStored.self,
196+
store: store
197+
)
198+
199+
#expect(storage.wrappedValue == nil)
200+
201+
let retrievedValue = store.object(
202+
forKey: TestOptionalDateStored.key
203+
)
204+
#expect(retrievedValue == nil)
205+
} else {
206+
Issue.record("AppStorage Date support requires macOS 15.0+")
207+
}
208+
#else
209+
Issue.record("SwiftUI is not available on this platform")
210+
#endif
211+
}
212+
213+
@Test
214+
internal func testOptionalDateSetToNil() async throws {
215+
#if canImport(SwiftUI)
216+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
217+
guard let store = UserDefaults(suiteName: #function) else {
218+
Issue.record("Failed to create UserDefaults")
219+
return
220+
}
221+
defer { store.removePersistentDomain(forName: #function) }
222+
223+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
224+
let storage = AppStorage(
225+
for: TestOptionalDateStored.self,
226+
store: store
227+
)
228+
229+
storage.wrappedValue = testDate
230+
#expect(storage.wrappedValue == testDate)
231+
232+
storage.wrappedValue = nil
233+
#expect(storage.wrappedValue == nil)
234+
235+
let retrievedValue = store.object(
236+
forKey: TestOptionalDateStored.key
237+
)
238+
#expect(retrievedValue == nil)
239+
} else {
240+
Issue.record("AppStorage Date support requires macOS 15.0+")
241+
}
242+
#else
243+
Issue.record("SwiftUI is not available on this platform")
244+
#endif
245+
}
246+
247+
@Test
248+
internal func testOptionalDateWithReflectingKey() async throws {
249+
#if canImport(SwiftUI)
250+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
251+
guard let store = UserDefaults(suiteName: #function) else {
252+
Issue.record("Failed to create UserDefaults")
253+
return
254+
}
255+
defer { store.removePersistentDomain(forName: #function) }
256+
257+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
258+
let storage = AppStorage(
259+
for: TestReflectingOptionalDateStored.self,
260+
store: store
261+
)
262+
263+
storage.wrappedValue = testDate
264+
#expect(storage.wrappedValue == testDate)
265+
266+
let key = TestReflectingOptionalDateStored.key
267+
#expect(key.contains("TestReflectingOptionalDateStored"))
268+
#expect(store.object(forKey: key) != nil)
269+
} else {
270+
Issue.record("AppStorage Date support requires macOS 15.0+")
271+
}
272+
#else
273+
Issue.record("SwiftUI is not available on this platform")
274+
#endif
275+
}
276+
}
277+
278+
#endif

0 commit comments

Comments
 (0)