āļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ SwiftUI: āļāļēāļĢāļāļĢāļąāļāđāļāđāļ LazyVStack āđāļĨāļ°āļĢāļēāļĒāļāļēāļĢāļāļĩāđāļāļąāļāļāđāļāļ
āđāļāļāļāļīāļāļāļēāļĢāļāļĢāļąāļāđāļāđāļāļŠāļģāļŦāļĢāļąāļ LazyVStack āđāļĨāļ°āļĢāļēāļĒāļāļēāļĢ SwiftUI āļĨāļāļāļēāļĢāđāļāđāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģ āļāļĢāļąāļāļāļĢāļļāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļēāļĢāđāļĨāļ·āđāļāļ āđāļĨāļ°āļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļāļāđāļāļāļīāļāļāļĨāļēāļāļāļĩāđāļāļāļāđāļāļĒ

āļĢāļēāļĒāļāļēāļĢāđāļāđāļāļŦāļāļķāđāļāđāļāļŠāđāļ§āļāļāļĢāļ°āļāļāļāļāļĩāđāđāļāđāļāđāļāļĒāļāļĩāđāļŠāļļāļāđāļāđāļāļāļāļĨāļīāđāļāļāļąāļ iOS LazyVStack āđāļĨāļ° List āļāļāļ SwiftUI āđāļŦāđāđāļāļĨāļđāļāļąāļāļāļĩāđāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāđāļāļāļēāļĢāđāļŠāļāļāļāļāļĨāđāļĨāļāļāļąāļāļāđāļāļĄāļđāļĨ āđāļāđāļāļēāļĢāđāļāđāļāļēāļāļāļĩāđāđāļĄāđāļāļđāļāļāđāļāļāļāļēāļāļĨāļāļāļĢāļ°āļŠāļāļāļēāļĢāļāđāļāļđāđāđāļāđāđāļāđāļāļĒāđāļēāļāļĢāļ§āļāđāļĢāđāļ§ āļāļēāļĢāđāļāđāļēāđāļāļāļĨāđāļāļ āļēāļĒāđāļāļāļāļāļŠāđāļ§āļāļāļĢāļ°āļāļāļāđāļŦāļĨāđāļēāļāļĩāđāļāđāļ§āļĒāļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļāļāđāļāļāļīāļāļāļĨāļēāļāļāļĩāđāļāļāļāđāļāļĒāđāļĨāļ°āļŠāļĢāđāļēāļāļāļīāļāđāļāļāļĢāđāđāļāļāļāļĩāđāļĨāļ·āđāļāđāļŦāļĨ
āļāļāļāļ§āļēāļĄāļāļĩāđāļāļģāđāļŠāļāļāđāļāļāļāļīāļāļāļēāļĢāļāļĢāļąāļāđāļāđāļāļāļĩāđāļāļģāđāļāđāļāļŠāļģāļŦāļĢāļąāļāļĢāļēāļĒāļāļēāļĢ SwiftUI: lazy loading āļāļēāļĢāļĢāļĩāđāļāđāļāļīāļĨāļ§āļīāļ§ āļāļēāļĢāļāļąāļāļāļēāļĢāļāļąāļ§āļĢāļ°āļāļļ āđāļĨāļ°āļĢāļđāļāđāļāļāļāļąāđāļāļŠāļđāļāļŠāļģāļŦāļĢāļąāļāļāļļāļāļāđāļāļĄāļđāļĨāļāļāļēāļāđāļŦāļāđ
āļāļģāļāļ§āļēāļĄāđāļāđāļēāđāļ Lazy Loading āđāļ SwiftUI
Lazy loading āļāļ°āļŠāļĢāđāļēāļāļ§āļīāļ§āđāļāļāļēāļ°āđāļĄāļ·āđāļāļāļĢāļēāļāļāļāļāļŦāļāđāļēāļāļāđāļāđāļēāļāļąāđāļ āļāđāļēāļāļāļēāļ VStack āļāļĩāđāļŠāļĢāđāļēāļāļ§āļīāļ§āļĨāļđāļāļāļąāđāļāļŦāļĄāļāļāļąāļāļāļĩ LazyVStack āļāļ°āđāļĨāļ·āđāļāļāļāļēāļĢāļŠāļĢāđāļēāļāļāļĩāđāļāļāļāđāļ āļĨāļāļāļēāļĢāđāļāđāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģāđāļĨāļ°āđāļ§āļĨāļēāđāļāļāļēāļĢāđāļĢāļāđāļāļāļĢāđāđāļĢāļīāđāļĄāļāđāļāđāļāđāļāļĒāđāļēāļāļĄāļēāļ
import SwiftUI
// â Problem: VStack instantiates all 10,000 views immediately
struct NonLazyListView: View {
let items = (1...10000).map { "Item \($0)" }
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
// Each view is created at launch
ExpensiveRowView(title: item)
}
}
}
}
}
// â
Solution: LazyVStack creates views on demand
struct LazyListView: View {
let items = (1...10000).map { "Item \($0)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
// Only visible views are created
ExpensiveRowView(title: item)
}
}
}
}
}āļāļ§āļēāļĄāđāļāļāļāđāļēāļāļāļāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļ°āđāļŦāđāļāđāļāđāļāļąāļāđāļāļāđāļĄāđāļāļąāļāđāļāļĩāļĒāļāđāļĄāđāļāļĩāđāļĢāđāļāļĒāļĢāļēāļĒāļāļēāļĢ āļāđāļ§āļĒ 10,000 āļĢāļēāļĒāļāļēāļĢ VStack āļāļēāļāđāļāđāđāļ§āļĨāļēāļŦāļĨāļēāļĒāļ§āļīāļāļēāļāļĩāđāļāļāļēāļĢāđāļĢāļīāđāļĄāļāđāļ āđāļāļāļāļ°āļāļĩāđ LazyVStack āļĒāļąāļāļāļāļāļāļāļŠāļāļāļāļāļąāļāļāļĩ
āļāļēāļĢāļ§āļąāļāļāļĨāļāļĢāļ°āļāļāļāļāļ Lazy Loading
Instruments āļāđāļ§āļĒāđāļŦāđāļŠāļēāļĄāļēāļĢāļāļ§āļąāļāļāļēāļĢāđāļāđāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģāđāļĨāļ° CPU āđāļāđāļāļĒāđāļēāļāđāļĄāđāļāļĒāļģ āļāļĩāđāļāļ·āļāļ§āļīāļ§āļāļāļŠāļāļāļāļĩāđāđāļŠāļāļāļāļ§āļēāļĄāđāļāļāļāđāļēāļ:
struct ExpensiveRowView: View {
let title: String
// Simulating expensive initialization
init(title: String) {
self.title = title
// Log to visualize when the view is created
print("Creating row: \(title)")
}
var body: some View {
HStack {
// Image with processing
Circle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(title)
.font(.headline)
Text("Subtitle with computation")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding()
}
}āđāļĄāļ·āđāļāļĢāļąāļāļāđāļ§āļĒ VStack āļĨāđāļāļāļāļąāđāļāļŦāļĄāļ 10,000 āļĢāļēāļĒāļāļēāļĢāļāļ°āļāļĢāļēāļāļāļāļąāļāļāļĩ āļāđāļ§āļĒ LazyVStack āļĄāļĩāđāļāļĩāļĒāļāļĢāļēāļĒāļāļēāļĢāļāļĩāđāļĄāļāļāđāļŦāđāļāđāļāđ (āļāļĢāļ°āļĄāļēāļ 15-20 āļāļķāđāļāļāļĒāļđāđāļāļąāļāļāļāļēāļāļŦāļāđāļēāļāļ) āļāļĩāđāļāļđāļāļĨāđāļāļāđāļāļāļāļāđāļĢāļ āđāļāļĒāļĄāļĩāļĢāļēāļĒāļāļēāļĢāđāļāļīāđāļĄāđāļāļīāļĄāļāļĢāļēāļāļāļāļķāđāļāđāļĄāļ·āđāļāđāļĨāļ·āđāļāļ
LazyVStack āđāļāđāļāļ§āļīāļ§āļāļĩāđāļŠāļĢāđāļēāļāđāļ§āđāđāļāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģāļŦāļĨāļąāļāļāļēāļāļāļĩāđāļāļĢāļēāļāļ āļāđāļēāļāļāļēāļ List āļāļĩāđāļĢāļĩāđāļāđāļāļīāļĨāđāļāļĨāļĨāđāļāļĒāđāļēāļāđāļāļāļāļĩāļ āļ§āļīāļ§āđāļ LazyVStack āļāļ°āļāļāļāļĒāļđāđāļāļāļāļ§āđāļēāļŠāđāļ§āļāļāļĢāļ°āļāļāļāļŦāļĨāļąāļāļāļ°āļāļđāļāļāļģāļĨāļēāļĒ
āļāļ§āļēāļĄāļŠāļģāļāļąāļāļāļāļāļāļąāļ§āļĢāļ°āļāļļāļāļĩāđāđāļŠāļāļĩāļĒāļĢ
āļāļąāļ§āļĢāļ°āļāļļāđāļāđāļāļāļĨāđāļāļŦāļĨāļąāļāļāļāļāļāļēāļĢāļāļąāļāđāļāļāļĢāļēāļĒāļāļēāļĢ SwiftUI āļāļąāļ§āļĢāļ°āļāļļāļāļĩāđāđāļĄāđāđāļŠāļāļĩāļĒāļĢāļāļģāđāļŦāđāđāļāļīāļāļāļēāļĢāļŠāļĢāđāļēāļāļ§āļīāļ§āđāļŦāļĄāđāļāļĩāđāđāļĄāđāļāļģāđāļāđāļāđāļĨāļ°āļāļēāļāļāļĢāļ°āļāļļāđāļāļāļąāđāļāļāļēāļāļŠāļēāļĒāļāļē āđāļāđāļ āđāļāļāļīāđāļĄāļāļąāļāļāļĩāđāļāļīāļāļāļĨāļēāļāļŦāļĢāļ·āļāļāļēāļĢāļŠāļđāļāđāļŠāļĩāļĒāļāļģāđāļŦāļāđāļāļāļēāļĢāđāļĨāļ·āđāļāļ
// â Problem: using index as identifier
struct UnstableIdentifierView: View {
@State private var items = ["A", "B", "C", "D"]
var body: some View {
List {
// Index changes if an element is deleted
ForEach(items.indices, id: \.self) { index in
Text(items[index])
}
}
}
}
// â Problem: using UUID() in ForEach
struct RegeneratedIdentifierView: View {
let items = ["A", "B", "C", "D"]
var body: some View {
List {
// UUID() generates a new ID on each render
ForEach(items, id: \.self) { item in
// Subtle issue if items contain duplicates
Text(item)
}
}
}
}
// â
Solution: model with stable identifier
struct Item: Identifiable {
let id: UUID // Created once
var name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
struct StableIdentifierView: View {
@State private var items = [
Item(name: "A"),
Item(name: "B"),
Item(name: "C"),
Item(name: "D")
]
var body: some View {
List {
// id is stable for the item's lifetime
ForEach(items) { item in
Text(item.name)
}
}
}
}āļāļēāļĢāđāļāđāļāļąāļ§āļĢāļ°āļāļļāļāļĩāđāđāļĄāđāļāđāļģāļāļąāļāđāļĨāļ°āļāļāļāļāļāđāļ§āļĒāđāļŦāđ SwiftUI āļŠāļēāļĄāļēāļĢāļāđāļĒāļāđāļĒāļ°āļāļāļāđāļāļĢāļ°āļāļāļāđāļāđāļāļĒāđāļēāļāļāļđāļāļāđāļāļāļĢāļ°āļŦāļ§āđāļēāļāļāļēāļĢāļāļąāļāđāļāļ āđāļāļāļīāđāļĄāļāļąāļ āđāļĨāļ°āļāļēāļĢāđāļāļĢāļĩāļĒāļāđāļāļĩāļĒāļ
āļāļēāļĢāļāļĢāļąāļāđāļāđāļāđāļāļĨāļĨāđāļāđāļ§āļĒ Equatable
SwiftUI āđāļāļĢāļĩāļĒāļāđāļāļĩāļĒāļāļ§āļīāļ§āđāļāļ·āđāļāļāļīāļāļēāļĢāļāļēāļ§āđāļēāļāļģāđāļāđāļāļāđāļāļāđāļĢāļāđāļāļāļĢāđāđāļŦāļĄāđāļŦāļĢāļ·āļāđāļĄāđ āđāļāļĒāļāđāļēāđāļĢāļīāđāļĄāļāđāļ āļāļēāļĢāđāļāļĢāļĩāļĒāļāđāļāļĩāļĒāļāļāļĩāđāđāļāđ reflection āļāļķāđāļāļāļēāļāļĄāļĩāļāđāļēāđāļāđāļāđāļēāļĒāļŠāļđāļ āļāļēāļĢāđāļāđ Equatable āļāđāļ§āļĒāđāļŦāđāļŠāļēāļĄāļēāļĢāļāđāļāļĢāļĩāļĒāļāđāļāļĩāļĒāļāđāļāđāļāļĒāđāļēāļāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāđāļĨāļ°āļāļąāļāđāļāļ
// Data model
struct Contact: Identifiable, Equatable {
let id: UUID
var name: String
var email: String
var avatarURL: URL?
var lastActivity: Date
// Custom comparison: ignore lastActivity
// if other properties are identical
static func == (lhs: Contact, rhs: Contact) -> Bool {
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.email == rhs.email &&
lhs.avatarURL == rhs.avatarURL
// lastActivity intentionally excluded
}
}
// Optimized cell view
struct ContactRow: View, Equatable {
let contact: Contact
// Explicit comparison to avoid unnecessary re-renders
static func == (lhs: ContactRow, rhs: ContactRow) -> Bool {
lhs.contact == rhs.contact
}
var body: some View {
HStack(spacing: 12) {
// Async avatar
AsyncImage(url: contact.avatarURL) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "person.circle.fill")
.foregroundStyle(.gray)
default:
ProgressView()
}
}
.frame(width: 44, height: 44)
.clipShape(Circle())
// Contact information
VStack(alignment: .leading, spacing: 2) {
Text(contact.name)
.font(.body.weight(.medium))
Text(contact.email)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
}
}
// List using EquatableView
struct ContactListView: View {
let contacts: [Contact]
var body: some View {
List {
ForEach(contacts) { contact in
// EquatableView prevents re-renders if contact unchanged
EquatableView(content: ContactRow(contact: contact))
}
}
}
}āļāļēāļĢāļāļĢāļąāļāđāļāđāļāļāļĩāđāļĨāļāļ āļēāļĢāļ° CPU āļāļĒāđāļēāļāļĄāļĩāļāļąāļĒāļŠāļģāļāļąāļāđāļāļĢāļ°āļŦāļ§āđāļēāļāļāļēāļĢāđāļĨāļ·āđāļāļāđāļĢāđāļ§ āđāļāļĒāđāļāļāļēāļ°āļāļąāļāđāļāļĨāļĨāđāļāļĩāđāļĄāļĩāļāļēāļĢāļāļģāļāļ§āļāļŦāļĢāļ·āļāļĢāļđāļāļ āļēāļ
āļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ iOS āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļēāļĢāļāļąāļāļāļēāļĢāļāļēāļĢāđāļŦāļĨāļāļĢāļđāļāļ āļēāļāđāļāļāļāļ°āļāļīāļāđāļāļĢāļāļąāļŠ
āļĢāļđāļāļ āļēāļāļĄāļąāļāļāļ°āļāļĨāļēāļĒāđāļāđāļāļāļļāļāļāļāļāļ§āļāļāđāļēāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāđāļāļĢāļēāļĒāļāļēāļĢ āļāļēāļĢāļāļąāļāļāļēāļĢāļāļĩāđāđāļĄāđāļāļđāļāļāđāļāļāļāļģāđāļŦāđāđāļāļīāļāļāļēāļĢāļāļĢāļ°āļāļļāļāđāļāļāļēāļĢāđāļĨāļ·āđāļāļāđāļĨāļ°āļāļēāļĢāđāļāđāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģāļĄāļēāļāđāļāļīāļāđāļ
import SwiftUI
// Singleton image cache
actor ImageCache {
static let shared = ImageCache()
private var cache = NSCache<NSString, UIImage>()
private init() {
// Memory limit: 50 MB
cache.totalCostLimit = 50 * 1024 * 1024
}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url.absoluteString as NSString)
}
func setImage(_ image: UIImage, for url: URL) {
// Cost estimation: image bytes
let cost = Int(image.size.width * image.size.height * 4)
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
}
}
// Optimized image view with caching
struct CachedAsyncImage: View {
let url: URL?
let size: CGSize
@State private var image: UIImage?
@State private var isLoading = false
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if isLoading {
Rectangle()
.fill(Color.gray.opacity(0.2))
.overlay(ProgressView())
} else {
Rectangle()
.fill(Color.gray.opacity(0.2))
}
}
.frame(width: size.width, height: size.height)
.clipped()
.task(id: url) {
await loadImage()
}
}
private func loadImage() async {
guard let url else { return }
// Check cache
if let cached = await ImageCache.shared.image(for: url) {
self.image = cached
return
}
isLoading = true
defer { isLoading = false }
// Download and resize
do {
let (data, _) = try await URLSession.shared.data(from: url)
// Resize to save memory
if let original = UIImage(data: data),
let resized = await resizeImage(original, to: size) {
await ImageCache.shared.setImage(resized, for: url)
self.image = resized
}
} catch {
// Handle error silently
}
}
private func resizeImage(_ image: UIImage, to size: CGSize) async -> UIImage? {
// Use screen scale
let scale = await UIScreen.main.scale
let targetSize = CGSize(
width: size.width * scale,
height: size.height * scale
)
return await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let renderer = UIGraphicsImageRenderer(size: targetSize)
let resized = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
continuation.resume(returning: resized)
}
}
}
}āļāļēāļĢāđāļāđāļāļēāļāļāļĩāđāļāļŠāļĄāļāļŠāļēāļāļāļēāļĢāđāļāļāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģ āļāļēāļĢāļāļĢāļąāļāļāļāļēāļāļĨāđāļ§āļāļŦāļāđāļē āđāļĨāļ°āļāļēāļĢāđāļŦāļĨāļāđāļāļāļāļ°āļāļīāļāđāļāļĢāļāļąāļŠāđāļāļ·āđāļāļāļĢāļ°āļŠāļāļāļēāļĢāļāđāļāļēāļĢāđāļĨāļ·āđāļāļāļāļĩāđāļĨāļ·āđāļāđāļŦāļĨ
Prefetching āļāļąāļāļāļĢāļīāļĒāļ°āđāļāļ·āđāļāļāļēāļĢāļāļēāļāļāļēāļĢāļāđ
āļŠāļģāļŦāļĢāļąāļāļĢāļēāļĒāļāļēāļĢāļāļĩāđāļĒāļēāļ§āļĄāļēāļ prefetching āļāļ°āđāļŦāļĨāļāļĢāļđāļāļ āļēāļāļāđāļāļāļāļĩāđāļāļ°āļāļĢāļēāļāļ:
// Prefetching coordinator
@Observable
final class ImagePrefetcher {
private var prefetchTasks: [URL: Task<Void, Never>] = [:]
private let prefetchDistance = 10 // Number of items ahead
func prefetchImages(for items: [Contact], visibleRange: Range<Int>) {
// Calculate prefetch range
let prefetchStart = max(0, visibleRange.lowerBound - prefetchDistance)
let prefetchEnd = min(items.count, visibleRange.upperBound + prefetchDistance)
// Launch prefetch for items in range
for index in prefetchStart..<prefetchEnd {
guard let url = items[index].avatarURL else { continue }
// Avoid duplicates
guard prefetchTasks[url] == nil else { continue }
prefetchTasks[url] = Task {
// Check if already cached
if await ImageCache.shared.image(for: url) != nil {
return
}
// Prefetch
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
await ImageCache.shared.setImage(image, for: url)
}
} catch {
// Ignore prefetch errors
}
}
}
// Cancel out-of-range prefetches
cancelOutOfRangePrefetches(validRange: prefetchStart..<prefetchEnd, items: items)
}
private func cancelOutOfRangePrefetches(validRange: Range<Int>, items: [Contact]) {
let validURLs = Set(
items[validRange].compactMap { $0.avatarURL }
)
for (url, task) in prefetchTasks {
if !validURLs.contains(url) {
task.cancel()
prefetchTasks.removeValue(forKey: url)
}
}
}
}āļāļēāļĢāđāļĨāļ·āļāļāļĢāļ°āļŦāļ§āđāļēāļ List āđāļĨāļ° LazyVStack
āļāļēāļĢāđāļĨāļ·āļāļāļĢāļ°āļŦāļ§āđāļēāļ List āđāļĨāļ° LazyVStack āļāļķāđāļāļāļĒāļđāđāļāļąāļāļāļĢāļāļĩāļāļēāļĢāđāļāđāļāļēāļ āđāļāđāļĨāļ°āļŠāđāļ§āļāļāļĢāļ°āļāļāļāļĄāļĩāļāđāļāđāļāđāđāļāļĢāļĩāļĒāļāđāļāļāļēāļ°āļāļĩāđāļāļ§āļĢāļāļĢāļēāļ
// â
List: ideal for interactive content
// - Automatic cell recycling
// - Native swipe actions support
// - Built-in separators and styles
struct ContactsWithSwipeActions: View {
@State private var contacts: [Contact] = []
var body: some View {
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteContact(contact)
} label: {
Label("Delete", systemImage: "trash")
}
}
.swipeActions(edge: .leading) {
Button {
favoriteContact(contact)
} label: {
Label("Favorite", systemImage: "star")
}
.tint(.yellow)
}
}
}
.listStyle(.plain)
}
private func deleteContact(_ contact: Contact) {
contacts.removeAll { $0.id == contact.id }
}
private func favoriteContact(_ contact: Contact) {
// Favorite logic
}
}
// â
LazyVStack: ideal for custom layouts
// - Full control over spacing and padding
// - No imposed styles
// - Better performance for simple display
struct CustomFeedView: View {
let posts: [Post]
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(posts) { post in
PostCard(post: post)
}
}
.padding(.horizontal)
}
}
}
// Post model for example
struct Post: Identifiable {
let id: UUID
let author: String
let content: String
let imageURL: URL?
}
struct PostCard: View {
let post: Post
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Header
HStack {
Circle()
.fill(Color.blue)
.frame(width: 40, height: 40)
Text(post.author)
.font(.headline)
Spacer()
}
// Content
Text(post.content)
// Optional image
if let imageURL = post.imageURL {
CachedAsyncImage(url: imageURL, size: CGSize(width: 300, height: 200))
.cornerRadius(12)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
}
}List āļĢāļĩāđāļāđāļāļīāļĨāđāļāļĨāļĨāđāļāļĒāđāļēāļāđāļāļāļāļĩāļ āļāļķāđāļāļāļēāļāļāļģāđāļŦāđāđāļāļīāļāļāļąāļāļŦāļēāļāļąāļ state āļ āļēāļĒāđāļ (@State) āļāđāļē @State āđāļāđāļāļĨāļĨāđāļāļāļ List āļāļēāļāļāļđāļāđāļāđāļāđāļģāđāļāļĒāđāļĄāđāļāļēāļāļāļīāļ āļāļ§āļĢāđāļāđāļ state āđāļ§āđāđāļāđāļĄāđāļāļĨāļāđāļāļĄāļđāļĨāļŦāļĢāļ·āļāđāļ ViewModel
āđāļāļāļāļąāļāđāļĨāļ°āļŠāđāļ§āļāļŦāļąāļ§āļāļĩāđāļāļĢāļąāļāđāļāđāļāđāļĨāđāļ§
āļāļēāļĢāļāļąāļāđāļāļ·āđāļāļŦāļēāđāļāđāļāđāļāļāļāļąāļāļāđāļ§āļĒāļāļĢāļąāļāļāļĢāļļāļāļāļ§āļēāļĄāļŠāļēāļĄāļēāļĢāļāđāļāļāļēāļĢāļāđāļēāļāđāļāđāļāļēāļāļŠāđāļāļāļĨāļāļĢāļ°āļāļāļāđāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļŦāļēāļāđāļāđāļāļēāļāđāļĄāđāļāļĩ āļŠāđāļ§āļāļŦāļąāļ§āļāļĩāđāļāļĢāļķāļāđāļĨāļ°āļāļēāļĢāļāļąāļāļāļēāļĢāđāļāļāļāļąāļāļāđāļāļāļāļēāļĢāļāļ§āļēāļĄāđāļŠāđāđāļāđāļāđāļāļāļīāđāļĻāļĐ
// Grouped data model
struct GroupedContacts {
let letter: String
let contacts: [Contact]
}
// View with optimized sections
struct SectionedContactList: View {
let groupedContacts: [GroupedContacts]
var body: some View {
ScrollView {
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
ForEach(groupedContacts, id: \.letter) { group in
Section {
// Section content
ForEach(group.contacts) { contact in
ContactRow(contact: contact)
.padding(.horizontal)
.padding(.vertical, 8)
// Custom separator
if contact.id != group.contacts.last?.id {
Divider()
.padding(.leading, 68)
}
}
} header: {
// Optimized pinned header
SectionHeader(title: group.letter)
}
}
}
}
}
}
// Lightweight header for performance
struct SectionHeader: View {
let title: String
var body: some View {
Text(title)
.font(.headline)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
}
}
// Optimized grouping function
extension Array where Element == Contact {
func groupedByFirstLetter() -> [GroupedContacts] {
// Dictionary for O(n) grouping
var groups: [String: [Contact]] = [:]
for contact in self {
let letter = String(contact.name.prefix(1)).uppercased()
groups[letter, default: []].append(contact)
}
// Sort groups alphabetically
return groups
.map { GroupedContacts(letter: $0.key, contacts: $0.value) }
.sorted { $0.letter < $1.letter }
}
}āļŠāđāļ§āļāļŦāļąāļ§āļāļĩāđāļāļĢāļķāļ (pinnedViews: [.sectionHeaders]) āļĒāļąāļāļāļāļĄāļāļāđāļŦāđāļāđāļāđāļĢāļ°āļŦāļ§āđāļēāļāļāļēāļĢāđāļĨāļ·āđāļāļ āļāļĢāļąāļāļāļĢāļļāļāļāļēāļĢāļāļģāļāļēāļāđāļāļĢāļēāļĒāļāļēāļĢāļāļĩāđāļĒāļēāļ§
āļāļēāļĢāđāļāđāļāļŦāļāđāļēāđāļĨāļ°āļāļēāļĢāđāļĨāļ·āđāļāļāđāļāļāđāļĄāđāļāļģāļāļąāļ
āļŠāļģāļŦāļĢāļąāļāļāļļāļāļāđāļāļĄāļđāļĨāļāļāļēāļāđāļŦāļāđ āļāļēāļĢāđāļāđāļāļŦāļāđāļēāļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļāļāļēāļĢāđāļŦāļĨāļāļāđāļāļĄāļđāļĨāļāļąāđāļāļŦāļĄāļāļĨāļāđāļāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģ āļāļēāļĢāđāļāđāļāļēāļāļāđāļāļāđāļāļĢāđāļāđāļŠāļŠāļģāļŦāļĢāļąāļāļāļđāđāđāļāđ
// ViewModel handling pagination
@Observable
final class PaginatedListViewModel {
private(set) var items: [Contact] = []
private(set) var isLoading = false
private(set) var hasMorePages = true
private var currentPage = 0
private let pageSize = 20
private let dataService: ContactDataService
init(dataService: ContactDataService) {
self.dataService = dataService
}
func loadInitialData() async {
guard items.isEmpty else { return }
await loadNextPage()
}
func loadMoreIfNeeded(currentItem: Contact) async {
// Trigger loading when approaching the end
guard let index = items.firstIndex(where: { $0.id == currentItem.id }) else {
return
}
// Load 5 items before the end
let thresholdIndex = items.count - 5
if index >= thresholdIndex {
await loadNextPage()
}
}
private func loadNextPage() async {
guard !isLoading, hasMorePages else { return }
isLoading = true
defer { isLoading = false }
do {
let newItems = try await dataService.fetchContacts(
page: currentPage,
limit: pageSize
)
items.append(contentsOf: newItems)
currentPage += 1
hasMorePages = newItems.count == pageSize
} catch {
// Handle error
}
}
}
// View with infinite scroll
struct InfiniteContactList: View {
@State private var viewModel: PaginatedListViewModel
init(dataService: ContactDataService) {
_viewModel = State(initialValue: PaginatedListViewModel(dataService: dataService))
}
var body: some View {
List {
ForEach(viewModel.items) { contact in
ContactRow(contact: contact)
.task {
// Check if more loading needed
await viewModel.loadMoreIfNeeded(currentItem: contact)
}
}
// Loading indicator at end of list
if viewModel.isLoading {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding()
}
}
.task {
await viewModel.loadInitialData()
}
}
}
// Protocol for data service
protocol ContactDataService {
func fetchContacts(page: Int, limit: Int) async throws -> [Contact]
}āļĢāļđāļāđāļāļāļāļĩāđāļĢāļąāļāļāļĢāļ°āļāļąāļāļāļēāļĢāđāļŦāļĨāļāļāļĩāđāļĨāļ·āđāļāđāļŦāļĨāđāļāļĒāđāļĄāđāļāļīāļāļāļąāđāļāļāļīāļāđāļāļāļĢāđāđāļāļāđāļĨāļ°āļāđāļ§āļĒāđāļŦāđāļŠāļēāļĄāļēāļĢāļāļāļąāļāļāļēāļĢāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģāđāļāđāļāļĒāđāļēāļāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ
āļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ iOS āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļēāļĢāļāļģ Profiling āļāđāļ§āļĒ Instruments
āļāļēāļĢāļĢāļ°āļāļļāļāļąāļāļŦāļēāļāđāļēāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāđāļāļāļāļēāļĢāđāļāļĢāļ·āđāļāļāļĄāļ·āļāļ§āļąāļāļāļĨāļāļĩāđāđāļĄāđāļāļĒāļģ Instruments āļĄāļĩāđāļāļĄāđāļāļĨāļāļŦāļĨāļēāļĒāđāļāļāļāļĩāđāđāļŦāļĄāļēāļ°āļŠāļģāļŦāļĢāļąāļ SwiftUI
// Measurement points for debugging
struct PerformanceMonitor {
// Measure view creation time
static func measureViewCreation<T: View>(
_ name: String,
@ViewBuilder content: () -> T
) -> T {
let start = CFAbsoluteTimeGetCurrent()
let view = content()
let elapsed = CFAbsoluteTimeGetCurrent() - start
#if DEBUG
if elapsed > 0.016 { // More than 16ms = frame drop
print("â ïļ [\(name)] View creation took \(elapsed * 1000)ms")
}
#endif
return view
}
}
// Extension to trace renders
extension View {
func debugRender(_ label: String) -> some View {
#if DEBUG
let _ = Self._printChanges()
print("ð Rendering: \(label)")
#endif
return self
}
func measureRender(_ label: String) -> some View {
modifier(RenderMeasureModifier(label: label))
}
}
struct RenderMeasureModifier: ViewModifier {
let label: String
@State private var renderCount = 0
func body(content: Content) -> some View {
content
.onAppear {
renderCount += 1
#if DEBUG
print("ð [\(label)] Render count: \(renderCount)")
#endif
}
}
}āļĢāļēāļĒāļāļēāļĢāļāļĢāļ§āļāļŠāļāļāļāļēāļĢāļāļĢāļąāļāđāļāđāļāļāđāļ§āļĒ Instruments
āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļģ profiling āļĢāļēāļĒāļāļēāļĢ SwiftUI āļāļĩāđāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ:
- Time Profiler: āļĢāļ°āļāļļāļāļąāļāļāđāļāļąāļāļāļĩāđāđāļāđ CPU āļĄāļēāļāļāļĩāđāļŠāļļāļ
- Allocations: āļāļĢāļ§āļāļŠāļāļāļāļēāļĢāđāļāļīāļāđāļāļāļāļāļŦāļāđāļ§āļĒāļāļ§āļēāļĄāļāļģāļĢāļ°āļŦāļ§āđāļēāļāļāļēāļĢāđāļĨāļ·āđāļāļ
- SwiftUI Instrument: āđāļŠāļāļāļ āļēāļāļāļēāļĢāļāļĢāļ°āđāļĄāļīāļ body
- Core Animation: āļāļĢāļ§āļāļāļąāļāļāļēāļĢāļĨāļāļĨāļāļāļāļāđāļāļĢāļĄ
// Instrumented view for profiling
struct ProfiledContactList: View {
let contacts: [Contact]
var body: some View {
let _ = Self._printChanges() // Shows changes triggering re-render
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
.measureRender("ContactRow-\(contact.id)")
}
}
}
}API debugging āļāļāļ SwiftUI āļāļĩāđāļāļ°āļāļīāļĄāļāđāđāļāļĒāļąāļāļāļāļāđāļāļĨāļ§āđāļēāļĄāļĩāļāļļāļāļŠāļĄāļāļąāļāļīāđāļāļāļĩāđāđāļāļĨāļĩāđāļĒāļāđāļāļĨāļāđāļĨāļ°āļāļĢāļ°āļāļļāđāļāļāļēāļĢāļāļĢāļ°āđāļĄāļīāļ body āđāļŦāļĄāđ āļāļģāđāļāđāļāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļĢāļ°āļāļļāļāļēāļĢāđāļĢāļāđāļāļāļĢāđāđāļŦāļĄāđāļāļĩāđāđāļĄāđāļāļģāđāļāđāļ
āļāļēāļĢāļāļĢāļąāļāđāļāđāļāļāļąāđāļāļŠāļđāļāļāđāļ§āļĒ drawingGroup
āļŠāļģāļŦāļĢāļąāļāļ§āļīāļ§āļāļĩāđāļāļąāļāļāđāļāļāļāļķāđāļāļĄāļĩāđāļāļāđāļāļāļāđāļ āļēāļāļŦāļĨāļēāļĒāļāļĒāđāļēāļ drawingGroup() āļŠāļēāļĄāļēāļĢāļāļāļĢāļąāļāļāļĢāļļāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļĒāđāļēāļāļĄāļĩāļāļąāļĒāļŠāļģāļāļąāļāđāļāļĒāļāļēāļĢāđāļĢāļŠāđāļāļāļĢāđāđāļĢāļāđāļ§āļīāļ§āđāļāđāļāđāļĨāđāļĒāļāļĢāđ Metal
// Cell with complex visual effects
struct ComplexVisualRow: View {
let item: Item
var body: some View {
HStack(spacing: 16) {
// Circle with gradient and shadow
Circle()
.fill(
RadialGradient(
colors: [.blue, .purple, .pink],
center: .center,
startRadius: 0,
endRadius: 25
)
)
.frame(width: 50, height: 50)
.shadow(color: .purple.opacity(0.5), radius: 8, y: 4)
VStack(alignment: .leading, spacing: 4) {
Text(item.name)
.font(.headline)
// Progress bar with gradient
GeometryReader { geometry in
Capsule()
.fill(Color.gray.opacity(0.2))
.overlay(alignment: .leading) {
Capsule()
.fill(
LinearGradient(
colors: [.green, .yellow, .orange],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: geometry.size.width * item.progress)
}
}
.frame(height: 8)
}
}
.padding()
// Rasterization for performance
.drawingGroup()
}
}
// List using optimized cells
struct OptimizedComplexList: View {
let items: [Item]
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ComplexVisualRow(item: item)
}
}
.padding()
}
}
}
struct Item: Identifiable {
let id: UUID
let name: String
let progress: Double
}drawingGroup() āļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāđāļāđāļāļāļīāđāļĻāļĐāļŠāļģāļŦāļĢāļąāļāļ§āļīāļ§āļāļĩāđāļāļŠāļĄāļāļŠāļēāļāđāļāļĢāđāļāļĩāļĒāļāļāđ āđāļāļē āđāļĨāļ°āđāļāļāđāļāļāļāđāđāļāļĨāļ
āļāļāļŠāļĢāļļāļ
āļāļēāļĢāļāļĢāļąāļāđāļāđāļāļĢāļēāļĒāļāļēāļĢ SwiftUI āļāļķāđāļāļāļĒāļđāđāļāļąāļāļāļ§āļēāļĄāđāļāđāļēāđāļāļāļĒāđāļēāļāļĨāļķāļāļāļķāđāļāđāļāļĩāđāļĒāļ§āļāļąāļāļāļĨāđāļ lazy loading āļāļēāļĢāļĢāļĩāđāļāđāļāļīāļĨ āđāļĨāļ°āļāļēāļĢāđāļāļĢāļĩāļĒāļāđāļāļĩāļĒāļāļ§āļīāļ§ āđāļāļāļāļīāļāļāļĩāđāļāļģāđāļŠāļāļāļāđāļ§āļĒāđāļŦāđāļŠāļēāļĄāļēāļĢāļāļŠāļĢāđāļēāļāļāļīāļāđāļāļāļĢāđāđāļāļāļāļĩāđāļŠāļēāļĄāļēāļĢāļāļāļąāļāļāļēāļĢāļāļąāļāļāļāļāđāļāļĢāļ°āļāļāļāļŦāļĨāļēāļĒāļāļąāļāļĢāļēāļĒāļāļēāļĢāđāļāļāļāļ°āļāļĩāđāļĒāļąāļāļāļāļāļēāļĢāđāļĨāļ·āđāļāļāļāļĩāđāļĨāļ·āđāļāđāļŦāļĨāļāļĩāđ 60 FPS
āļĢāļēāļĒāļāļēāļĢāļāļĢāļ§āļāļŠāļāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ SwiftUI
- â
āđāļāđ
LazyVStackāļŦāļĢāļ·āļListāđāļāļVStackāļŠāļģāļŦāļĢāļąāļāļāļāļĨāđāļĨāļāļāļąāļ - â
āđāļāđ
Identifiableāļāđāļ§āļĒ ID āļāļĩāđāđāļŠāļāļĩāļĒāļĢāđāļĨāļ°āđāļĄāđāļāđāļģāļāļąāļ - â
āļāļģ
EquatableāļĄāļēāđāļāđāļŠāļģāļŦāļĢāļąāļāđāļāļĨāļĨāđāļāļĩāđāļāļąāļāļāđāļāļ - â āđāļāļāđāļĨāļ°āļāļĢāļąāļāļāļāļēāļāļĢāļđāļāļ āļēāļāļāđāļāļāđāļŠāļāļāļāļĨ
- â āđāļŦāļĨāļāļāđāļāļĄāļđāļĨāļĨāđāļ§āļāļŦāļāđāļēāļāđāļ§āļĒ prefetching āļāļąāļāļāļĢāļīāļĒāļ°
- â
āđāļĨāļ·āļāļ
ListāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāđāļāđāļāļāļ (swipe) āļŦāļĢāļ·āļLazyVStackāļŠāļģāļŦāļĢāļąāļāđāļĨāļĒāđāđāļāļēāļāđāļāļĩāđāļāļģāļŦāļāļāđāļāļ - â
āđāļāđ
pinnedViewsāļŠāļģāļŦāļĢāļąāļāļŠāđāļ§āļāļŦāļąāļ§āđāļāļāļāļąāļ - â āđāļāđāļāļēāļĢāđāļāđāļāļŦāļāđāļēāļŠāļģāļŦāļĢāļąāļāļāļļāļāļāđāļāļĄāļđāļĨāļāļāļēāļāđāļŦāļāđ
- â āļāļģ profiling āđāļāđāļāļāļĢāļ°āļāļģāļāđāļ§āļĒ Instruments
- â
āđāļāđ
drawingGroup()āļāļąāļāļ§āļīāļ§āļāļĩāđāļĄāļĩāđāļāļāđāļāļāļāđāļ āļēāļāļāļĩāđāļāļąāļāļāđāļāļ
āđāļĢāļīāđāļĄāļāļķāļāļāđāļāļĄāđāļĨāļĒ!
āļāļāļŠāļāļāļāļ§āļēāļĄāļĢāļđāđāļāļāļāļāļļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāļŠāļąāļĄāļ āļēāļĐāļāđāđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āđāļāđāļ
āđāļāļĢāđ
āļāļāļāļ§āļēāļĄāļāļĩāđāđāļāļĩāđāļĒāļ§āļāđāļāļ

