275 lines
7.7 KiB
Markdown
275 lines
7.7 KiB
Markdown
---
|
|
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/swiftui-advanced.mdc
|
|
confidence: 0.9
|
|
namespace: work
|
|
last_synced: 2026-04-28
|
|
alwaysApply: false
|
|
---
|
|
|
|
# Advanced SwiftUI Patterns
|
|
|
|
## ViewModifier Composition
|
|
Create reusable ViewModifiers for common styling:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - Composable ViewModifiers
|
|
struct DuckDuckGoButtonStyle: ViewModifier {
|
|
let style: ButtonStyleType
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
.background(backgroundColorForStyle())
|
|
.foregroundColor(textColorForStyle())
|
|
.cornerRadius(8)
|
|
.font(.body.weight(.medium))
|
|
}
|
|
|
|
private func backgroundColorForStyle() -> Color {
|
|
switch style {
|
|
case .primary:
|
|
return Color(designSystemColor: .buttonPrimaryBackground)
|
|
case .secondary:
|
|
return Color(designSystemColor: .buttonSecondaryBackground)
|
|
case .ghost:
|
|
return .clear
|
|
}
|
|
}
|
|
|
|
private func textColorForStyle() -> Color {
|
|
switch style {
|
|
case .primary:
|
|
return Color(designSystemColor: .buttonPrimaryText)
|
|
case .secondary:
|
|
return Color(designSystemColor: .buttonSecondaryText)
|
|
case .ghost:
|
|
return Color(designSystemColor: .textLink)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
extension View {
|
|
func duckDuckGoButtonStyle(_ style: ButtonStyleType) -> some View {
|
|
modifier(DuckDuckGoButtonStyle(style: style))
|
|
}
|
|
}
|
|
```
|
|
|
|
## PreferenceKey for Cross-View Communication
|
|
Use PreferenceKey for sophisticated view communication:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - PreferenceKey for collecting data from child views
|
|
struct ViewSizePreferenceKey: PreferenceKey {
|
|
static var defaultValue: [String: CGSize] = [:]
|
|
|
|
static func reduce(value: inout [String: CGSize], nextValue: () -> [String: CGSize]) {
|
|
value.merge(nextValue()) { $1 }
|
|
}
|
|
}
|
|
|
|
struct SizeReportingView<Content: View>: View {
|
|
let id: String
|
|
let content: Content
|
|
|
|
init(id: String, @ViewBuilder content: () -> Content) {
|
|
self.id = id
|
|
self.content = content()
|
|
}
|
|
|
|
var body: some View {
|
|
content
|
|
.background(
|
|
GeometryReader { geometry in
|
|
Color.clear
|
|
.preference(key: ViewSizePreferenceKey.self,
|
|
value: [id: geometry.size])
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
struct ParentView: View {
|
|
@State private var childSizes: [String: CGSize] = [:]
|
|
|
|
var body: some View {
|
|
VStack {
|
|
SizeReportingView(id: "header") {
|
|
HeaderView()
|
|
}
|
|
|
|
SizeReportingView(id: "content") {
|
|
ContentView()
|
|
}
|
|
}
|
|
.onPreferenceChange(ViewSizePreferenceKey.self) { sizes in
|
|
self.childSizes = sizes
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Environment-based Dependency Injection
|
|
Use SwiftUI Environment for dependency injection:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - Environment-based DI
|
|
struct DependencyProviderKey: EnvironmentKey {
|
|
static let defaultValue: DependencyProvider = AppDependencyProvider.shared
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var dependencies: DependencyProvider {
|
|
get { self[DependencyProviderKey.self] }
|
|
set { self[DependencyProviderKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
// Usage in views
|
|
struct FeatureView: View {
|
|
@Environment(\.dependencies) var dependencies
|
|
@StateObject private var viewModel: FeatureViewModel
|
|
|
|
init() {
|
|
// Note: This approach has limitations - see ios-architecture.md for preferred DI pattern
|
|
}
|
|
|
|
var body: some View {
|
|
// View implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
## Complex Animation Patterns
|
|
Use sophisticated animations for better UX:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - Coordinated animations
|
|
struct TabSwitcherView: View {
|
|
@State private var selectedTab: Int = 0
|
|
@State private var animationPhase: AnimationPhase = .idle
|
|
|
|
enum AnimationPhase {
|
|
case idle, switching, settled
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
TabPickerView(selectedTab: $selectedTab)
|
|
.animation(.easeInOut(duration: 0.3), value: selectedTab)
|
|
|
|
TabContent(selectedTab: selectedTab)
|
|
.transition(.asymmetric(
|
|
insertion: .move(edge: .trailing).combined(with: .opacity),
|
|
removal: .move(edge: .leading).combined(with: .opacity)
|
|
))
|
|
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: selectedTab)
|
|
}
|
|
.onChange(of: selectedTab) { newValue in
|
|
withAnimation(.easeInOut(duration: 0.1)) {
|
|
animationPhase = .switching
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
withAnimation(.easeOut(duration: 0.1)) {
|
|
animationPhase = .settled
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Custom Layout with Layout Protocol (iOS 16+)
|
|
Create sophisticated layouts:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - Custom layout for complex arrangements
|
|
@available(iOS 16.0, macOS 13.0, *)
|
|
struct FlexibleGrid: Layout {
|
|
let spacing: CGFloat
|
|
let itemSize: CGSize
|
|
|
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
let containerWidth = proposal.width ?? 300
|
|
let itemsPerRow = max(1, Int(containerWidth / (itemSize.width + spacing)))
|
|
let rows = (subviews.count + itemsPerRow - 1) / itemsPerRow
|
|
|
|
let totalWidth = CGFloat(itemsPerRow) * itemSize.width + CGFloat(itemsPerRow - 1) * spacing
|
|
let totalHeight = CGFloat(rows) * itemSize.height + CGFloat(rows - 1) * spacing
|
|
|
|
return CGSize(width: totalWidth, height: totalHeight)
|
|
}
|
|
|
|
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
let containerWidth = bounds.width
|
|
let itemsPerRow = max(1, Int(containerWidth / (itemSize.width + spacing)))
|
|
|
|
for (index, subview) in subviews.enumerated() {
|
|
let row = index / itemsPerRow
|
|
let column = index % itemsPerRow
|
|
|
|
let x = bounds.minX + CGFloat(column) * (itemSize.width + spacing)
|
|
let y = bounds.minY + CGFloat(row) * (itemSize.height + spacing)
|
|
|
|
subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(itemSize))
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## State Management with @Observable (iOS 17+)
|
|
Use the modern @Observable macro when available:
|
|
|
|
```swift
|
|
// ✅ MODERN - @Observable for iOS 17+
|
|
@available(iOS 17.0, macOS 14.0, *)
|
|
@Observable
|
|
final class ModernViewModel {
|
|
var items: [Item] = []
|
|
var isLoading = false
|
|
var selectedItem: Item?
|
|
|
|
private let service: FeatureServiceProtocol
|
|
|
|
init(service: FeatureServiceProtocol) {
|
|
self.service = service
|
|
}
|
|
|
|
@MainActor
|
|
func loadData() async {
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
items = try await service.fetchItems()
|
|
} catch {
|
|
// Handle error
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
struct ModernFeatureView: View {
|
|
@State private var viewModel: ModernViewModel
|
|
|
|
init(service: FeatureServiceProtocol) {
|
|
self._viewModel = State(initialValue: ModernViewModel(service: service))
|
|
}
|
|
|
|
var body: some View {
|
|
List(viewModel.items, selection: $viewModel.selectedItem) { item in
|
|
ItemRow(item: item)
|
|
}
|
|
.task {
|
|
await viewModel.loadData()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
See `swiftui-style.md` for basic SwiftUI patterns and design system integration.
|