swift-code-reviewer-skill 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -162
- package/README.md +91 -21
- package/SKILL.md +107 -725
- package/bin/install.js +87 -22
- package/package.json +16 -2
- package/references/companion-skills.md +70 -0
- package/skills/README.md +43 -0
- package/skills/swift-concurrency/NOTICE.md +18 -0
- package/skills/swift-concurrency/SKILL.md +235 -0
- package/skills/swift-concurrency/references/actors.md +640 -0
- package/skills/swift-concurrency/references/async-await-basics.md +249 -0
- package/skills/swift-concurrency/references/async-sequences.md +635 -0
- package/skills/swift-concurrency/references/core-data.md +533 -0
- package/skills/swift-concurrency/references/glossary.md +96 -0
- package/skills/swift-concurrency/references/linting.md +38 -0
- package/skills/swift-concurrency/references/memory-management.md +542 -0
- package/skills/swift-concurrency/references/migration.md +721 -0
- package/skills/swift-concurrency/references/performance.md +574 -0
- package/skills/swift-concurrency/references/sendable.md +578 -0
- package/skills/swift-concurrency/references/tasks.md +604 -0
- package/skills/swift-concurrency/references/testing.md +565 -0
- package/skills/swift-concurrency/references/threading.md +452 -0
- package/skills/swift-expert/NOTICE.md +18 -0
- package/skills/swift-expert/SKILL.md +226 -0
- package/skills/swift-expert/references/async-concurrency.md +363 -0
- package/skills/swift-expert/references/memory-performance.md +380 -0
- package/skills/swift-expert/references/protocol-oriented.md +357 -0
- package/skills/swift-expert/references/swiftui-patterns.md +294 -0
- package/skills/swift-expert/references/testing-patterns.md +402 -0
- package/skills/swift-testing/NOTICE.md +18 -0
- package/skills/swift-testing/SKILL.md +295 -0
- package/skills/swift-testing/references/async-testing.md +245 -0
- package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
- package/skills/swift-testing/references/fixtures.md +193 -0
- package/skills/swift-testing/references/integration-testing.md +189 -0
- package/skills/swift-testing/references/migration-xctest.md +301 -0
- package/skills/swift-testing/references/parameterized-tests.md +171 -0
- package/skills/swift-testing/references/snapshot-testing.md +201 -0
- package/skills/swift-testing/references/test-doubles.md +243 -0
- package/skills/swift-testing/references/test-organization.md +231 -0
- package/skills/swiftui-expert-skill/NOTICE.md +18 -0
- package/skills/swiftui-expert-skill/SKILL.md +281 -0
- package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
- package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
- package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
- package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
- package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/skills/swiftui-expert-skill/references/state-management.md +417 -0
- package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
- package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
- package/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/templates/agents/swift-code-reviewer.md +78 -0
- package/templates/commands/review.md +56 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# SwiftUI Patterns
|
|
2
|
+
|
|
3
|
+
> Reference for: Swift Expert
|
|
4
|
+
> Load when: Building SwiftUI views, state management, custom modifiers
|
|
5
|
+
|
|
6
|
+
## State Management
|
|
7
|
+
|
|
8
|
+
```swift
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
// @State for local view state
|
|
12
|
+
struct CounterView: View {
|
|
13
|
+
@State private var count = 0
|
|
14
|
+
|
|
15
|
+
var body: some View {
|
|
16
|
+
VStack {
|
|
17
|
+
Text("Count: \(count)")
|
|
18
|
+
Button("Increment") { count += 1 }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// @Binding for two-way data flow
|
|
24
|
+
struct ToggleView: View {
|
|
25
|
+
@Binding var isOn: Bool
|
|
26
|
+
|
|
27
|
+
var body: some View {
|
|
28
|
+
Toggle("Enable Feature", isOn: $isOn)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// @StateObject for observable objects (view owns it)
|
|
33
|
+
class ViewModel: ObservableObject {
|
|
34
|
+
@Published var items: [String] = []
|
|
35
|
+
@Published var isLoading = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
struct ContentView: View {
|
|
39
|
+
@StateObject private var viewModel = ViewModel()
|
|
40
|
+
|
|
41
|
+
var body: some View {
|
|
42
|
+
List(viewModel.items, id: \.self) { item in
|
|
43
|
+
Text(item)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// @ObservedObject for passed-in observable objects
|
|
49
|
+
struct DetailView: View {
|
|
50
|
+
@ObservedObject var viewModel: ViewModel
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// @EnvironmentObject for dependency injection
|
|
54
|
+
struct AppView: View {
|
|
55
|
+
@EnvironmentObject var appState: AppState
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Modern View Composition
|
|
60
|
+
|
|
61
|
+
```swift
|
|
62
|
+
// View builder for custom containers
|
|
63
|
+
struct ConditionalView<Content: View>: View {
|
|
64
|
+
let condition: Bool
|
|
65
|
+
@ViewBuilder let content: () -> Content
|
|
66
|
+
|
|
67
|
+
var body: some View {
|
|
68
|
+
if condition {
|
|
69
|
+
content()
|
|
70
|
+
} else {
|
|
71
|
+
EmptyView()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Custom ViewModifier
|
|
77
|
+
struct CardModifier: ViewModifier {
|
|
78
|
+
func body(content: Content) -> some View {
|
|
79
|
+
content
|
|
80
|
+
.padding()
|
|
81
|
+
.background(Color.white)
|
|
82
|
+
.cornerRadius(12)
|
|
83
|
+
.shadow(radius: 4)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
extension View {
|
|
88
|
+
func cardStyle() -> some View {
|
|
89
|
+
modifier(CardModifier())
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Usage
|
|
94
|
+
Text("Hello")
|
|
95
|
+
.cardStyle()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Environment Values
|
|
99
|
+
|
|
100
|
+
```swift
|
|
101
|
+
// Custom environment key
|
|
102
|
+
private struct ThemeKey: EnvironmentKey {
|
|
103
|
+
static let defaultValue: Theme = .light
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
extension EnvironmentValues {
|
|
107
|
+
var theme: Theme {
|
|
108
|
+
get { self[ThemeKey.self] }
|
|
109
|
+
set { self[ThemeKey.self] = newValue }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
extension View {
|
|
114
|
+
func theme(_ theme: Theme) -> some View {
|
|
115
|
+
environment(\.theme, theme)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Usage
|
|
120
|
+
struct ThemedView: View {
|
|
121
|
+
@Environment(\.theme) var theme
|
|
122
|
+
|
|
123
|
+
var body: some View {
|
|
124
|
+
Text("Themed")
|
|
125
|
+
.foregroundColor(theme.textColor)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Preference Keys
|
|
131
|
+
|
|
132
|
+
```swift
|
|
133
|
+
// Collecting data from child views
|
|
134
|
+
struct SizePreferenceKey: PreferenceKey {
|
|
135
|
+
static var defaultValue: CGSize = .zero
|
|
136
|
+
|
|
137
|
+
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
|
138
|
+
value = nextValue()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
struct MeasurableView: View {
|
|
143
|
+
@State private var size: CGSize = .zero
|
|
144
|
+
|
|
145
|
+
var body: some View {
|
|
146
|
+
Text("Measure me")
|
|
147
|
+
.background(
|
|
148
|
+
GeometryReader { geometry in
|
|
149
|
+
Color.clear
|
|
150
|
+
.preference(key: SizePreferenceKey.self, value: geometry.size)
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
.onPreferenceChange(SizePreferenceKey.self) { newSize in
|
|
154
|
+
size = newSize
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Animations
|
|
161
|
+
|
|
162
|
+
```swift
|
|
163
|
+
// Implicit animations
|
|
164
|
+
struct AnimatedView: View {
|
|
165
|
+
@State private var scale: CGFloat = 1.0
|
|
166
|
+
|
|
167
|
+
var body: some View {
|
|
168
|
+
Circle()
|
|
169
|
+
.scaleEffect(scale)
|
|
170
|
+
.animation(.spring(response: 0.5, dampingFraction: 0.6), value: scale)
|
|
171
|
+
.onTapGesture {
|
|
172
|
+
scale = scale == 1.0 ? 1.5 : 1.0
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Explicit animations
|
|
178
|
+
struct ExplicitAnimationView: View {
|
|
179
|
+
@State private var offset: CGFloat = 0
|
|
180
|
+
|
|
181
|
+
var body: some View {
|
|
182
|
+
Text("Slide")
|
|
183
|
+
.offset(x: offset)
|
|
184
|
+
.onTapGesture {
|
|
185
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
186
|
+
offset = offset == 0 ? 100 : 0
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Custom transitions
|
|
193
|
+
extension AnyTransition {
|
|
194
|
+
static var slideAndFade: AnyTransition {
|
|
195
|
+
AnyTransition.slide.combined(with: .opacity)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Async/Await Integration
|
|
201
|
+
|
|
202
|
+
```swift
|
|
203
|
+
struct AsyncDataView: View {
|
|
204
|
+
@State private var data: [Item] = []
|
|
205
|
+
@State private var isLoading = false
|
|
206
|
+
|
|
207
|
+
var body: some View {
|
|
208
|
+
List(data) { item in
|
|
209
|
+
Text(item.title)
|
|
210
|
+
}
|
|
211
|
+
.task {
|
|
212
|
+
await loadData()
|
|
213
|
+
}
|
|
214
|
+
.refreshable {
|
|
215
|
+
await loadData()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func loadData() async {
|
|
220
|
+
isLoading = true
|
|
221
|
+
defer { isLoading = false }
|
|
222
|
+
|
|
223
|
+
do {
|
|
224
|
+
data = try await API.fetchItems()
|
|
225
|
+
} catch {
|
|
226
|
+
print("Error: \(error)")
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Custom Layouts (iOS 16+)
|
|
233
|
+
|
|
234
|
+
```swift
|
|
235
|
+
struct WaterfallLayout: Layout {
|
|
236
|
+
var columns: Int = 2
|
|
237
|
+
var spacing: CGFloat = 8
|
|
238
|
+
|
|
239
|
+
func sizeThatFits(
|
|
240
|
+
proposal: ProposedViewSize,
|
|
241
|
+
subviews: Subviews,
|
|
242
|
+
cache: inout ()
|
|
243
|
+
) -> CGSize {
|
|
244
|
+
// Calculate total size needed
|
|
245
|
+
let columnWidth = (proposal.width! - spacing * CGFloat(columns - 1)) / CGFloat(columns)
|
|
246
|
+
var columnHeights = Array(repeating: CGFloat(0), count: columns)
|
|
247
|
+
|
|
248
|
+
for subview in subviews {
|
|
249
|
+
let column = columnHeights.enumerated().min(by: { $0.element < $1.element })!.offset
|
|
250
|
+
let size = subview.sizeThatFits(.init(width: columnWidth, height: nil))
|
|
251
|
+
columnHeights[column] += size.height + spacing
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return CGSize(
|
|
255
|
+
width: proposal.width!,
|
|
256
|
+
height: columnHeights.max()! - spacing
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func placeSubviews(
|
|
261
|
+
in bounds: CGRect,
|
|
262
|
+
proposal: ProposedViewSize,
|
|
263
|
+
subviews: Subviews,
|
|
264
|
+
cache: inout ()
|
|
265
|
+
) {
|
|
266
|
+
let columnWidth = (bounds.width - spacing * CGFloat(columns - 1)) / CGFloat(columns)
|
|
267
|
+
var columnHeights = Array(repeating: CGFloat(0), count: columns)
|
|
268
|
+
|
|
269
|
+
for subview in subviews {
|
|
270
|
+
let column = columnHeights.enumerated().min(by: { $0.element < $1.element })!.offset
|
|
271
|
+
let x = bounds.minX + CGFloat(column) * (columnWidth + spacing)
|
|
272
|
+
let y = bounds.minY + columnHeights[column]
|
|
273
|
+
|
|
274
|
+
subview.place(
|
|
275
|
+
at: CGPoint(x: x, y: y),
|
|
276
|
+
proposal: .init(width: columnWidth, height: nil)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
columnHeights[column] += subview.dimensions(in: .init(width: columnWidth, height: nil)).height + spacing
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Performance Tips
|
|
286
|
+
|
|
287
|
+
- Use `@State` for simple value types
|
|
288
|
+
- Use `@StateObject` for reference types you create
|
|
289
|
+
- Use `@ObservedObject` for reference types passed in
|
|
290
|
+
- Prefer `@Environment` over prop drilling
|
|
291
|
+
- Use `equatable()` modifier for expensive views
|
|
292
|
+
- Leverage `id()` modifier to control view identity
|
|
293
|
+
- Use `task(id:)` to cancel and restart async work
|
|
294
|
+
- Avoid computing expensive values in body - use `@State` or computed properties
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
> Reference for: Swift Expert
|
|
4
|
+
> Load when: XCTest, async testing, mocking, test strategies
|
|
5
|
+
|
|
6
|
+
## XCTest Basics
|
|
7
|
+
|
|
8
|
+
```swift
|
|
9
|
+
import XCTest
|
|
10
|
+
@testable import MyApp
|
|
11
|
+
|
|
12
|
+
final class UserTests: XCTestCase {
|
|
13
|
+
var sut: UserManager!
|
|
14
|
+
|
|
15
|
+
override func setUp() {
|
|
16
|
+
super.setUp()
|
|
17
|
+
sut = UserManager()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override func tearDown() {
|
|
21
|
+
sut = nil
|
|
22
|
+
super.tearDown()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testUserCreation() {
|
|
26
|
+
// Given
|
|
27
|
+
let name = "John Doe"
|
|
28
|
+
let email = "john@example.com"
|
|
29
|
+
|
|
30
|
+
// When
|
|
31
|
+
let user = sut.createUser(name: name, email: email)
|
|
32
|
+
|
|
33
|
+
// Then
|
|
34
|
+
XCTAssertEqual(user.name, name)
|
|
35
|
+
XCTAssertEqual(user.email, email)
|
|
36
|
+
XCTAssertNotNil(user.id)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func testValidation() throws {
|
|
40
|
+
// Unwrapping optionals in tests
|
|
41
|
+
let user = try XCTUnwrap(sut.findUser(id: 123))
|
|
42
|
+
XCTAssertEqual(user.name, "Test User")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Async Testing
|
|
48
|
+
|
|
49
|
+
```swift
|
|
50
|
+
final class AsyncTests: XCTestCase {
|
|
51
|
+
func testAsyncFunction() async throws {
|
|
52
|
+
// Test async/await code directly
|
|
53
|
+
let result = try await fetchData()
|
|
54
|
+
XCTAssertEqual(result.count, 10)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func testAsyncSequence() async throws {
|
|
58
|
+
var results: [Int] = []
|
|
59
|
+
|
|
60
|
+
for try await value in numberStream() {
|
|
61
|
+
results.append(value)
|
|
62
|
+
if results.count >= 5 {
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
XCTAssertEqual(results.count, 5)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func testWithTimeout() async throws {
|
|
71
|
+
// Test with timeout
|
|
72
|
+
try await withTimeout(seconds: 5) {
|
|
73
|
+
try await longRunningOperation()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func testConcurrentOperations() async throws {
|
|
78
|
+
async let result1 = fetchData(id: 1)
|
|
79
|
+
async let result2 = fetchData(id: 2)
|
|
80
|
+
|
|
81
|
+
let (data1, data2) = try await (result1, result2)
|
|
82
|
+
|
|
83
|
+
XCTAssertNotNil(data1)
|
|
84
|
+
XCTAssertNotNil(data2)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Helper for timeout
|
|
89
|
+
func withTimeout<T>(
|
|
90
|
+
seconds: TimeInterval,
|
|
91
|
+
operation: @escaping () async throws -> T
|
|
92
|
+
) async throws -> T {
|
|
93
|
+
try await withThrowingTaskGroup(of: T.self) { group in
|
|
94
|
+
group.addTask {
|
|
95
|
+
try await operation()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
group.addTask {
|
|
99
|
+
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
|
100
|
+
throw TimeoutError()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let result = try await group.next()!
|
|
104
|
+
group.cancelAll()
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Mocking
|
|
111
|
+
|
|
112
|
+
```swift
|
|
113
|
+
// Protocol for dependency injection
|
|
114
|
+
protocol DataService {
|
|
115
|
+
func fetch(id: Int) async throws -> Data
|
|
116
|
+
func save(_ data: Data) async throws
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Production implementation
|
|
120
|
+
class APIDataService: DataService {
|
|
121
|
+
func fetch(id: Int) async throws -> Data {
|
|
122
|
+
// Real API call
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func save(_ data: Data) async throws {
|
|
126
|
+
// Real save operation
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Mock for testing
|
|
131
|
+
class MockDataService: DataService {
|
|
132
|
+
var fetchCalled = false
|
|
133
|
+
var fetchID: Int?
|
|
134
|
+
var fetchResult: Data?
|
|
135
|
+
var fetchError: Error?
|
|
136
|
+
|
|
137
|
+
var saveCalled = false
|
|
138
|
+
var savedData: Data?
|
|
139
|
+
var saveError: Error?
|
|
140
|
+
|
|
141
|
+
func fetch(id: Int) async throws -> Data {
|
|
142
|
+
fetchCalled = true
|
|
143
|
+
fetchID = id
|
|
144
|
+
|
|
145
|
+
if let error = fetchError {
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return fetchResult ?? Data()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func save(_ data: Data) async throws {
|
|
153
|
+
saveCalled = true
|
|
154
|
+
savedData = data
|
|
155
|
+
|
|
156
|
+
if let error = saveError {
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Using mock in tests
|
|
163
|
+
final class DataManagerTests: XCTestCase {
|
|
164
|
+
func testDataFetch() async throws {
|
|
165
|
+
// Given
|
|
166
|
+
let mockService = MockDataService()
|
|
167
|
+
mockService.fetchResult = "test data".data(using: .utf8)
|
|
168
|
+
let manager = DataManager(service: mockService)
|
|
169
|
+
|
|
170
|
+
// When
|
|
171
|
+
let result = try await manager.loadData(id: 123)
|
|
172
|
+
|
|
173
|
+
// Then
|
|
174
|
+
XCTAssertTrue(mockService.fetchCalled)
|
|
175
|
+
XCTAssertEqual(mockService.fetchID, 123)
|
|
176
|
+
XCTAssertNotNil(result)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Test Doubles
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
// Spy - records interactions
|
|
185
|
+
class SpyDelegate: UserManagerDelegate {
|
|
186
|
+
private(set) var didUpdateUserCalled = false
|
|
187
|
+
private(set) var updatedUser: User?
|
|
188
|
+
private(set) var callCount = 0
|
|
189
|
+
|
|
190
|
+
func didUpdateUser(_ user: User) {
|
|
191
|
+
didUpdateUserCalled = true
|
|
192
|
+
updatedUser = user
|
|
193
|
+
callCount += 1
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Stub - provides predetermined responses
|
|
198
|
+
class StubNetworkService: NetworkService {
|
|
199
|
+
var stubbedResponse: Result<Data, Error> = .success(Data())
|
|
200
|
+
|
|
201
|
+
func fetch(url: URL) async throws -> Data {
|
|
202
|
+
try stubbedResponse.get()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fake - working implementation with shortcuts
|
|
207
|
+
class FakeDatabase: Database {
|
|
208
|
+
private var storage: [String: Data] = [:]
|
|
209
|
+
|
|
210
|
+
func save(key: String, value: Data) {
|
|
211
|
+
storage[key] = value
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func load(key: String) -> Data? {
|
|
215
|
+
storage[key]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func clear() {
|
|
219
|
+
storage.removeAll()
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Performance Testing
|
|
225
|
+
|
|
226
|
+
```swift
|
|
227
|
+
final class PerformanceTests: XCTestCase {
|
|
228
|
+
func testSortingPerformance() {
|
|
229
|
+
let numbers = (0..<10000).shuffled()
|
|
230
|
+
|
|
231
|
+
measure {
|
|
232
|
+
_ = numbers.sorted()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func testCustomMetrics() {
|
|
237
|
+
let metrics: [XCTMetric] = [
|
|
238
|
+
XCTClockMetric(),
|
|
239
|
+
XCTCPUMetric(),
|
|
240
|
+
XCTMemoryMetric(),
|
|
241
|
+
XCTStorageMetric()
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
let options = XCTMeasureOptions()
|
|
245
|
+
options.iterationCount = 10
|
|
246
|
+
|
|
247
|
+
measure(metrics: metrics, options: options) {
|
|
248
|
+
performExpensiveOperation()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## UI Testing
|
|
255
|
+
|
|
256
|
+
```swift
|
|
257
|
+
final class AppUITests: XCTestCase {
|
|
258
|
+
var app: XCUIApplication!
|
|
259
|
+
|
|
260
|
+
override func setUp() {
|
|
261
|
+
super.setUp()
|
|
262
|
+
continueAfterFailure = false
|
|
263
|
+
app = XCUIApplication()
|
|
264
|
+
app.launch()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func testLoginFlow() {
|
|
268
|
+
// Test UI interactions
|
|
269
|
+
let emailField = app.textFields["Email"]
|
|
270
|
+
emailField.tap()
|
|
271
|
+
emailField.typeText("test@example.com")
|
|
272
|
+
|
|
273
|
+
let passwordField = app.secureTextFields["Password"]
|
|
274
|
+
passwordField.tap()
|
|
275
|
+
passwordField.typeText("password123")
|
|
276
|
+
|
|
277
|
+
app.buttons["Login"].tap()
|
|
278
|
+
|
|
279
|
+
// Verify navigation
|
|
280
|
+
XCTAssertTrue(app.navigationBars["Dashboard"].exists)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func testButtonEnabled() {
|
|
284
|
+
let button = app.buttons["Submit"]
|
|
285
|
+
XCTAssertFalse(button.isEnabled)
|
|
286
|
+
|
|
287
|
+
app.textFields["Username"].tap()
|
|
288
|
+
app.textFields["Username"].typeText("testuser")
|
|
289
|
+
|
|
290
|
+
XCTAssertTrue(button.isEnabled)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Testing Actors
|
|
296
|
+
|
|
297
|
+
```swift
|
|
298
|
+
final class ActorTests: XCTestCase {
|
|
299
|
+
func testActorIsolation() async throws {
|
|
300
|
+
actor Counter {
|
|
301
|
+
private var value = 0
|
|
302
|
+
|
|
303
|
+
func increment() -> Int {
|
|
304
|
+
value += 1
|
|
305
|
+
return value
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func reset() {
|
|
309
|
+
value = 0
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let counter = Counter()
|
|
314
|
+
|
|
315
|
+
// Test concurrent access
|
|
316
|
+
await withTaskGroup(of: Int.self) { group in
|
|
317
|
+
for _ in 0..<100 {
|
|
318
|
+
group.addTask {
|
|
319
|
+
await counter.increment()
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let finalValue = await counter.increment()
|
|
325
|
+
XCTAssertEqual(finalValue, 101)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Snapshot Testing
|
|
331
|
+
|
|
332
|
+
```swift
|
|
333
|
+
import SnapshotTesting
|
|
334
|
+
|
|
335
|
+
final class ViewSnapshotTests: XCTestCase {
|
|
336
|
+
func testButtonAppearance() {
|
|
337
|
+
let button = UIButton()
|
|
338
|
+
button.setTitle("Tap Me", for: .normal)
|
|
339
|
+
button.backgroundColor = .blue
|
|
340
|
+
button.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
|
|
341
|
+
|
|
342
|
+
assertSnapshot(matching: button, as: .image)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func testViewControllerLayout() {
|
|
346
|
+
let vc = MyViewController()
|
|
347
|
+
assertSnapshot(matching: vc, as: .image(on: .iPhone13))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func testDarkMode() {
|
|
351
|
+
let view = MyView()
|
|
352
|
+
assertSnapshot(matching: view, as: .image(traits: .init(userInterfaceStyle: .dark)))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Test Organization
|
|
358
|
+
|
|
359
|
+
```swift
|
|
360
|
+
// MARK: - Test Cases
|
|
361
|
+
extension UserManagerTests {
|
|
362
|
+
// MARK: Creation Tests
|
|
363
|
+
func testUserCreation() { }
|
|
364
|
+
func testUserCreationWithInvalidData() { }
|
|
365
|
+
|
|
366
|
+
// MARK: Validation Tests
|
|
367
|
+
func testEmailValidation() { }
|
|
368
|
+
func testPasswordValidation() { }
|
|
369
|
+
|
|
370
|
+
// MARK: Persistence Tests
|
|
371
|
+
func testUserSave() { }
|
|
372
|
+
func testUserLoad() { }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// MARK: - Test Helpers
|
|
376
|
+
extension UserManagerTests {
|
|
377
|
+
func makeTestUser() -> User {
|
|
378
|
+
User(name: "Test", email: "test@example.com")
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
func setupMockData() {
|
|
382
|
+
// Common test setup
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Best Practices
|
|
388
|
+
|
|
389
|
+
- Use `@testable import` to test internal types
|
|
390
|
+
- One assertion concept per test (can have multiple XCTAssert calls)
|
|
391
|
+
- Use Given-When-Then pattern for clarity
|
|
392
|
+
- Name tests descriptively: `test_methodName_condition_expectedResult`
|
|
393
|
+
- Use setUp/tearDown for common test setup
|
|
394
|
+
- Prefer dependency injection for testability
|
|
395
|
+
- Use protocols to enable mocking
|
|
396
|
+
- Test edge cases and error conditions
|
|
397
|
+
- Use async/await for testing async code
|
|
398
|
+
- Measure performance with XCTest metrics
|
|
399
|
+
- Use UI testing for critical user flows
|
|
400
|
+
- Mock external dependencies
|
|
401
|
+
- Keep tests fast and independent
|
|
402
|
+
- Use test doubles appropriately (mock, stub, spy, fake)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# swift-testing — Attribution Notice
|
|
2
|
+
|
|
3
|
+
Bundled into `swift-code-reviewer-skill` on 2026-04-21 from
|
|
4
|
+
`~/.claude/skills/swift-testing` (resolved from symlink `~/.agents/skills/swift-testing`).
|
|
5
|
+
|
|
6
|
+
Primary author (best-effort attribution): **Antoine van der Lee ([@AvdLee](https://github.com/AvdLee))**
|
|
7
|
+
Also credited: [@Dimillian](https://github.com/Dimillian), [@bocato](https://github.com/bocato)
|
|
8
|
+
|
|
9
|
+
These three authors' public Swift/SwiftUI content — including SwiftLee, IceCubesApp,
|
|
10
|
+
and their various open-source contributions — informed the skills bundled here.
|
|
11
|
+
|
|
12
|
+
License: the upstream folder did not contain a LICENSE file at the time of vendoring.
|
|
13
|
+
Content is reproduced here in good faith for reference alongside this MIT-licensed
|
|
14
|
+
project. If you are an upstream author and want the attribution corrected, the license
|
|
15
|
+
clarified, or the content removed, please open an issue at:
|
|
16
|
+
https://github.com/Viniciuscarvalho/swift-code-reviewer-skill/issues
|
|
17
|
+
|
|
18
|
+
Changes from upstream: none (verbatim copy; `.DS_Store` files excluded).
|