swift-code-reviewer-skill 1.2.0 → 1.3.0
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 +43 -169
- package/README.md +43 -2
- package/SKILL.md +194 -711
- package/bin/install.js +1 -1
- package/package.json +2 -1
- package/references/agent-loop-feedback.md +148 -0
- package/references/companion-skills.md +70 -0
- package/references/spec-adherence.md +157 -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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# SwiftUI Image Optimization Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [AsyncImage Best Practices](#asyncimage-best-practices)
|
|
6
|
+
- [Image Decoding and Downsampling (Optional Optimization)](#image-decoding-and-downsampling-optional-optimization)
|
|
7
|
+
- [UIImage Loading and Memory](#uiimage-loading-and-memory)
|
|
8
|
+
- [SF Symbols](#sf-symbols)
|
|
9
|
+
- [Summary Checklist](#summary-checklist)
|
|
10
|
+
|
|
11
|
+
## AsyncImage Best Practices
|
|
12
|
+
|
|
13
|
+
### Basic AsyncImage with Phase Handling
|
|
14
|
+
|
|
15
|
+
```swift
|
|
16
|
+
// Good - handles loading and error states
|
|
17
|
+
AsyncImage(url: imageURL) { phase in
|
|
18
|
+
switch phase {
|
|
19
|
+
case .empty:
|
|
20
|
+
ProgressView()
|
|
21
|
+
case .success(let image):
|
|
22
|
+
image
|
|
23
|
+
.resizable()
|
|
24
|
+
.aspectRatio(contentMode: .fit)
|
|
25
|
+
case .failure:
|
|
26
|
+
Image(systemName: "photo")
|
|
27
|
+
.foregroundStyle(.secondary)
|
|
28
|
+
@unknown default:
|
|
29
|
+
EmptyView()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
.frame(width: 200, height: 200)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For custom placeholders, replace `ProgressView()` in the `.empty` case with your placeholder view. Add `.transition(.opacity)` to the success case and `.animation(.easeInOut, value: imageURL)` to the container for fade-in transitions.
|
|
36
|
+
|
|
37
|
+
## Image Decoding and Downsampling (Optional Optimization)
|
|
38
|
+
|
|
39
|
+
**When you encounter `UIImage(data:)` usage, consider suggesting image downsampling as a potential performance improvement**, especially for large images in lists or grids.
|
|
40
|
+
|
|
41
|
+
### Current Pattern That Could Be Optimized
|
|
42
|
+
|
|
43
|
+
```swift
|
|
44
|
+
// Current pattern - decodes full image on main thread
|
|
45
|
+
// Unsafe - force unwrap can crash if imageData is invalid
|
|
46
|
+
Image(uiImage: UIImage(data: imageData)!)
|
|
47
|
+
.resizable()
|
|
48
|
+
.aspectRatio(contentMode: .fit)
|
|
49
|
+
.frame(width: 200, height: 200)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Suggested Optimization Pattern
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
// Suggested optimization - decode and downsample off main thread
|
|
56
|
+
struct OptimizedImageView: View {
|
|
57
|
+
let imageData: Data
|
|
58
|
+
let targetSize: CGSize
|
|
59
|
+
@State private var processedImage: UIImage?
|
|
60
|
+
|
|
61
|
+
var body: some View {
|
|
62
|
+
Group {
|
|
63
|
+
if let processedImage {
|
|
64
|
+
Image(uiImage: processedImage)
|
|
65
|
+
.resizable()
|
|
66
|
+
.aspectRatio(contentMode: .fit)
|
|
67
|
+
} else {
|
|
68
|
+
ProgressView()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
.task {
|
|
72
|
+
processedImage = await decodeAndDownsample(imageData, targetSize: targetSize)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func decodeAndDownsample(_ data: Data, targetSize: CGSize) async -> UIImage? {
|
|
77
|
+
await Task.detached {
|
|
78
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let options: [CFString: Any] = [
|
|
83
|
+
kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height),
|
|
84
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
85
|
+
kCGImageSourceCreateThumbnailWithTransform: true
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return UIImage(cgImage: cgImage)
|
|
93
|
+
}.value
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Usage
|
|
98
|
+
OptimizedImageView(
|
|
99
|
+
imageData: imageData,
|
|
100
|
+
targetSize: CGSize(width: 200, height: 200)
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Reusable Downsampling Actor
|
|
105
|
+
|
|
106
|
+
For production use, wrap the logic in an `actor` with scale-aware sizing and cache-disabled source options:
|
|
107
|
+
|
|
108
|
+
```swift
|
|
109
|
+
actor ImageProcessor {
|
|
110
|
+
func downsample(data: Data, targetSize: CGSize) -> UIImage? {
|
|
111
|
+
let scale = await UIScreen.main.scale
|
|
112
|
+
let maxPixel = max(targetSize.width, targetSize.height) * scale
|
|
113
|
+
let sourceOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
|
|
114
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, sourceOptions as CFDictionary) else { return nil }
|
|
115
|
+
let downsampleOptions: [CFString: Any] = [
|
|
116
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
117
|
+
kCGImageSourceThumbnailMaxPixelSize: maxPixel,
|
|
118
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
119
|
+
kCGImageSourceShouldCacheImmediately: true
|
|
120
|
+
]
|
|
121
|
+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) else { return nil }
|
|
122
|
+
return UIImage(cgImage: cgImage)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Key details: `kCGImageSourceShouldCache: false` on the source prevents the full-resolution image from being cached in memory. Multiplying `targetSize` by `UIScreen.main.scale` ensures the thumbnail is sharp on Retina displays. `kCGImageSourceShouldCacheImmediately: true` on the thumbnail forces decoding at creation time rather than at first render.
|
|
128
|
+
|
|
129
|
+
### When to Suggest This Optimization
|
|
130
|
+
|
|
131
|
+
Mention this optimization when you see `UIImage(data:)` usage, particularly in:
|
|
132
|
+
- Scrollable content (List, ScrollView with LazyVStack/LazyHStack)
|
|
133
|
+
- Grid layouts with many images
|
|
134
|
+
- Image galleries or carousels
|
|
135
|
+
- Any scenario where large images are displayed at smaller sizes
|
|
136
|
+
|
|
137
|
+
**Don't automatically apply it**—present it as an optional improvement for performance-sensitive scenarios.
|
|
138
|
+
|
|
139
|
+
## UIImage Loading and Memory
|
|
140
|
+
|
|
141
|
+
### UIImage(named:) Caches in System Cache
|
|
142
|
+
|
|
143
|
+
`UIImage(named:)` adds images to the system cache, which can cause memory spikes when loading many images (e.g., in a slider or gallery). For single-use or frequently-rotated images, use `UIImage(contentsOfFile:)` to bypass the cache:
|
|
144
|
+
|
|
145
|
+
```swift
|
|
146
|
+
// Caches in system cache -- memory builds up
|
|
147
|
+
let image = UIImage(named: "Wallpapers/image_001.jpg")
|
|
148
|
+
|
|
149
|
+
// No system caching -- memory stays flat
|
|
150
|
+
guard let path = Bundle.main.path(forResource: "Wallpapers/image_001.jpg", ofType: nil) else { return nil }
|
|
151
|
+
let image = UIImage(contentsOfFile: path)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### NSCache for Controlled Image Caching
|
|
155
|
+
|
|
156
|
+
When image processing (resizing, filtering) is needed, use `NSCache` with a `countLimit` to bound memory instead of relying on system caching:
|
|
157
|
+
|
|
158
|
+
```swift
|
|
159
|
+
struct ImageCache {
|
|
160
|
+
private let cache = NSCache<NSString, UIImage>()
|
|
161
|
+
|
|
162
|
+
init(countLimit: Int = 50) {
|
|
163
|
+
cache.countLimit = countLimit
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
subscript(key: String) -> UIImage? {
|
|
167
|
+
get { cache.object(forKey: key as NSString) }
|
|
168
|
+
nonmutating set {
|
|
169
|
+
if let newValue {
|
|
170
|
+
cache.setObject(newValue, forKey: key as NSString)
|
|
171
|
+
} else {
|
|
172
|
+
cache.removeObject(forKey: key as NSString)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## SF Symbols
|
|
180
|
+
|
|
181
|
+
```swift
|
|
182
|
+
Image(systemName: "star.fill")
|
|
183
|
+
.foregroundStyle(.yellow)
|
|
184
|
+
.symbolRenderingMode(.multicolor) // or .hierarchical, .palette, .monochrome
|
|
185
|
+
|
|
186
|
+
// Animated symbols (iOS 17+)
|
|
187
|
+
Image(systemName: "antenna.radiowaves.left.and.right")
|
|
188
|
+
.symbolEffect(.variableColor)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Variants are available via naming convention: `star.circle.fill`, `star.square.fill`, `folder.badge.plus`.
|
|
192
|
+
|
|
193
|
+
## Summary Checklist
|
|
194
|
+
|
|
195
|
+
- [ ] Use `AsyncImage` with proper phase handling
|
|
196
|
+
- [ ] Handle empty, success, and failure states
|
|
197
|
+
- [ ] Consider downsampling for `UIImage(data:)` in performance-sensitive scenarios
|
|
198
|
+
- [ ] Decode and downsample images off the main thread
|
|
199
|
+
- [ ] Use appropriate target sizes for downsampling
|
|
200
|
+
- [ ] Consider image caching for frequently accessed images
|
|
201
|
+
- [ ] Use SF Symbols with appropriate rendering modes
|
|
202
|
+
|
|
203
|
+
**Performance Note**: Image downsampling is an optional optimization. Only suggest it when you encounter `UIImage(data:)` usage in performance-sensitive contexts like scrollable lists or grids.
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# Latest SwiftUI APIs Reference
|
|
2
|
+
|
|
3
|
+
> Based on a comparison of Apple's documentation using the Sosumi MCP, we found the latest recommended APIs to use.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Always Use (iOS 15+)](#always-use-ios-15)
|
|
7
|
+
- [When Targeting iOS 16+](#when-targeting-ios-16)
|
|
8
|
+
- [When Targeting iOS 17+](#when-targeting-ios-17)
|
|
9
|
+
- [When Targeting iOS 18+](#when-targeting-ios-18)
|
|
10
|
+
- [When Targeting iOS 26+](#when-targeting-ios-26)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Always Use (iOS 15+)
|
|
15
|
+
|
|
16
|
+
These APIs have been deprecated long enough that there is no reason to use the old variants.
|
|
17
|
+
|
|
18
|
+
### Compact Replacements
|
|
19
|
+
|
|
20
|
+
These replacements have minimal API shape changes. Most are near-direct swaps; a few require an additional parameter or structural adjustment:
|
|
21
|
+
|
|
22
|
+
- **`navigationTitle(_:)`** instead of `navigationBarTitle(_:)`
|
|
23
|
+
- **`toolbar { ToolbarItem(...) }`** instead of `navigationBarItems(...)` (structural change)
|
|
24
|
+
- **`toolbarVisibility(.hidden, for: .navigationBar)`** instead of `navigationBarHidden(_:)`
|
|
25
|
+
- **`statusBarHidden(_:)`** instead of `statusBar(hidden:)`
|
|
26
|
+
- **`ignoresSafeArea(_:edges:)`** instead of `edgesIgnoringSafeArea(_:)`
|
|
27
|
+
- **`preferredColorScheme(_:)`** instead of `colorScheme(_:)`
|
|
28
|
+
- **`foregroundStyle(_:)`** instead of `foregroundColor(_:)` (e.g., `.foregroundStyle(.primary)`)
|
|
29
|
+
- **`clipShape(.rect(cornerRadius:))`** instead of `cornerRadius()`
|
|
30
|
+
- **`textInputAutocapitalization(_:)`** instead of `autocapitalization(_:)` (note: `.never` replaces `.none`)
|
|
31
|
+
- **`animation(_:value:)`** instead of `animation(_:)` (adds required `value:` parameter; back-deploys to iOS 13+)
|
|
32
|
+
|
|
33
|
+
### Presentation
|
|
34
|
+
|
|
35
|
+
- **Always use `.confirmationDialog(_:isPresented:actions:message:)`** instead of `actionSheet(...)`.
|
|
36
|
+
- **Always use `.alert(_:isPresented:actions:message:)`** instead of `alert(isPresented:content:)`.
|
|
37
|
+
|
|
38
|
+
Both take a title `String`, `isPresented: Binding<Bool>`, an `actions` builder with `Button` items (supporting `role: .destructive` / `.cancel`), and an optional `message` builder:
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
.alert("Delete Item?", isPresented: $showAlert) {
|
|
42
|
+
Button("Delete", role: .destructive) { deleteItem() }
|
|
43
|
+
Button("Cancel", role: .cancel) { }
|
|
44
|
+
} message: {
|
|
45
|
+
Text("This action cannot be undone.")
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Text Input
|
|
50
|
+
|
|
51
|
+
**Always use `onSubmit(of:_:)` and `focused(_:equals:)` instead of `TextField` `onEditingChanged`/`onCommit` callbacks.**
|
|
52
|
+
|
|
53
|
+
```swift
|
|
54
|
+
@FocusState private var isFocused: Bool
|
|
55
|
+
|
|
56
|
+
TextField("Search", text: $query)
|
|
57
|
+
.focused($isFocused)
|
|
58
|
+
.onSubmit { performSearch() }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Accessibility
|
|
62
|
+
|
|
63
|
+
**Always use dedicated accessibility modifiers instead of the generic `accessibility(...)` variants.** Use `.accessibilityLabel()`, `.accessibilityValue()`, `.accessibilityHint()`, `.accessibilityAddTraits()`, `.accessibilityHidden()` instead of `.accessibility(label:)`, `.accessibility(value:)`, etc.
|
|
64
|
+
|
|
65
|
+
### Custom Environment / Container Values
|
|
66
|
+
|
|
67
|
+
**Always use the `@Entry` macro instead of manual `EnvironmentKey` conformance.** The `@Entry` macro was introduced in Xcode 16 and back-deploys to all OS versions.
|
|
68
|
+
|
|
69
|
+
```swift
|
|
70
|
+
// Modern — one line replaces ~10 lines of EnvironmentKey boilerplate
|
|
71
|
+
extension EnvironmentValues {
|
|
72
|
+
@Entry var myCustomValue: String = "Default value"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Styling
|
|
77
|
+
|
|
78
|
+
**Always use `Button` instead of `onTapGesture()` unless you need tap location or count.**
|
|
79
|
+
|
|
80
|
+
```swift
|
|
81
|
+
Button("Tap me") { performAction() }
|
|
82
|
+
|
|
83
|
+
// Use onTapGesture only when you need location or count
|
|
84
|
+
Image("photo")
|
|
85
|
+
.onTapGesture(count: 2) { handleDoubleTap() }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## When Targeting iOS 16+
|
|
91
|
+
|
|
92
|
+
### Navigation
|
|
93
|
+
|
|
94
|
+
**Use `NavigationStack` (or `NavigationSplitView`) instead of `NavigationView`.** Value-based `NavigationLink(value:)` with `.navigationDestination(for:)` replaces destination-based links.
|
|
95
|
+
|
|
96
|
+
```swift
|
|
97
|
+
NavigationStack {
|
|
98
|
+
List(items) { item in
|
|
99
|
+
NavigationLink(value: item) { Text(item.name) }
|
|
100
|
+
}
|
|
101
|
+
.navigationDestination(for: Item.self) { DetailView(item: $0) }
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Simple Renames
|
|
106
|
+
|
|
107
|
+
- **`tint(_:)`** instead of `accentColor(_:)`
|
|
108
|
+
- **`autocorrectionDisabled(_:)`** instead of `disableAutocorrection(_:)`
|
|
109
|
+
|
|
110
|
+
### Clipboard
|
|
111
|
+
|
|
112
|
+
**Prefer `PasteButton` for user-initiated paste UI** to avoid paste prompts. It handles permissions automatically. Use `UIPasteboard` only when you need programmatic or non-`Transferable` clipboard access (triggers the paste permission prompt).
|
|
113
|
+
|
|
114
|
+
```swift
|
|
115
|
+
PasteButton(payloadType: String.self) { strings in
|
|
116
|
+
pastedText = strings.first ?? ""
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## When Targeting iOS 17+
|
|
123
|
+
|
|
124
|
+
### State Management
|
|
125
|
+
|
|
126
|
+
- **Prefer `@Observable` over `ObservableObject` for new code.** Use `@State` instead of `@StateObject`; use `@Bindable` instead of `@ObservedObject`. See `state-management.md` for full `@Observable` migration patterns.
|
|
127
|
+
|
|
128
|
+
### Events
|
|
129
|
+
|
|
130
|
+
**Use `onChange(of:initial:_:)` or `onChange(of:) { }` instead of `onChange(of:perform:)`.**
|
|
131
|
+
|
|
132
|
+
The deprecated variant passes only the new value. The modern variants provide either both old and new values, or a no-parameter closure.
|
|
133
|
+
|
|
134
|
+
- **No-parameter** (most common): `.onChange(of: value) { doSomething() }`
|
|
135
|
+
- **Old and new values**: `.onChange(of: value) { old, new in ... }`
|
|
136
|
+
- **With initial trigger**: `.onChange(of: value, initial: true) { ... }`
|
|
137
|
+
- **Deprecated**: `.onChange(of: value) { newValue in ... }` — single-parameter closure
|
|
138
|
+
|
|
139
|
+
### Gestures
|
|
140
|
+
|
|
141
|
+
- **`MagnifyGesture`** instead of `MagnificationGesture` (access magnitude via `value.magnification`)
|
|
142
|
+
- **`RotateGesture`** instead of `RotationGesture` (access angle via `value.rotation`)
|
|
143
|
+
|
|
144
|
+
### Layout
|
|
145
|
+
|
|
146
|
+
**Consider `containerRelativeFrame()` or `visualEffect()` as alternatives to `GeometryReader` for sizing and position-based effects.** `GeometryReader` is not deprecated and remains necessary for many measurement-based layouts.
|
|
147
|
+
|
|
148
|
+
```swift
|
|
149
|
+
Image("hero")
|
|
150
|
+
.resizable()
|
|
151
|
+
.containerRelativeFrame(.horizontal) { length, axis in length * 0.8 }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- **`visualEffect { content, geometry in ... }`** — position-based effects (parallax, offsets) without a `GeometryReader` wrapper.
|
|
155
|
+
- **`onGeometryChange(for:of:action:)`** — react to geometry changes of a specific view; useful for driving state/effects. `GeometryReader` is still better when layout itself depends on geometry. Note the two-closure shape:
|
|
156
|
+
```swift
|
|
157
|
+
.onGeometryChange(for: CGFloat.self) { proxy in proxy.size.height } action: { newHeight in height = newHeight }
|
|
158
|
+
```
|
|
159
|
+
- **`.coordinateSpace(.named("scroll"))`** instead of `.coordinateSpace(name: "scroll")`.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## When Targeting iOS 18+
|
|
164
|
+
|
|
165
|
+
### Tabs
|
|
166
|
+
|
|
167
|
+
**Use the `Tab` API instead of `tabItem(_:)`.**
|
|
168
|
+
|
|
169
|
+
```swift
|
|
170
|
+
TabView {
|
|
171
|
+
Tab("Home", systemImage: "house") { HomeView() }
|
|
172
|
+
Tab("Search", systemImage: "magnifyingglass") { SearchView() }
|
|
173
|
+
Tab("Profile", systemImage: "person") { ProfileView() }
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
When using `Tab(role:)`, all tabs must use the `Tab` syntax. Mixing `Tab(role:)` with `.tabItem()` causes compilation errors.
|
|
178
|
+
|
|
179
|
+
### Previews
|
|
180
|
+
|
|
181
|
+
**Use `@Previewable` for dynamic properties in previews.**
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
// Modern (iOS 18+)
|
|
185
|
+
#Preview {
|
|
186
|
+
@Previewable @State var isOn = false
|
|
187
|
+
Toggle("Setting", isOn: $isOn)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## When Targeting iOS 26+
|
|
194
|
+
|
|
195
|
+
For Liquid Glass APIs (`glassEffect`, `GlassEffectContainer`, glass button styles), see [liquid-glass.md](liquid-glass.md).
|
|
196
|
+
|
|
197
|
+
### Scroll Edge Effects
|
|
198
|
+
|
|
199
|
+
**Use `scrollEdgeEffectStyle(_:for:)` to configure scroll edge behavior.**
|
|
200
|
+
|
|
201
|
+
```swift
|
|
202
|
+
ScrollView {
|
|
203
|
+
// content
|
|
204
|
+
}
|
|
205
|
+
.scrollEdgeEffectStyle(.soft, for: .top)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Background Extension
|
|
209
|
+
|
|
210
|
+
**Use `backgroundExtensionEffect()` for edge-extending blurred backgrounds.**
|
|
211
|
+
|
|
212
|
+
Views behind a Liquid Glass sidebar can appear clipped. This modifier mirrors and blurs content outside the safe area so artwork remains visible.
|
|
213
|
+
|
|
214
|
+
```swift
|
|
215
|
+
Image("hero")
|
|
216
|
+
.backgroundExtensionEffect()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
|
|
220
|
+
|
|
221
|
+
### Tab Bar
|
|
222
|
+
|
|
223
|
+
**Use `tabBarMinimizeBehavior(_:)` to control tab bar minimization on scroll.**
|
|
224
|
+
|
|
225
|
+
```swift
|
|
226
|
+
TabView {
|
|
227
|
+
// tabs
|
|
228
|
+
}
|
|
229
|
+
.tabBarMinimizeBehavior(.onScrollDown)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Use `tabViewBottomAccessory` for persistent controls above the tab bar.** Read `tabViewBottomAccessoryPlacement` from the environment to adapt content when the accessory collapses into the tab bar area.
|
|
233
|
+
|
|
234
|
+
```swift
|
|
235
|
+
TabView {
|
|
236
|
+
// tabs
|
|
237
|
+
}
|
|
238
|
+
.tabViewBottomAccessory {
|
|
239
|
+
NowPlayingBar()
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Use `Tab(role: .search)` for a dedicated search tab.** The tab separates from the rest and morphs into a search field when selected.
|
|
244
|
+
|
|
245
|
+
```swift
|
|
246
|
+
TabView {
|
|
247
|
+
Tab("Home", systemImage: "house") { HomeView() }
|
|
248
|
+
Tab("Profile", systemImage: "person") { ProfileView() }
|
|
249
|
+
Tab(role: .search) { SearchResultsView() }
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> Source: "What's new in SwiftUI" (WWDC25, session 256) and "Build a SwiftUI app with the new design" (WWDC25, session 323)
|
|
254
|
+
|
|
255
|
+
### Toolbars
|
|
256
|
+
|
|
257
|
+
**Use `ToolbarSpacer` to control grouping of toolbar items.** Fixed spacers visually separate related groups; flexible spacers push items apart.
|
|
258
|
+
|
|
259
|
+
```swift
|
|
260
|
+
.toolbar {
|
|
261
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
262
|
+
Button("Up", systemImage: "chevron.up") { }
|
|
263
|
+
}
|
|
264
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
265
|
+
Button("Down", systemImage: "chevron.down") { }
|
|
266
|
+
}
|
|
267
|
+
ToolbarSpacer(.fixed)
|
|
268
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
269
|
+
Button("Settings", systemImage: "gear") { }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Use `sharedBackgroundVisibility(.hidden)` to remove the glass group background from an individual toolbar item.**
|
|
275
|
+
|
|
276
|
+
```swift
|
|
277
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
278
|
+
Image(systemName: "person.circle.fill")
|
|
279
|
+
.sharedBackgroundVisibility(.hidden)
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Use `badge(_:)` on toolbar item content to display an indicator.**
|
|
284
|
+
|
|
285
|
+
```swift
|
|
286
|
+
ToolbarItem(placement: .topBarTrailing) {
|
|
287
|
+
Button("Notifications", systemImage: "bell") { }
|
|
288
|
+
.badge(unreadCount)
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
> Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
|
|
293
|
+
|
|
294
|
+
### Search
|
|
295
|
+
|
|
296
|
+
**Use `searchToolbarBehavior(.minimizable)` to opt into a minimized search button.** The system may automatically minimize search into a toolbar button depending on available space. Use this modifier to explicitly opt in.
|
|
297
|
+
|
|
298
|
+
```swift
|
|
299
|
+
NavigationStack {
|
|
300
|
+
ContentView()
|
|
301
|
+
.searchable(text: $query)
|
|
302
|
+
.searchToolbarBehavior(.minimizable)
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
> Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
|
|
307
|
+
|
|
308
|
+
### Animations
|
|
309
|
+
|
|
310
|
+
**Use `@Animatable` macro instead of manual `animatableData` declarations.** The macro auto-synthesizes `animatableData` from all animatable properties. Use `@AnimatableIgnored` to exclude specific properties.
|
|
311
|
+
|
|
312
|
+
```swift
|
|
313
|
+
@Animatable
|
|
314
|
+
struct Wedge: Shape {
|
|
315
|
+
var startAngle: Angle
|
|
316
|
+
var endAngle: Angle
|
|
317
|
+
@AnimatableIgnored var drawClockwise: Bool
|
|
318
|
+
|
|
319
|
+
func path(in rect: CGRect) -> Path { /* ... */ }
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
> Source: "What's new in SwiftUI" (WWDC25, session 256)
|
|
324
|
+
|
|
325
|
+
### Presentations
|
|
326
|
+
|
|
327
|
+
**Use `navigationZoomTransition` to morph sheets out of their source view.** Toolbar items and buttons can serve as the transition source.
|
|
328
|
+
|
|
329
|
+
```swift
|
|
330
|
+
.toolbar {
|
|
331
|
+
ToolbarItem {
|
|
332
|
+
Button("Add", systemImage: "plus") { showSheet = true }
|
|
333
|
+
.navigationTransitionSource(id: "addSheet", namespace: namespace)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
.sheet(isPresented: $showSheet) {
|
|
337
|
+
AddItemView()
|
|
338
|
+
.navigationTransitionDestination(id: "addSheet", namespace: namespace)
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
> Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
|
|
343
|
+
|
|
344
|
+
### Controls
|
|
345
|
+
|
|
346
|
+
**Use `controlSize(.extraLarge)` for extra-large prominent action buttons.**
|
|
347
|
+
|
|
348
|
+
```swift
|
|
349
|
+
Button("Get Started") { }
|
|
350
|
+
.buttonStyle(.borderedProminent)
|
|
351
|
+
.controlSize(.extraLarge)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Use `concentric` corner style for buttons that match their container's corners.**
|
|
355
|
+
|
|
356
|
+
```swift
|
|
357
|
+
Button("Confirm") { }
|
|
358
|
+
.clipShape(.rect(cornerRadius: 12, style: .concentric))
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Sliders now support tick marks and a neutral value.**
|
|
362
|
+
|
|
363
|
+
```swift
|
|
364
|
+
Slider(value: $speed, in: 0.5...2.0, step: 0.25) {
|
|
365
|
+
Text("Speed")
|
|
366
|
+
} ticks: {
|
|
367
|
+
SliderTick(value: 0.6)
|
|
368
|
+
SliderTick(value: 0.9)
|
|
369
|
+
}
|
|
370
|
+
.sliderNeutralValue(1.0)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
> Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
|
|
374
|
+
|
|
375
|
+
### Rich Text
|
|
376
|
+
|
|
377
|
+
**Use `TextEditor` with an `AttributedString` binding for rich text editing.** Supports bold, italic, underline, strikethrough, custom fonts, foreground/background colors, paragraph styles, and Genmoji.
|
|
378
|
+
|
|
379
|
+
```swift
|
|
380
|
+
@State private var text: AttributedString = "Hello, world!"
|
|
381
|
+
|
|
382
|
+
var body: some View {
|
|
383
|
+
TextEditor(text: $text)
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
> Source: "Cook up a rich text experience in SwiftUI with AttributedString" (WWDC25, session 280)
|
|
388
|
+
|
|
389
|
+
### Web Content
|
|
390
|
+
|
|
391
|
+
**Use `WebView` to display web content.** For richer interaction, create a `WebPage` observable model.
|
|
392
|
+
|
|
393
|
+
```swift
|
|
394
|
+
// Simple URL display
|
|
395
|
+
WebView(url: URL(string: "https://example.com")!)
|
|
396
|
+
|
|
397
|
+
// With observable model
|
|
398
|
+
@State private var page = WebPage()
|
|
399
|
+
|
|
400
|
+
WebView(page)
|
|
401
|
+
.onAppear { page.load(URLRequest(url: myURL)) }
|
|
402
|
+
.navigationTitle(page.title ?? "")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
> Source: "Meet WebKit for SwiftUI" (WWDC25, session 231)
|
|
406
|
+
|
|
407
|
+
### Drag and Drop
|
|
408
|
+
|
|
409
|
+
**Use `dragContainer` for multi-item drag operations.** Combine with `DragConfiguration` for custom drag behavior and `onDragSessionUpdated` to observe events.
|
|
410
|
+
|
|
411
|
+
```swift
|
|
412
|
+
PhotoGrid(photos: photos)
|
|
413
|
+
.dragContainer(for: Photo.self) { selection in
|
|
414
|
+
return selection.map { $0.transferable }
|
|
415
|
+
}
|
|
416
|
+
.onDragSessionUpdated { session in
|
|
417
|
+
if session.phase == .endedWithDelete {
|
|
418
|
+
deleteSelectedPhotos()
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
> Source: "What's new in SwiftUI" (WWDC25, session 256)
|
|
424
|
+
|
|
425
|
+
### Scene Bridging
|
|
426
|
+
|
|
427
|
+
**UIKit and AppKit lifecycle apps can now request SwiftUI scenes.** This enables using SwiftUI-only scene types like `MenuBarExtra` and `ImmersiveSpace` from imperative lifecycle apps via `UIApplication.shared.activateSceneSession(for:errorHandler:)`.
|
|
428
|
+
|
|
429
|
+
> Source: "What's new in SwiftUI" (WWDC25, session 256)
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Quick Lookup Table
|
|
434
|
+
|
|
435
|
+
| Deprecated | Recommended | Since |
|
|
436
|
+
|-----------|-------------|-------|
|
|
437
|
+
| `navigationBarTitle(_:)` | `navigationTitle(_:)` | iOS 15+ |
|
|
438
|
+
| `navigationBarItems(...)` | `toolbar { ToolbarItem(...) }` | iOS 15+ |
|
|
439
|
+
| `navigationBarHidden(_:)` | `toolbarVisibility(.hidden, for: .navigationBar)` | iOS 15+ |
|
|
440
|
+
| `statusBar(hidden:)` | `statusBarHidden(_:)` | iOS 15+ |
|
|
441
|
+
| `edgesIgnoringSafeArea(_:)` | `ignoresSafeArea(_:edges:)` | iOS 15+ |
|
|
442
|
+
| `colorScheme(_:)` | `preferredColorScheme(_:)` | iOS 15+ |
|
|
443
|
+
| `foregroundColor(_:)` | `foregroundStyle(_:)` | iOS 15+ |
|
|
444
|
+
| `cornerRadius(_:)` | `clipShape(.rect(cornerRadius:))` | iOS 15+ |
|
|
445
|
+
| `actionSheet(...)` | `confirmationDialog(...)` | iOS 15+ |
|
|
446
|
+
| `alert(isPresented:content:)` | `alert(_:isPresented:actions:message:)` | iOS 15+ |
|
|
447
|
+
| `autocapitalization(_:)` | `textInputAutocapitalization(_:)` | iOS 15+ |
|
|
448
|
+
| `accessibility(label:)` etc. | `accessibilityLabel()` etc. | iOS 15+ |
|
|
449
|
+
| `TextField` `onCommit`/`onEditingChanged` | `onSubmit` + `focused` | iOS 15+ |
|
|
450
|
+
| `animation(_:)` (no value) | `animation(_:value:)` | Back-deploys (iOS 13+) |
|
|
451
|
+
| Manual `EnvironmentKey` | `@Entry` macro | Back-deploys (Xcode 16+) |
|
|
452
|
+
| `NavigationView` | `NavigationStack` / `NavigationSplitView` | iOS 16+ |
|
|
453
|
+
| `accentColor(_:)` | `tint(_:)` | iOS 16+ |
|
|
454
|
+
| `disableAutocorrection(_:)` | `autocorrectionDisabled(_:)` | iOS 16+ |
|
|
455
|
+
| `UIPasteboard.general` | `PasteButton` | iOS 16+ |
|
|
456
|
+
| `onChange(of:perform:)` | `onChange(of:) { }` or `onChange(of:) { old, new in }` | iOS 17+ |
|
|
457
|
+
| `MagnificationGesture` | `MagnifyGesture` | iOS 17+ |
|
|
458
|
+
| `RotationGesture` | `RotateGesture` | iOS 17+ |
|
|
459
|
+
| `coordinateSpace(name:)` | `coordinateSpace(.named(...))` | iOS 17+ |
|
|
460
|
+
| `ObservableObject` | `@Observable` | iOS 17+ |
|
|
461
|
+
| `tabItem(_:)` | `Tab` API | iOS 18+ |
|
|
462
|
+
| Manual `animatableData` | `@Animatable` macro | iOS 26+ |
|
|
463
|
+
| `presentationBackground(_:)` on sheets | Default Liquid Glass sheet material | iOS 26+ |
|
|
464
|
+
| Custom toolbar background hacks | `scrollEdgeEffectStyle(_:for:)` | iOS 26+ |
|