Skip to content

Commit 259fd07

Browse files
leogdionclaude
andcommitted
Refactor file organization for improved maintainability
Split large test and source files into focused, maintainable units: **AppStorageDateTests Split:** - Created enum-based test suite pattern with extensions - Separated non-optional date tests into AppStorageDateTests+NonOptional.swift - Separated optional date tests into AppStorageDateTests+Optional.swift - Main file now contains enum namespace and shared test helper types - All 7 tests organized into 2 logical suites with @suite attributes **ObservableDownloader Split:** - Extracted internal implementation helpers to ObservableDownloader+Internal.swift - Main file focuses on public API and core class definition - Improved separation of concerns while maintaining functionality **DevContainer Updates:** - Added Swift 6.2 and 6.3 devcontainer configurations - Updated existing devcontainer images to stable releases - Improved container naming for clarity All changes verified with swift test (7/7 tests passing) and lint checks (0 violations). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 755878f commit 259fd07

File tree

10 files changed

+458
-244
lines changed

10 files changed

+458
-244
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Swift",
3-
"image": "swiftlang/swift:nightly-6.1-jammy",
3+
"image": "swift:6.2",
44
"features": {
55
"ghcr.io/devcontainers/features/common-utils:2": {
66
"installZsh": "false",

.devcontainer/swift-6.0/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "Swift",
2+
"name": "Swift 6.0",
33
"image": "swift:6.0",
44
"features": {
55
"ghcr.io/devcontainers/features/common-utils:2": {

.devcontainer/swift-6.1/devcontainer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "Swift",
3-
"image": "swiftlang/swift:nightly-6.1-jammy",
2+
"name": "Swift 6.1",
3+
"image": "swift:6.1",
44
"features": {
55
"ghcr.io/devcontainers/features/common-utils:2": {
66
"installZsh": "false",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "Swift 6.2",
3+
"image": "swiftlang/swift:6.2",
4+
"features": {
5+
"ghcr.io/devcontainers/features/common-utils:2": {
6+
"installZsh": "false",
7+
"username": "vscode",
8+
"upgradePackages": "false"
9+
},
10+
"ghcr.io/devcontainers/features/git:1": {
11+
"version": "os-provided",
12+
"ppa": "false"
13+
}
14+
},
15+
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
16+
"runArgs": [
17+
"--cap-add=SYS_PTRACE",
18+
"--security-opt",
19+
"seccomp=unconfined"
20+
],
21+
// Configure tool-specific properties.
22+
"customizations": {
23+
// Configure properties specific to VS Code.
24+
"vscode": {
25+
// Set *default* container specific settings.json values on container create.
26+
"settings": {
27+
"lldb.library": "/usr/lib/liblldb.so"
28+
},
29+
// Add the IDs of extensions you want installed when the container is created.
30+
"extensions": [
31+
"sswg.swift-lang"
32+
]
33+
}
34+
},
35+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
36+
// "forwardPorts": [],
37+
38+
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
39+
"remoteUser": "root"
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "Swift 6.3",
3+
"image": "swiftlang/swift:nightly-6.3-jammy",
4+
"features": {
5+
"ghcr.io/devcontainers/features/common-utils:2": {
6+
"installZsh": "false",
7+
"username": "vscode",
8+
"upgradePackages": "false"
9+
},
10+
"ghcr.io/devcontainers/features/git:1": {
11+
"version": "os-provided",
12+
"ppa": "false"
13+
}
14+
},
15+
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
16+
"runArgs": [
17+
"--cap-add=SYS_PTRACE",
18+
"--security-opt",
19+
"seccomp=unconfined"
20+
],
21+
// Configure tool-specific properties.
22+
"customizations": {
23+
// Configure properties specific to VS Code.
24+
"vscode": {
25+
// Set *default* container specific settings.json values on container create.
26+
"settings": {
27+
"lldb.library": "/usr/lib/liblldb.so"
28+
},
29+
// Add the IDs of extensions you want installed when the container is created.
30+
"extensions": [
31+
"sswg.swift-lang"
32+
]
33+
}
34+
},
35+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
36+
// "forwardPorts": [],
37+
38+
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
39+
"remoteUser": "root"
40+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// ObservableDownloader+Internal.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(Combine) && canImport(Observation)
31+
import Combine
32+
import Foundation
33+
34+
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
35+
extension ObservableDownloader {
36+
// MARK: - Internal Implementation
37+
38+
/// Calls the completion closure with the result of the download.
39+
internal func onCompletion(_ result: Result<Void, any Error>) {
40+
assert(completion != nil)
41+
completion?(result)
42+
}
43+
44+
internal func finishedDownloadingToAsync(_ location: URL) {
45+
locationURLSubject.send(location)
46+
}
47+
48+
internal func progressUpdatedAsync(_ progress: DownloadUpdate) {
49+
self.downloadUpdate.send(progress)
50+
}
51+
52+
internal func didCompleteAsync(withError error: (any Error)?) {
53+
guard let error else {
54+
// Handle success case.
55+
return
56+
}
57+
let userInfo = (error as NSError).userInfo
58+
if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
59+
resumeDataSubject.send(resumeData)
60+
}
61+
}
62+
}
63+
#endif

Sources/RadiantProgress/ObservableDownloader.swift

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
internal let requestSubject = PassthroughSubject<DownloadRequest, Never>()
7575
internal let locationURLSubject = PassthroughSubject<URL, Never>()
7676
internal let downloadUpdate = PassthroughSubject<DownloadUpdate, Never>()
77-
private var completion: ((Result<Void, any Error>) -> Void)?
77+
internal var completion: ((Result<Void, any Error>) -> Void)?
7878

7979
private let formatter = ByteCountFormatter()
8080

@@ -161,12 +161,6 @@
161161
self.cancellables = setupPublishers(self)
162162
}
163163

164-
/// Calls the completion closure with the result of the download.
165-
internal func onCompletion(_ result: Result<Void, any Error>) {
166-
assert(completion != nil)
167-
completion?(result)
168-
}
169-
170164
/// Cancels the current download task.
171165
public func cancel() { task?.cancel() }
172166

@@ -208,25 +202,6 @@
208202
Task { @MainActor in self.didCompleteAsync(withError: error) }
209203
}
210204

211-
private func finishedDownloadingToAsync(_ location: URL) {
212-
locationURLSubject.send(location)
213-
}
214-
215-
private func progressUpdatedAsync(_ progress: DownloadUpdate) {
216-
self.downloadUpdate.send(progress)
217-
}
218-
219-
private func didCompleteAsync(withError error: (any Error)?) {
220-
guard let error else {
221-
// Handle success case.
222-
return
223-
}
224-
let userInfo = (error as NSError).userInfo
225-
if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
226-
resumeDataSubject.send(resumeData)
227-
}
228-
}
229-
230205
deinit {
231206
MainActor.assumeIsolated {
232207
for cancellable in self.cancellables { cancellable.cancel() }
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//
2+
// AppStorageDateTests+NonOptional.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+
import Foundation
31+
import RadiantKit
32+
import Testing
33+
34+
#if canImport(SwiftUI)
35+
import SwiftUI
36+
#endif
37+
38+
extension AppStorageDateTests {
39+
@Suite("Non-Optional Date Storage Tests")
40+
@MainActor
41+
internal struct NonOptionalDateTests {
42+
@Test(.enabled(if: AppStorageDateTests.isAppStorageDateAvailable))
43+
internal func testNonOptionalDateStorage() async throws {
44+
#if canImport(SwiftUI)
45+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
46+
guard let store = UserDefaults(suiteName: #function) else {
47+
Issue.record("Failed to create UserDefaults")
48+
return
49+
}
50+
defer { store.removePersistentDomain(forName: #function) }
51+
52+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
53+
let storage = AppStorage(
54+
wrappedValue: testDate,
55+
for: TestDateStored.self,
56+
store: store
57+
)
58+
59+
#expect(storage.wrappedValue == testDate)
60+
} else {
61+
Issue.record("AppStorage Date support requires macOS 15.0+")
62+
}
63+
#else
64+
Issue.record("SwiftUI is not available on this platform")
65+
#endif
66+
}
67+
68+
@Test(.enabled(if: AppStorageDateTests.isAppStorageDateAvailable))
69+
internal func testNonOptionalDateUpdate() async throws {
70+
#if canImport(SwiftUI)
71+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
72+
guard let store = UserDefaults(suiteName: #function) else {
73+
Issue.record("Failed to create UserDefaults")
74+
return
75+
}
76+
defer { store.removePersistentDomain(forName: #function) }
77+
78+
let initialDate = Date(timeIntervalSince1970: 1_704_067_200)
79+
let storage = AppStorage(
80+
wrappedValue: initialDate,
81+
for: TestDateStored.self,
82+
store: store
83+
)
84+
85+
#expect(storage.wrappedValue == initialDate)
86+
87+
let newDate = Date(timeIntervalSince1970: 1_735_689_600)
88+
storage.wrappedValue = newDate
89+
90+
#expect(storage.wrappedValue == newDate)
91+
#expect(store.object(forKey: TestDateStored.key) != nil)
92+
} else {
93+
Issue.record("AppStorage Date support requires macOS 15.0+")
94+
}
95+
#else
96+
Issue.record("SwiftUI is not available on this platform")
97+
#endif
98+
}
99+
100+
@Test(.enabled(if: AppStorageDateTests.isAppStorageDateAvailable))
101+
internal func testNonOptionalDateWithReflectingKey() async throws {
102+
#if canImport(SwiftUI)
103+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
104+
guard let store = UserDefaults(suiteName: #function) else {
105+
Issue.record("Failed to create UserDefaults")
106+
return
107+
}
108+
defer { store.removePersistentDomain(forName: #function) }
109+
110+
let testDate = Date(timeIntervalSince1970: 1_704_067_200)
111+
let storage = AppStorage(
112+
wrappedValue: testDate,
113+
for: TestReflectingDateStored.self,
114+
store: store
115+
)
116+
117+
#expect(storage.wrappedValue == testDate)
118+
119+
let key = TestReflectingDateStored.key
120+
#expect(key.contains("TestReflectingDateStored"))
121+
} else {
122+
Issue.record("AppStorage Date support requires macOS 15.0+")
123+
}
124+
#else
125+
Issue.record("SwiftUI is not available on this platform")
126+
#endif
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)