ViewModifier āđāļāļāļāļģāļŦāļāļāđāļāļāđāļ SwiftUI: āļĢāļđāļāđāļāļāļāļĩāđāļāļģāļāļĨāļąāļāļĄāļēāđāļāđāđāļŦāļĄāđāđāļāđāļŠāļģāļŦāļĢāļąāļ Design System
āļŠāļĢāđāļēāļ ViewModifier āđāļāļāļāļģāļŦāļāļāđāļāļāđāļ SwiftUI āļŠāļģāļŦāļĢāļąāļ design system āļāļĩāđāļŠāļāļāļāļĨāđāļāļāļāļąāļ āļĢāļđāļāđāļāļ āđāļāļ§āļāļēāļāļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāļāļĩāđāļŠāļļāļ āđāļĨāļ°āļāļąāļ§āļāļĒāđāļēāļāļāļĩāđāđāļāđāļāļēāļāđāļāđāļāļĢāļīāļāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāļŠāđāļāļĨāđ view āļāļāļ iOS āļāļĒāđāļēāļāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ

SwiftUI @Observable vs @State: āđāļāđāļāļąāļ§āđāļŦāļāđāļĄāļ·āđāļāđāļŦāļĢāđāđāļāļāļĩ 2026
āđāļāđāļēāđāļāļāļ§āļēāļĄāđāļāļāļāđāļēāļāļĢāļ°āļŦāļ§āđāļēāļ @Observable āđāļĨāļ° @State āđāļ SwiftUI āđāļāļ·āđāļāđāļĨāļ·āļāļāđāļāļĢāļ·āđāļāļāļĄāļ·āļāļāļąāļāļāļēāļĢ state āļāļĩāđāđāļŦāļĄāļēāļ°āļāļąāļāđāļāļ iOS

SwiftUI: āļāļēāļĢāļŠāļĢāđāļēāļāļāļīāļāđāļāļāļĢāđāđāļāļāļāļĩāđāļāļąāļāļŠāļĄāļąāļĒāļŠāļģāļŦāļĢāļąāļ iOS
āļāļđāđāļĄāļ·āļāļāļēāļĢāļŠāļĢāđāļēāļāļāļīāļāđāļāļāļĢāđāđāļāļāļāļĩāđāļāļąāļāļŠāļĄāļąāļĒāļāđāļ§āļĒ SwiftUI: āđāļ§āļĒāļēāļāļĢāļāđāđāļāļ declarative, āļāļāļĄāđāļāđāļāļāļāđ, āđāļāļāļīāđāļĄāļāļąāļ āđāļĨāļ°āđāļāļ§āļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāļāļĩāđāļŠāļļāļāļŠāļģāļŦāļĢāļąāļ iOS 18