Skip to content

Latest commit

 

History

History
395 lines (288 loc) · 9.48 KB

File metadata and controls

395 lines (288 loc) · 9.48 KB

Views Only Architecture

Overview

The "Views Only" implementation demonstrates the simplest approach to building a NavigationSplitView app in SwiftUI. All state is managed directly within views using @State and @Binding.

When to Use This Approach

Good for:

  • Prototypes and MVPs
  • Learning SwiftUI
  • Simple apps with minimal logic
  • Quick demonstrations
  • Apps that won't grow complex

Not ideal for:

  • Apps with complex business logic
  • Apps requiring unit testing
  • Large production applications
  • Apps with shared state across many views

Architecture Pattern

Data Flow

Parent View (@State)
    ↓ (binding)
Child View (@Binding)
    ↓ (binding)
Grandchild View (@Binding)

State is owned at the top level and passed down through the view hierarchy using bindings.

File Structure

Examples/ViewOnly/
├── ContentView.swift              # Root view - owns all state
├── SidebarView.swift              # Sidebar (first pane)
├── ContentListView.swift          # Content list (second pane)
├── DetailView.swift               # Detail view (third pane)
├── OverviewTab.swift              # Detail tab - overview
├── DetailsTab.swift               # Detail tab - details
├── OptionsTab.swift               # Detail tab - options
├── SettingsContentView.swift     # Custom settings view
├── SidebarCategory.swift          # Category enum
├── ListItem.swift                 # Data model
├── InfoRow.swift                  # Reusable UI component
└── DetailRow.swift                # Reusable UI component

Key Components

1. ContentView (Root)

Purpose: Entry point that owns all state

struct NavigationSplitViewViewOnlyContentView: View {
    // State owned at the top level
    @State private var selectedCategory: NavigationSplitViewViewOnlySidebarCategory? = .category1
    @State private var selectedItem: NavigationSplitViewViewOnlyListItem? = nil

    var body: some View {
        NavigationSplitView {
            // Pass bindings to children with $
            SidebarView(selectedCategory: $selectedCategory)
        } content: {
            // Conditional content based on category
            if category.showsItemList {
                ContentListView(category: category, selectedItem: $selectedItem)
            } else {
                SettingsContentView()
            }
        } detail: {
            DetailView(selectedItem: selectedItem)
        }
    }
}

Key Points:

  • @State creates mutable state owned by this view
  • $ creates a binding to pass to child views
  • State is the single source of truth

2. SidebarView

Purpose: Displays categories and handles selection

struct NavigationSplitViewViewOnlySidebarView: View {
    // Binding allows this view to read AND write the parent's state
    @Binding var selectedCategory: NavigationSplitViewViewOnlySidebarCategory?

    var body: some View {
        List(categories, selection: $selectedCategory) { category in
            NavigationLink(value: category) {
                HStack {
                    Image(systemName: category.icon)
                    Text(category.rawValue)
                }
            }
        }
    }
}

Key Points:

  • @Binding connects to parent's @State
  • Changes here update the parent automatically
  • No local state needed

3. ContentListView

Purpose: Shows list of items for a category

struct NavigationSplitViewViewOnlyContentListView: View {
    let category: NavigationSplitViewViewOnlySidebarCategory
    @Binding var selectedItem: NavigationSplitViewViewOnlyListItem?

    // Data stored directly in the view
    private let allItems: [NavigationSplitViewViewOnlySidebarCategory: [NavigationSplitViewViewOnlyListItem]] = [
        .category1: [/* items */],
        .category2: [/* items */],
        .category3: [/* items */]
    ]

    var body: some View {
        List(filteredItems, selection: $selectedItem) { item in
            // Display items
        }
    }
}

Key Points:

  • Mix of let (read-only) and @Binding (read-write)
  • Data is hardcoded in the view
  • Filtering logic is in the view

SwiftUI Concepts Used

@State

Creates mutable state owned by the view:

@State private var count = 0        // Owned by this view
@State private var isEnabled = true // Changes cause view to update

@Binding

Creates a two-way connection to a parent's @State:

struct ChildView: View {
    @Binding var value: String  // Connected to parent's @State

    var body: some View {
        TextField("Enter", text: $value)  // Changes update parent
    }
}

Optional Chaining (?.)

Safely access properties on optionals:

