SwiftUI Design with SharedModels

Arangott Ramesh Chandran
7 min readMar 25, 2023

--

As you embark on developing apps with SwiftUI, you’ll observe noticeable differences in application architecture and design patterns. You may encounter areas of confusion, such as deciding whether to utilize MVVM or implement a ViewModel and determining how to separate business logic from SwiftUI view classes. In this narrative, I’ll outline a straightforward method that I employed in one of my applications.

Introduction

“SwiftUI is a Declarative UI framework that automatically updates the UI when the underlying data changes, thanks to features such as the State property wrapper. This means that a ViewModel class is not strictly necessary in SwiftUI, since the framework manages the property’s storage and updates the view hierarchy as needed.

However, Model classes are still essential for defining the data in the application. In cases where multiple Model classes are used, it can be challenging to manage application-level State and reuse code across different Models. In this article, I will explain an approach that I tried in one of my applications to address these challenges.”

Model classes in UIKit and SwiftUI

In UIKit, the Model class is typically a standalone class that represents the data or business logic of an application. It is responsible for storing and managing data, and can be used by other classes such as View Controllers and Views to access and manipulate the data.

In SwiftUI, the Model class is usually a struct or a class that conforms to the ObservableObject protocol. It represents the data or state of a view, and is responsible for updating the view whenever the data changes. The @Published property wrapper is used to mark properties in the Model class as observable, which triggers updates to the view whenever the property is modified.

An example of a Model class using UIKit

// Simple Model class for managing a user data
class Task {
var title: String
var dueDate: Date
var completed: Bool

init(title: String, dueDate: Date, completed: Bool = false) {
self.title = title
self.dueDate = dueDate
self.completed = completed
}
}

class TasksViewController: UIViewController {
var tasks: [Task] = []

func addTask(_ task: Task) {
tasks.append(task)
// Update the view to reflect the new task
}

func deleteTask(at index: Int) {
tasks.remove(at: index)
// Update the view to reflect the deleted task
}
}

The above example can be improved, but it’s still a topic of debate. While UIKit generally follows the MVC pattern, if you’re more concerned with shared business logic and code reusability, you may want to consider using the MVVM model. In this case, the ViewController directly handles the task list and cannot be referenced in other ViewControllers.

How MVVM Architecture Can Enhance Your Codebase

// Task model class
class Task {
var title: String
var dueDate: Date
var completed: Bool

init(title: String, dueDate: Date, completed: Bool = false) {
self.title = title
self.dueDate = dueDate
self.completed = completed
}
}

// TaskViewModel class
class TaskViewModel {
private var tasks: [Task] = []

func fetchTasks() {
// Fetch tasks from a data source and populate the tasks array
let task1 = Task(title: "Complete math homework", dueDate: Date(), completed: false)
let task2 = Task(title: "Go grocery shopping", dueDate: Date(), completed: false)
let task3 = Task(title: "Finish work presentation", dueDate: Date(), completed: true)
tasks = [task1, task2, task3]
}

func markTaskAsCompleted(at index: Int) {
tasks[index].completed = true
}

func numberOfTasks() -> Int {
return tasks.count
}

func task(at index: Int) -> Task {
return tasks[index]
}
}

// TaskListViewController class
class TaskListViewController: UIViewController {
private let taskViewModel = TaskViewModel()

override func viewDidLoad() {
super.viewDidLoad()
taskViewModel.fetchTasks()
// Display tasks in the UI
}

func didCompleteTask(at index: Int) {
taskViewModel.markTaskAsCompleted(at: index)
// Update the UI to reflect the completed task
}
}

Implementing the Same Functionality in SwiftUI

import SwiftUI

class TaskList: ObservableObject {
@Published var tasks = [
Task(title: "Buy groceries", dueDate: Date(), completed: false),
Task(title: "Finish project", dueDate: Date().addingTimeInterval(86400), completed: false),
Task(title: "Call Mom", dueDate: Date().addingTimeInterval(172800), completed: false)
]

func fetchTasks() {
//ToDo
}

func saveTasks() {
//ToDo
}
}

struct Task: Identifiable {
let id = UUID()
var title: String
var dueDate: Date
var completed: Bool
}

struct TaskListView: View {
@ObservedObject var taskList = TaskList()

var body: some View {
NavigationView {
List {
ForEach(taskList.tasks) { task in
// logic for Tasks
}
}
}
}
}

In the example, the TaskList class could be interpreted as a ViewModel because it's responsible for managing the list of Task objects and publishing changes to the UI.

However, it's worth noting that in SwiftUI, the line between what constitutes a Model and a ViewModel can be somewhat blurred, as SwiftUI's @ObservedObject and @StateObject property wrappers allow for more flexibility in organizing your data and logic. Ultimately, the naming conventions you choose will depend on your own preferences and the specific needs of your application. In my scenario, a dedicated ViewModel class is not required.

