Declarative UI Philosophy
SwiftUI represents a paradigm shift from imperative to declarative UI development. Instead of describing how to build the interface, we describe what the interface should look like for any given state.
// Traditional UIKit approach (imperative)
class ProfileViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var profileImageView: UIImageView!
@IBOutlet weak var followButton: UIButton!
func updateUI(with user: User) {
nameLabel.text = user.name
profileImageView.image = user.profileImage
if user.isFollowing {
followButton.setTitle("Unfollow", for: .normal)
followButton.backgroundColor = .systemRed
} else {
followButton.setTitle("Follow", for: .normal)
followButton.backgroundColor = .systemBlue
}
}
}
// SwiftUI approach (declarative)
struct ProfileView: View {
let user: User
@State private var isFollowing: Bool
var body: some View {
VStack {
AsyncImage(url: user.profileImageURL)
.frame(width: 100, height: 100)
.clipShape(Circle())
Text(user.name)
.font(.title)
Button(isFollowing ? "Unfollow" : "Follow") {
isFollowing.toggle()
}
.foregroundColor(.white)
.padding()
.background(isFollowing ? .red : .blue)
.cornerRadius(8)
}
}
}
State Management Patterns
// @State for local component state
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("+1") { count += 1 }
}
}
}
// @StateObject for view model lifecycle management
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func loadUsers() async {
isLoading = true
defer { isLoading = false }
do {
users = try await UserService.fetchUsers()
} catch {
print("Error loading users: \(error)")
}
}
}
struct UserListView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView("Loading...")
} else {
List(viewModel.users) { user in
UserRowView(user: user)
}
}
}
.navigationTitle("Users")
.task {
await viewModel.loadUsers()
}
}
}
}
// @ObservedObject for shared objects
struct UserRowView: View {
@ObservedObject var user: User
var body: some View {
HStack {
AsyncImage(url: user.avatarURL)
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if user.isOnline {
Circle()
.fill(.green)
.frame(width: 8, height: 8)
}
}
}
}
Async/Await Integration
// Modern async data loading
struct AsyncDataView: View {
let loadData: () async throws -> Void
let content: () -> Content
@State private var isLoading = false
@State private var error: Error?
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let error = error {
ErrorView(error: error) {
await reload()
}
} else {
content()
}
}
.task {
await reload()
}
}
private func reload() async {
isLoading = true
error = nil
do {
try await loadData()
} catch {
self.error = error
}
isLoading = false
}
}
// Usage
struct PostsView: View {
@State private var posts: [Post] = []
var body: some View {
AsyncDataView {
posts = try await PostService.fetchPosts()
} content: {
List(posts) { post in
PostRowView(post: post)
}
}
.navigationTitle("Posts")
}
}
// Custom AsyncImage with better error handling
struct RemoteImage: View {
let url: URL?
let placeholder: Image
@State private var phase: AsyncImagePhase = .empty
var body: some View {
Group {
switch phase {
case .empty:
placeholder
.foregroundColor(.gray)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
Image(systemName: "photo")
.foregroundColor(.red)
@unknown default:
placeholder
}
}
.task {
await loadImage()
}
}
private func loadImage() async {
guard let url = url else {
phase = .failure(URLError(.badURL))
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let uiImage = UIImage(data: data) {
phase = .success(Image(uiImage: uiImage))
} else {
phase = .failure(URLError(.cannotDecodeContentData))
}
} catch {
phase = .failure(error)
}
}
}
Custom View Modifiers
// Reusable card style modifier
struct CardModifier: ViewModifier {
let backgroundColor: Color
let cornerRadius: CGFloat
let shadowRadius: CGFloat
func body(content: Content) -> some View {
content
.padding()
.background(backgroundColor)
.cornerRadius(cornerRadius)
.shadow(radius: shadowRadius)
}
}
extension View {
func cardStyle(
backgroundColor: Color = .white,
cornerRadius: CGFloat = 8,
shadowRadius: CGFloat = 2
) -> some View {
modifier(CardModifier(
backgroundColor: backgroundColor,
cornerRadius: cornerRadius,
shadowRadius: shadowRadius
))
}
}
// Loading state modifier
struct LoadingModifier: ViewModifier {
let isLoading: Bool
func body(content: Content) -> some View {
ZStack {
content
.disabled(isLoading)
.blur(radius: isLoading ? 2 : 0)
if isLoading {
ProgressView()
.scaleEffect(1.5)
}
}
}
}
extension View {
func loading(_ isLoading: Bool) -> some View {
modifier(LoadingModifier(isLoading: isLoading))
}
}
// Usage
struct ProductCard: View {
let product: Product
@State private var isLoading = false
var body: some View {
VStack(alignment: .leading) {
RemoteImage(
url: product.imageURL,
placeholder: Image(systemName: "photo")
)
.frame(height: 200)
Text(product.name)
.font(.headline)
Text(product.price, format: .currency(code: "USD"))
.font(.title2)
.fontWeight(.bold)
}
.cardStyle()
.loading(isLoading)
.onTapGesture {
Task {
isLoading = true
await purchaseProduct()
isLoading = false
}
}
}
private func purchaseProduct() async {
// Purchase logic
}
}
Opaque Types and some View
// Understanding opaque return types
protocol Shape {
func area() -> Double
}
// Without opaque types - exposes implementation
func makeCircle() -> Circle {
return Circle(radius: 5)
}
// With opaque types - hides implementation
func makeShape() -> some Shape {
return Circle(radius: 5) // Could return any Shape
}
// SwiftUI leverages this heavily
struct ContentView: View {
var body: some View { // Opaque type
VStack {
Text("Hello")
Button("Tap me") { }
}
// Compiler knows exact type, we don't need to
}
}
// Custom containers with opaque types
struct ConditionalContainer: View {
let condition: Bool
let trueContent: TrueContent
let falseContent: FalseContent
var body: some View {
Group {
if condition {
trueContent
} else {
falseContent
}
}
}
}
// ViewBuilder for custom container views
struct CustomCard: View {
let title: String
@ViewBuilder let content: () -> Content
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
.padding(.bottom, 4)
content()
}
.cardStyle()
}
}
// Usage
struct ProfileCard: View {
let user: User
var body: some View {
CustomCard(title: "Profile") {
HStack {
AsyncImage(url: user.avatarURL)
.frame(width: 50, height: 50)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.caption)
.foregroundColor(.secondary)
}
}
Divider()
Text(user.bio)
.font(.body)
}
}
}
Advanced Composition Patterns
// Generic list view with pull-to-refresh
struct RefreshableList: View {
let items: [Item]
let onRefresh: () async -> Void
@ViewBuilder let rowContent: (Item) -> RowContent
var body: some View {
List(items, id: \.id) { item in
rowContent(item)
}
.refreshable {
await onRefresh()
}
}
}
// Environment-based theming
struct Theme {
let primaryColor: Color
let secondaryColor: Color
let backgroundColor: Color
let textColor: Color
}
private struct ThemeKey: EnvironmentKey {
static let defaultValue = Theme(
primaryColor: .blue,
secondaryColor: .gray,
backgroundColor: .white,
textColor: .black
)
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// Themed button component
struct ThemedButton: View {
let title: String
let action: () -> Void
@Environment(\.theme) private var theme
var body: some View {
Button(title, action: action)
.foregroundColor(.white)
.padding()
.background(theme.primaryColor)
.cornerRadius(8)
}
}
// App with theme
struct ThemedApp: View {
@State private var isDarkMode = false
private var currentTheme: Theme {
isDarkMode ? darkTheme : lightTheme
}
private let lightTheme = Theme(
primaryColor: .blue,
secondaryColor: .gray,
backgroundColor: .white,
textColor: .black
)
private let darkTheme = Theme(
primaryColor: .orange,
secondaryColor: .secondary,
backgroundColor: .black,
textColor: .white
)
var body: some View {
NavigationView {
VStack {
Toggle("Dark Mode", isOn: $isDarkMode)
.padding()
ThemedButton("Primary Action") {
print("Action performed")
}
Spacer()
}
.navigationTitle("Themed App")
}
.environment(\.theme, currentTheme)
.background(currentTheme.backgroundColor)
}
}
Performance Optimization
// Lazy loading with LazyVStack
struct OptimizedListView: View {
let items: [LargeDataItem]
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ExpensiveRowView(item: item)
.id(item.id) // Explicit ID for better performance
}
}
.padding()
}
}
}
// Memoization with @State and computed properties
struct OptimizedCalculationView: View {
@State private var numbers: [Int] = []
@State private var calculationResult: Int?
// Expensive calculation only when numbers change
private var expensiveCalculation: Int {
if calculationResult == nil {
calculationResult = numbers.reduce(0) { result, number in
// Simulate expensive operation
Thread.sleep(forTimeInterval: 0.001)
return result + number * number
}
}
return calculationResult ?? 0
}
var body: some View {
VStack {
Text("Result: \(expensiveCalculation)")
Button("Add Random Number") {
numbers.append(Int.random(in: 1...100))
calculationResult = nil // Invalidate cache
}
}
}
}
// Efficient image caching
@MainActor
class ImageCache: ObservableObject {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
return cache[url]
}
func setImage(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
struct CachedAsyncImage: View {
let url: URL
@StateObject private var imageCache = ImageCache()
@State private var image: UIImage?
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
} else {
ProgressView()
.task {
await loadImage()
}
}
}
}
private func loadImage() async {
// Check cache first
if let cachedImage = imageCache.image(for: url) {
image = cachedImage
return
}
// Load from network
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let loadedImage = UIImage(data: data) {
imageCache.setImage(loadedImage, for: url)
image = loadedImage
}
} catch {
print("Failed to load image: \(error)")
}
}
}
Best Practices
- Embrace declarative thinking - describe what, not how
- Use @StateObject for view models and @ObservedObject for shared objects
- Leverage async/await for modern data loading patterns
- Create reusable components with ViewBuilder and generics
- Use environment values for app-wide configuration
- Optimize with lazy loading for large datasets
- Cache expensive operations and invalidate when needed