selectedCategory?.icon          // Returns icon if category exists, nil otherwise

Nil-Coalescing (??)

Provides a default value when optional is nil:

selectedCategory?.icon ?? "circle"  // Returns "circle" if category is nil

Logical AND (&&)

Both conditions must be true:

if isEnabled && hasData {  // Both must be true
    // Execute
}

Data Flow Example

Let's trace what happens when a user selects a category:

  1. User taps "Category 2" in sidebar
  2. SidebarView updates its @Binding var selectedCategory
  3. This automatically updates ContentView's @State var selectedCategory
  4. ContentView re-renders because its state changed
  5. ContentListView receives new category value
  6. ContentListView re-renders with filtered items for Category 2

All of this happens automatically - no manual notification code needed!

Advantages

✅ Simplicity

  • Minimal boilerplate
  • Easy to understand
  • Quick to implement
  • Perfect for learning

✅ SwiftUI-Native

  • Uses SwiftUI's built-in patterns
  • No additional frameworks needed
  • Follows Apple's examples

✅ Self-Contained

  • Everything is in one place
  • Easy to see the full picture
  • No jumping between files

Limitations

❌ Testing

  • Hard to unit test view logic
  • State is tied to views
  • Would need UI testing

❌ Reusability

  • Logic is coupled to views
  • Can't reuse business logic easily
  • State management is view-specific

❌ Complexity

  • Gets messy as app grows
  • Views become too large
  • Hard to maintain

❌ Separation

  • UI and logic are mixed
  • Changes affect both UI and behavior
  • Hard to work on in parallel

Common Patterns

Pattern 1: Lifting State Up

When multiple views need to share state, lift it to their common parent:

struct ParentView: View {
    @State private var sharedValue = ""  // Lifted state

    var body: some View {
        VStack {
            ChildA(value: $sharedValue)  // Both children
            ChildB(value: $sharedValue)  // can modify it
        }
    }
}

Pattern 2: Computed Properties

Use computed properties for derived values:

struct ListView: View {
    @State private var searchText = ""
    let allItems: [Item]

    // Computed - automatically updates when searchText changes
    var filteredItems: [Item] {
        allItems.filter { $0.name.contains(searchText) }
    }
}

Pattern 3: Enums for State

Use enums to represent mutually exclusive states:

enum ViewState {
    case loading
    case loaded([Item])
    case error(String)
}

@State private var state: ViewState = .loading

Migration to MVVM

When your app grows and needs better architecture:

  1. Extract data from views into Models
  2. Create ViewModels to manage state and logic
  3. Update views to observe ViewModels
  4. Test ViewModels independently

See MVVM_Architecture.md for details.

Best Practices

DO

✅ Keep state at the appropriate level ✅ Use private for @State properties ✅ Pass only what children need ✅ Use computed properties for derived data ✅ Group related state together

DON'T

❌ Put state in every view unnecessarily ❌ Duplicate state across views ❌ Make bindings to constants ❌ Overuse @State for complex logic ❌ Mix business logic with UI code

Example Walkthrough

Let's look at how the Settings feature works:

  1. Enum defines what content types exist:
enum SidebarCategory {
    case category1, category2, category3, settings

    var showsItemList: Bool {
        switch self {
        case .category1, .category2, .category3: return true
        case .settings: return false  // Different content!
        }
    }
}
  1. ContentView switches content based on category:
if category.showsItemList {
    ContentListView(...)  // Show list
} else {
    SettingsContentView()  // Show form
}
  1. SettingsContentView manages its own state:
struct SettingsContentView: View {
    @State private var isEnabled = true  // Local state
    @State private var fontSize: Double = 14

    var body: some View {
        Form {
            Toggle("Enable", isOn: $isEnabled)
            Slider(value: $fontSize, in: 10...24)
        }
    }
}

This pattern shows how different content types can coexist in the same NavigationSplitView!

Summary

The Views Only approach is perfect for learning SwiftUI and building simple apps. It's straightforward, uses SwiftUI's native features, and requires minimal code. However, as your app grows, consider migrating to MVVM for better separation of concerns and testability.

Next Steps:

  • Explore the code in Examples/ViewOnly/
  • Try modifying values and see what changes
  • Compare with the MVVM implementation
  • Read MVVM_Architecture.md when ready to learn more