Photo by ThisisEngineering RAEng on Unsplash

The Importance of Considering a SharedModel Class

In SwiftUI, a shared model class can be useful for several reasons. First, it allows for data to be shared and accessed between multiple views without having to pass data around manually. This can simplify the code and reduce the risk of errors.

The following example helps to explain the concept

(An example scenrio with Home screen & Task Screen)

In the above scenario, “TaskView” and “DashboardView” are both associated with the same “TaskList” model, but they are two distinct instances. If you want to share some state between them using memory, it can be challenging. In more complex applications that deal with numerous models, it may not be appropriate to declare “TaskList” as a Singleton class or EnvironmentObject.

Here, we can consider using a shared model class that can help with managing the state throughout the app.

Instead of declaring SharedModel class as a Singleton class, we can declare it as a EnvironmentObject.

“In SwiftUI, an EnvironmentObject is a way to share an instance of an object across the views of an application. It allows views to access and modify shared data without the need for explicit passing of data between the views.

To use an EnvironmentObject, an instance of the object is created and injected into the application's environment using the environmentObject() modifier on a view or a view hierarchy. Once the object is in the environment, it can be accessed from any view in the hierarchy using the @EnvironmentObject property wrapper.

Changes to the object’s state are automatically propagated to all views that depend on it, triggering updates to their UI. This makes it a useful tool for managing shared state in SwiftUI applications.”

(Using EnvironmentObject to share the SharedModel Instance)

SharedModel Class Implementation

import SwiftUI

class TaskList: ObservableObject {
@Published var tasks = [
Task(title: "Buy groceries", dueDate: Date(), completed: false),
Task(title: "Finish project", dueDate: Date().addingTimeInterval(86400), completed: false),
Task(title: "Call Mom", dueDate: Date().addingTimeInterval(172800), completed: false)
]
}

class SharedModel: ObservableObject {
@Published var taskList = TaskList()
}

ContentView Implementation

import SwiftUI

struct ContentView: View {
@EnvironmentObject var sharedModel: SharedModel

var body: some View {
TabView {
TaskListView()
.tabItem {
Image(systemName: "list.dash")
Text("Task List")
}

DashboardView()
.tabItem {
Image(systemName: "chart.pie.fill")
Text("Dashboard")
}
}
.environmentObject(sharedModel)
}
}

Accessing Tasks in TaskListView via SharedModel

struct TaskListView: View {
@EnvironmentObject var sharedModel: SharedModel

var body: some View {
NavigationView {
List {
ForEach(sharedModel.taskList.tasks) { task in
// Display each task here
}
}
// Navigation bar and other views here
}
}
}

“Now that TaskListView has a dependency on SharedModel, your SwiftUI preview may start crashing. To fix this, add the following to the Preview class.”

struct TaskListView_Preview: PreviewProvider {
static var previews: some View {
TaskListView()
.environmentObject(SharedModel())
}
}

In real complex applications your SharedModel class may looks like the following with many other Model classes.

import SwiftUI

class SharedModel: ObservableObject {
@Published var taskList = TaskList()
@Published var userModel = UserModel()
@Published var settings = Settings()
@Published var profile = Profile()
}

class TaskList: ObservableObject {
@Published var tasks = [
Task(title: "Buy groceries", dueDate: Date(), completed: false),
Task(title: "Finish project", dueDate: Date().addingTimeInterval(86400), completed: false),
Task(title: "Call Mom", dueDate: Date().addingTimeInterval(172800), completed: false)
]
}

class UserModel: ObservableObject {
@Published var name = "John Doe"
@Published var age = 30
@Published var email = "johndoe@example.com"
}

class Settings: ObservableObject {
@Published var notificationsEnabled = true
@Published var darkModeEnabled = false
@Published var language = "English"
}

class Profile: ObservableObject {
@Published var bio = "I'm a software developer."
@Published var interests = ["coding", "reading", "traveling"]
}

Conclusion

In conclusion, SwiftUI provides a modern and efficient way of building user interfaces for iOS, macOS, watchOS, and tvOS. However, as our applications grow in complexity and we add more models and views, it can become difficult to manage the state of the app. This is where SharedModel comes into play, allowing us to create a centralized place to store and manage the state of our application across multiple views and models. By using EnvironmentObjects and ObservableObjects, we can make our SharedModel accessible to all parts of the app, making it easier to manage and update the state of our app. Overall, SharedModel is an essential tool for building scalable, maintainable, and reusable SwiftUI applications.

--

--

Arangott Ramesh Chandran

🌐 Mobile App Developer | 📱 iOS | Android | Flutter | React Native | Firebase | 🎬 Movie Buff | ✈️ Avid Traveler | 🌴 Kerala, India , exploring new horizons.