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()) } }
- View Styles
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 } }