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.
✅ 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
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.
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
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:
@Statecreates mutable state owned by this view$creates a binding to pass to child views- State is the single source of truth
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:
@Bindingconnects to parent's@State- Changes here update the parent automatically
- No local state needed
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
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 updateCreates 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
}
}Safely access properties on optionals:
selectedCategory?.icon // Returns icon if category exists, nil otherwiseProvides a default value when optional is nil:
selectedCategory?.icon ?? "circle" // Returns "circle" if category is nilBoth conditions must be true:
if isEnabled && hasData { // Both must be true
// Execute
}Let's trace what happens when a user selects a category:
- User taps "Category 2" in sidebar
- SidebarView updates its
@Binding var selectedCategory - This automatically updates
ContentView's@State var selectedCategory - ContentView re-renders because its state changed
- ContentListView receives new category value
- ContentListView re-renders with filtered items for Category 2
All of this happens automatically - no manual notification code needed!
- Minimal boilerplate
- Easy to understand
- Quick to implement
- Perfect for learning
- Uses SwiftUI's built-in patterns
- No additional frameworks needed
- Follows Apple's examples
- Everything is in one place
- Easy to see the full picture
- No jumping between files
- Hard to unit test view logic
- State is tied to views
- Would need UI testing
- Logic is coupled to views
- Can't reuse business logic easily
- State management is view-specific
- Gets messy as app grows
- Views become too large
- Hard to maintain
- UI and logic are mixed
- Changes affect both UI and behavior
- Hard to work on in parallel
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
}
}
}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) }
}
}Use enums to represent mutually exclusive states:
enum ViewState {
case loading
case loaded([Item])
case error(String)
}
@State private var state: ViewState = .loadingWhen your app grows and needs better architecture:
- Extract data from views into Models
- Create ViewModels to manage state and logic
- Update views to observe ViewModels
- Test ViewModels independently
See MVVM_Architecture.md for details.
✅ 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
❌ 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
Let's look at how the Settings feature works:
- 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!
}
}
}- ContentView switches content based on category:
if category.showsItemList {
ContentListView(...) // Show list
} else {
SettingsContentView() // Show form
}- 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!
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.mdwhen ready to learn more