SwiftUI

Hero image for SwiftUI

SwiftUI offers a declarative approach to user interface design, and it comes with property wrappers that provide different functionality. These guidelines outline the best practices and conventions for building native UI with SwiftUI and its property wrappers.

Project Structure

The project structure used Clean code structure, plus a few additional SwiftUI and KIF-specific directories such as ViewModifiers, Coordinators, and ViewIds.

.
├── Modules
│   ├── Data
│   │   ├── Sources
│   │   │   ├── NetworkAPI
│   │   │   │   ├── Interceptors
│   │   │   │   ├── Models
│   │   │   │   ├── RequestConfigurations
│   │   │   │   ├── Core
│   │   │   │   │   ├── NetworkAPIError.swift
│   │   │   │   │   ├── NetworkAPIProtocol.swift
│   │   │   │   │   └── RequestConfiguration.swift
│   │   │   │   └── NetworkAPI.swift
│   │   │   └── Repositories
│   │   └── Tests
│   │       ├── Resources
│   │       └── Sources
│   │           ├── Dummies
│   │           │   ├── DummyNetworkModel.swift
│   │           │   └── DummyRequestConfiguration.swift
│   │           ├── Specs
│   │           │   └── NetworkAPISpec.swift
│   │           └── Utilities
│   │               └── NetworkStubber.swift
│   └── Domain
│       ├── Sources
│       │   ├── Entities
│       │   ├── Interfaces
│       │   └── UseCases
│       │       └── UseCaseFactoryProtocol.swift
│       └── Tests
│           ├── Resources
│           └── Sources
│               └── DummySpec.swift
└── ProjectName
    ├── Configurations
    ├── Resources
    ├── Sources
    │   ├── Application
    │   ├── Constants
    │   ├── Presentation
    │   │   ├── Models
    │   │   ├── Coordinators
    │   │   ├── Modules
    │   │   ├── Styles
    │   │   ├── ViewModifiers
    │   │   ├── Views
    │   │   └── ViewIds
    │   └── Supports
    │       ├── Helpers
    │       └── ...
    ├── ProjectNameTests
    └── ProjectNameKIFTests

Property Wrappers

  • Wrap a view model of a view that is initialized within a view as @StateObject.

    struct LoginView: View {
    
        @ObservedObject private var viewModel = LoginViewModel()
    }
    
    struct LoginView: View {
    
        @StateObject private var viewModel = LoginViewModel()
    }
    
  • Explicitly initialize state objects by placing your wrapped object’s initialization inside the StateObject initializer (which SwiftUI handles using an autoclosure) to ensure the object is initialized exactly once in a view’s life cycle.

    struct MovieView: View {
    
        @StateObject private var viewModel: MovieViewModel
    
        init(movie: Movie) {
            let viewModel = MovieViewModel(movie: movie)
            _viewModel = StateObject(wrappedValue: viewModel)
        }
    }
    
    struct MovieView: View {
    
        @StateObject private var viewModel: MovieViewModel
    
        init(movie: Movie) {
            _viewModel = StateObject(wrappedValue: MovieViewModel(movie: movie))
        }
    }
    
  • Wrap a view model of a view as @ObservedObject if it was initialized from elsewhere.

    struct MyView: View {
    
        @StateObject private var viewModel = MyViewModel()
    
        var body: some View {
            // UI code here
            MySubView(model: viewModel)
        }
    }
    
    struct MySubView: View {
    
        @StateObject var viewModel: MyViewModel
    
        var body: some View {
            // UI code here
        }
    }
    
    struct MyView: View {
    
        @StateObject private var viewModel = MyViewModel()
    
        var body: some View {
            // UI code here
            MySubView(viewModel: model)
        }
    }
    
    struct MySubView: View {
    
        @ObservedObject var viewModel: MyViewModel
    
        var body: some View {
            // UI code here
        }
    }
    
  • @Published should ONLY be applied to the view model’s properties that take responsibility for updating the view.

    class MyViewModel: ObservableObject {
    
        @Published private(set) var name = "Some Name"
        @Published private(set) var id = "dummy_id" // View does not consume this property
    
        func updateName(with name: String) {
            self.name = name
        }
    }
    
    struct MyView: View {
    
        @StateObject private var viewModel = MyViewModel()
    
        var body: some View {
            Text(viewModel.name)
        }
    }
    
    class MyViewModel: ObservableObject {
    
        @Published private(set) var name = "Some Name"
        private var id = "dummy_id" // View does not consume this property
    
        func updateName(with name: String) {
            self.name = name
        }
    }
    
    struct MyView: View {
    
        @StateObject private var viewModel = MyViewModel()
    
        var body: some View {
            Text(viewModel.name)
        }
    }
    

Conventions

ViewModifers and View Styles

  • Ensure UI code remains DRY by leveraging SwiftUI’s built-in View Styles and ViewModifier.

    • View Styles
      Button("Press Me") { ... }
          .padding()
          .background(Color.green)
          .foregroundStyle(.white)
          .clipShape(Capsule())
      
      Button("Press Me") { ... }
          .padding()
          .background(Color.green)
          .foregroundStyle(.white)
          .clipShape(Capsule())
      
      Button("Press Me") { ... }
          .buttonStyle(BlueButtonStyle())
      
      Button("Press Me") { ... }
          .buttonStyle(BlueButtonStyle())
      
      struct BlueButtonStyle: ButtonStyle {
      
          func makeBody(configuration: Configuration) -> some View {
              configuration.label
                  .padding()
                  .background(Color.green)
                  .foregroundStyle(.white)
                  .clipShape(Capsule())
          }
      }
      
    • Use ViewModifier to create a reusable modifier that can be applied to any view.
      Text("Rounded Label 1")
          .blur(radius: 30.0)
          .clipped()
      
      Button("Press Me") {...}
          .buttonStyle(BlueButtonStyle())
          .blur(radius: 30.0)
          .clipped()
      }
      
      Text("Rounded Label 1")
          .blur()
      
      Button("Press Me") {...}
          .buttonStyle(BlueButtonStyle())
          .blur()
      }
      
      fileprivate struct BlurModifier: ViewModifier {
      
          func body(content: Content) -> some View {
              content
                  .blur(radius: 30.0)
                  .clipped()
          }
      }
      
      extension View {
      
          func blur() -> some View {
              modifier(BlurModifier())
          }
      }
      

Naming

  • A view class representing a whole screen must end with the suffix Screen.

    struct LoginView: View {...}
    
    struct LoginScreen: View {...}
    

Declare an extracted view

  • Prioritize using a computed variable. Use a function if the extracted view requires parameter(s).

    var body: some View {
        logoView()
        buttonView(isFocused: true)
    }
    
    // Extracted View
    func logoView() -> some View {
        Image("nimble")
    }
    
    // Extracted View with parameters
    func buttonView(isFocused: Bool) -> some View {
        Button("nimble") {}
            .focus(isFocused)
    }
    
    var body: some View {
        logoView
        buttonView(isFocused: true)
    }
    
    var logoView: some View {
        Image("nimble")
    }
    
    func buttonView(isFocused: Bool) -> some View {
        Button("nimble") {}
            .focus(isFocused)
    }
    

View’s parameter as private

  • Always mark a parameter private if it is not intended to be accessed publicly.

    struct ExampleView: View {
        let isShown: Bool
    }
    
    struct ExampleView: View {
        private let isShown: Bool
    
        init(isShown: Bool) {
            self.isShown = isShown
        }
    }