Learn how to work with Xcode Previews in SwiftData App. SwiftData in combination with SwiftUI…
App Design Patterns in SwiftUI: A Comprehensive Guide
Design patterns are crucial in software development, providing tested, proven development paradigms. Effective use of design patterns can result in robust, scalable, and maintainable code. This guide explores frequently used design patterns in SwiftUI, offering straightforward explanations and unique code examples. By the end of this post, you’ll have a foundational understanding of how these patterns can be applied in your SwiftUI applications.
1. MVVM Pattern (Model-View-ViewModel)
Imagine you’re organizing a dinner party. You’re the chef (the ViewModel) in charge of preparing the meal, which is based on ingredients (the Model) like vegetables, meats, and spices. Your guests (the View) are waiting at the dining table, eager to enjoy whatever you serve them. You decide how to combine the ingredients into dishes, when to serve them, and how to present them on the plates. Your guests don’t see the preparation in the kitchen; they only see the beautifully prepared dishes you bring out to them.
In app development, the MVVM pattern (Model-View-ViewModel) mirrors this dinner party. The Model is like your ingredients, the raw data and business logic of your app. The View is akin to your guests, the user interface that presents the data to the user. The ViewModel is you, the chef, taking the Model’s data, preparing it (applying business logic), and deciding what and how the View should display the data.
This separation of concerns makes the app’s architecture more manageable. The ViewModel acts as a mediator that handles the logic and preparation of data, ensuring the View can remain simple and just focus on presenting the data. It leads to more maintainable and testable code, as each component has its distinct responsibility, much like how a well-organized dinner party runs smoothly when everyone knows their roles.
The MVVM pattern is widely used in SwiftUI for decoupling UI code from business logic and data models, facilitating easier testing and maintenance.
Here is a MVVM pattern example:
// Model
struct Person {
var name: String
var age: Int
}
// ViewModel
@Observable class PersonListViewModel {
var persons = [Person]()
func addPerson(name: String, age: Int) {
let newPerson = Person(name: name, age: age)
persons.append(newPerson)
}
}
// MockData
struct MockData {
static let persons = [Person(name: "Ali", age: 18), Person(name: "Jane", age: 22)]
}
// View
struct ContentView: View {
let viewModel = PersonListViewModel()
var body: some View {
VStack {
List(viewModel.persons, id: \.name) { person in
Text("\(person.name), Age: \(person.age)")
}.onAppear {
if viewModel.persons.isEmpty {
viewModel.persons = MockData.persons
}
}
Button {
viewModel.persons.append(Person(name: "Alex", age: 30))
} label: {
Text("Add Alex, 30")
}
}
}
}
#Preview {
ContentView()
}
2. Observation Pattern
Imagine you’re in a classroom where the teacher is an announcer with a news bulletin. Whenever there’s an update or news, the teacher announces it to the entire class. As a student (listener), if you’re in the class, you hear the news right away. If you step out, you miss the update. When you return, you’re back in the loop for any new announcements.
In programming, the Observer pattern works similarly. It’s a way for objects (observers) to “listen” for changes or updates in another object (the subject). The subject keeps a list of its observers and notifies them of any state changes, usually by calling one of their methods.
This pattern is useful when you want to create a system where changes to one object need to be automatically reflected in others. For example, in a weather app, the weather station (subject) might broadcast temperature updates to multiple display elements (observers), like a temperature gauge or a forecast panel, ensuring they all show the current temperature without having to check it individually.
The Observer pattern allows objects to notify other objects about changes in their state. SwiftUI’s @Published
and ObservableObject
leverage this pattern. Starting iOS 17 we can use @Observable without using @Published and ObservableObject
@Observable class BooksCounterViewModel {
var booksCount: Int = 0
func addBook() {
booksCount += 1
}
}
struct ContentView: View {
let viewModel = BooksCounterViewModel()
var body: some View {
VStack {
Button("Add One More Book", action: {
viewModel.addBook()
}).padding(.bottom)
Text("\(viewModel.booksCount) books in the Library")
}
.padding()
}
}
3. Singleton Pattern
Imagine you have only one remote control that works with all the electronic devices in your living room. No matter where you are in the room or which device you want to control, you reach for that same remote. This remote control is unique; there’s just one for everything, making it simple and convenient to manage your devices.
In programming, the Singleton pattern is like having that one remote control. It ensures that a class has only one instance and provides a global point of access to it. This means no matter how many times you try to create a new instance of this class, you’ll always get the same, single instance.
Singleton Pattern: User Preferences Example
The Singleton pattern ensures that only one instance of a class exists throughout the app’s lifecycle. Here, we’ll use it to manage user preferences.
class UserPreferences {
static let shared = UserPreferences()
var notificationsEnabled: Bool = false
var bgColor: Color?
private init() {} // Private initializer to ensure Singleton usage
}
SwiftUI ContentView
In the ContentView
, we use the Singleton to toggle notification settings and background color.
struct ContentView: View {
@State private var notificationsEnabled: Bool = UserPreferences.shared.notificationsEnabled
var body: some View {
ZStack {
UserPreferences.shared.bgColor ?? .white
Form {
Toggle("Enable Notifications and change BG", isOn: $notificationsEnabled)
}
.background( UserPreferences.shared.bgColor ?? .white)
.scrollContentBackground(.hidden)
.onChange(of: notificationsEnabled, initial: true) { oldValue, newValue in
UserPreferences.shared.notificationsEnabled = newValue
if UserPreferences.shared.bgColor == .red {
UserPreferences.shared.bgColor = .blue
} else {
UserPreferences.shared.bgColor = .red
}
}
.padding()
}
.padding(.top)
.ignoresSafeArea()
}
}
4. Builder Pattern
The Builder pattern is like ordering a custom burger at a restaurant. Instead of telling the chef all your preferences at once (which can be easy to mix up or forget), you specify each ingredient step by step: “Start with a beef patty, add lettuce, then cheese, and finally, pickles.” This way, you ensure the burger is made exactly to your liking, with all your chosen ingredients.
In programming, the Builder pattern lets you construct complex objects piece by piece. Instead of creating an object all at once (which might require a lot of parameters, some of which could be optional), you use a builder class that takes care of assembling the object for you. You call methods on the builder, each adding a part of the final object, and then finally, you ask the builder to give you the completed object.
This pattern is especially useful when you have an object that requires several steps to build or when the object needs many variations. It makes your code cleaner and more readable, as you avoid constructors with lots of parameters and can clearly see which object parts are being set at each step.
CustomText Builder
This builder will allow us to chain configuration methods to set up a Text
view with various styles.
struct CustomTextBuilder {
private var text: String
private var font: Font = .body
private var textColor: Color = .black
private var multilineTextAlignment: TextAlignment = .leading
init(_ text: String) {
self.text = text
}
func font(_ font: Font) -> CustomTextBuilder {
var builder = self
builder.font = font
return builder
}
func textColor(_ color: Color) -> CustomTextBuilder {
var builder = self
builder.textColor = color
return builder
}
func multilineTextAlignment(_ alignment: TextAlignment) -> CustomTextBuilder {
var builder = self
builder.multilineTextAlignment = alignment
return builder
}
func build() -> some View {
Text(text)
.font(font)
.foregroundColor(textColor)
.multilineTextAlignment(multilineTextAlignment)
}
}
Usage in ContentView
Now, let’s use our CustomTextBuilder
in a ContentView
to demonstrate its flexibility and how it enhances readability and customization.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
CustomTextBuilder("Hello, UIExamples.com Friend!")
.font(.title)
.textColor(.blue)
.multilineTextAlignment(.center)
.build()
CustomTextBuilder("This is an example using the Builder Pattern.")
.font(.headline)
.textColor(.red)
.multilineTextAlignment(.leading)
.build()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
5. Strategy Pattern
The Strategy pattern is a design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable. Imagine you have a task, and there are multiple ways (strategies) to accomplish it. The Strategy pattern allows you to switch between these strategies easily, even during runtime, based on the situation or user preference.
Here’s an easy way to understand it:
Think of the Strategy pattern as going on a trip and having to choose the mode of transportation. Your goal (task) is to get to your destination, but you can get there in several ways: you could drive a car, take a bus, ride a bike, or even walk. Each of these modes of transportation represents a different strategy for achieving your goal.
Using the Strategy pattern, you can easily switch your transportation method without changing your destination or how you plan your trip. Just like you might choose to ride a bike on a sunny day or take a bus when it’s raining, the Strategy pattern allows your software to adapt its behavior dynamically by switching between different algorithms or strategies based on the current conditions or user preferences.
Strategy Pattern Example
This simplified example of the Strategy pattern allows users to switch between bold and italic text styles within the app. By adhering to the TextStyleStrategy
protocol, BoldTextStyleStrategy
and ItalicTextStyleStrategy
provide concrete implementations for styling text. The ContentView
includes a toggle for the user to select their preferred text style. Depending on the toggle state (useBoldText
), it dynamically chooses the appropriate strategy to apply to the displayed text. This pattern showcases how you can encapsulate and interchange different behaviors (text styles, in this case) seamlessly within a SwiftUI view.
First, we define a protocol that all our text style strategies will conform to.
protocol TextStyleStrategy {
func applyStyle(to text: String) -> Text
}
Implement Concrete Strategies
Next, we create concrete implementations of TextStyleStrategy
for bold and italic text styles.
struct BoldTextStyleStrategy: TextStyleStrategy {
func applyStyle(to text: String) -> Text {
Text(text).fontWeight(.bold)
}
}
struct ItalicTextStyleStrategy: TextStyleStrategy {
func applyStyle(to text: String) -> Text {
Text(text).italic()
}
}
ContentView with Strategy Selection
In ContentView
, the user can toggle between bold and italic text styles.
import SwiftUI
struct ContentView: View {
@State private var useBoldText = true
private let text = "Hello, Strategy Pattern!"
var body: some View {
VStack(spacing: 20) {
Text("Choose Text Style:")
Toggle("Use Bold Text", isOn: $useBoldText)
.fixedSize()
if useBoldText {
BoldTextStyleStrategy().applyStyle(to: text)
} else {
ItalicTextStyleStrategy().applyStyle(to: text)
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6. Factory Pattern
Imagine you’re at a car manufacturing plant where there are different assembly lines for various car models. When you order a car, you don’t worry about the specifics of how each part is assembled. Instead, you just specify the model you want, and the factory takes care of producing the car according to that model’s specifications.
The Factory pattern in programming works similarly. It provides a way to create objects without specifying the exact class of object that will be created. You define an interface or a base class for creating an object, but let subclasses or another class decide which class to instantiate. This pattern is particularly useful when you have a set of related objects that share a common theme but have different details.
Simple SwiftUI Example: Animal Sounds
Let’s create a simple SwiftUI app that uses the Factory pattern to play different animal sounds.
This SwiftUI app demonstrates the Factory pattern by allowing the user to select an animal (Cat or Dog) and display the corresponding animal sound when a button is pressed. The AnimalSoundFactory
acts as the factory, creating an instance of AnimalSound
based on the selected animal type, abstracting the creation logic away from the ContentView
. This way, the ContentView
doesn’t need to know about the concrete implementation details of each animal sound, following the Factory pattern’s principle of creating objects without specifying their concrete classes.
First, define an interface for your product, in this case, AnimalSound
, and then implement concrete products.
protocol AnimalSound {
func makeSound() -> String
}
struct CatSound: AnimalSound {
func makeSound() -> String { "Meow!" }
}
struct DogSound: AnimalSound {
func makeSound() -> String { "Woof!" }
}
Create the Factory
Now, implement the Factory to decide which product to create based on an identifier.
enum AnimalType {
case cat, dog
}
class AnimalSoundFactory {
static func createAnimalSound(for type: AnimalType) -> AnimalSound {
switch type {
case .cat:
return CatSound()
case .dog:
return DogSound()
}
}
}
Use in SwiftUI View
Finally, use the Factory in a SwiftUI view to display an animal sound based on user selection.
struct ContentView: View {
@State private var selectedAnimal: AnimalType = .cat
@State private var animalSound: String = ""
var body: some View {
VStack {
Picker("Select an animal", selection: $selectedAnimal) {
Text("Cat").tag(AnimalType.cat)
Text("Dog").tag(AnimalType.dog)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Button("Make Sound") {
let animal = AnimalSoundFactory.createAnimalSound(for: selectedAnimal)
animalSound = animal.makeSound()
}
Text(animalSound)
.font(.largeTitle)
.padding()
}
}
}
7. Adapter Pattern
Imagine you’re on a trip abroad and you’ve brought along your favorite electronic gadgets. Upon arriving, you realize the plug of your charger doesn’t fit the local electrical outlets. The solution? You use an adapter that allows your charger to connect to the local outlets, enabling you to use your devices without any issue.
The Adapter pattern in programming serves a similar purpose. It allows objects with incompatible interfaces to work together. Essentially, it acts as a bridge between two incompatible interfaces, much like a physical adapter connects plugs to foreign outlets.
Simple SwiftUI Example: Temperature Display
Let’s say we have a legacy temperature sensor system that provides temperature in Fahrenheit, but we want to display the temperature in Celsius in our SwiftUI app. We can use the Adapter pattern to bridge the Fahrenheit system to our Celsius-based display.
Step 1: Define the Incompatible Interface and Adapter
First, we have our legacy system that gives us the temperature in Fahrenheit.
// Legacy system
class FahrenheitTemperatureSensor {
func getTemperatureFahrenheit() -> Double {
return 72.0 // Example temperature in Fahrenheit
}
}
Next, we create an adapter to convert Fahrenheit to Celsius.
// Adapter
class CelsiusTemperatureAdapter {
private var fahrenheitSensor: FahrenheitTemperatureSensor
init(fahrenheitSensor: FahrenheitTemperatureSensor) {
self.fahrenheitSensor = fahrenheitSensor
}
func getTemperatureCelsius() -> Double {
return (fahrenheitSensor.getTemperatureFahrenheit() - 32) * 5 / 9
}
}
Step 2: Use the Adapter in SwiftUI View
Now, let’s use the adapter in our SwiftUI view to display the temperature in Celsius.
import SwiftUI
struct ContentView: View {
private let sensor = FahrenheitTemperatureSensor()
private var adapter: CelsiusTemperatureAdapter
init() {
adapter = CelsiusTemperatureAdapter(fahrenheitSensor: sensor)
}
var body: some View {
VStack {
Text("Temperature")
.font(.headline)
Text("\(adapter.getTemperatureCelsius(), specifier: "%.1f") °C")
.font(.title)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Explanation
This SwiftUI example illustrates the Adapter pattern by using a CelsiusTemperatureAdapter
to convert temperatures from Fahrenheit (provided by a legacy FahrenheitTemperatureSensor
) to Celsius, enabling the ContentView
to display the temperature in the desired format. The adapter effectively bridges the gap between the old Fahrenheit system and the new requirement to display temperatures in Celsius, demonstrating how the Adapter pattern can solve compatibility issues in software development.
8. Facade Pattern
Imagine you’re at a complex entertainment system with multiple devices: a TV, a Blu-ray player, a sound system, and maybe even a game console. Each device has its own remote and settings. Turning on a movie involves several steps across different devices. Now, imagine having a single remote with a “Watch Movie” button that knows exactly which devices to turn on, which inputs to set, and what volume is just right. This simplification is what the Facade pattern does in programming.
The Facade pattern provides a simplified interface to a complex system, making it easier to use. It doesn’t remove the complexity; it just hides it behind a simpler interface.
Simple SwiftUI Example: Weather Info Display
Let’s create a Facade that simplifies fetching and displaying weather information from multiple subsystems (like temperature, humidity, and weather condition) into a single, easy-to-use interface.
This SwiftUI example demonstrates the Facade pattern by encapsulating the complexity of fetching weather information from multiple services (temperature, humidity, condition) into a single WeatherFacade
class. The ContentView
then uses this facade to display the combined weather information in a simple and concise way. The Facade pattern allows developers to work with a complex system through a simplified interface, making it easier to use and reducing dependencies.
Step 1: Define Subsystems
First, define the subsystems that provide specific pieces of weather information.
class TemperatureService {
func getTemperature() -> String { "72°F" }
}
class HumidityService {
func getHumidity() -> String { "65%" }
}
class ConditionService {
func getCondition() -> String { "Sunny" }
}
Step 2: Create the Facade
Next, create a Facade that uses these services to provide simplified access to the weather information.
class WeatherFacade {
private let temperatureService = TemperatureService()
private let humidityService = HumidityService()
private let conditionService = ConditionService()
func getWeatherInfo() -> String {
let temp = temperatureService.getTemperature()
let humidity = humidityService.getHumidity()
let condition = conditionService.getCondition()
return "It's \(temp) with \(humidity) humidity and \(condition) outside."
}
}
Step 3: Use the Facade in SwiftUI View
Finally, use the Facade in a SwiftUI view to display the weather information.
import SwiftUI
struct ContentView: View {
private let weatherFacade = WeatherFacade()
var body: some View {
Text(weatherFacade.getWeatherInfo())
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}