The Drawbacks of Using FetchRequest in SwiftUI Views and Models

Codecat15
5 min readAug 5, 2023

--

https://unsplash.com/photos/-HIiNFXcbtQ?utm_source=unsplash&utm_medium=referral&utm_content=creditShareLink

In SwiftUI, FetchRequest is a powerful property wrapper that simplifies data retrieval from Core Data and automates view updates.

However, using FetchRequest directly in SwiftUI views and data models can lead to several drawbacks and code complexities that developers should be aware of.

One can argue that SwiftUI is modern and does things differently but SwiftUI is not the only declarative language which does things differently, also there are programming paradigms over 30 decades that have withstood the test of time.

In this article, we will explore these drawbacks through code examples and understand why they violate good programming practices.

A. Violation of Separation of Concerns:

One of the key principles of good programming is the separation of concerns, where different parts of the code have distinct responsibilities. Placing FetchRequest in the data model or the SwiftUI view violates this principle, as it blurs the line between data retrieval and presentation logic.

Example:

struct ContentView: View {
@FetchRequest(entity: Task.entity(), sortDescriptors: [])
var tasks: FetchedResults<Task>

var body: some View {
VStack {
List(tasks) { task in
Text(task.name ?? "")
}
}
}
}

In this example, FetchRequest is directly used in the SwiftUI view, resulting in the view, handling the data retrieval logic, which goes against the separation of concerns.

B. Reduced Testability:

When FetchRequest is tightly integrated into the SwiftUI view or data model, it becomes challenging to test them in isolation.

With FetchRequest integrated into SwiftUI views, testing the view-related logic and data retrieval will happen together.

This makes it harder to focus solely on testing the view-related behavior without involving the data retrieval process. Unit testing requires testing individual units of code in isolation to ensure they work correctly independently.

Example:

struct TaskList: View {
@FetchRequest(entity: Task.entity(), sortDescriptors: [])
var tasks: FetchedResults<Task>

var body: some View {
List(tasks) { task in
Text(task.name ?? "")
}
}
}

struct ContentView: View {
var body: some View {
VStack {
TaskList()
}
}
}

In this example, testing the TaskList view in isolation becomes difficult due to the embedded FetchRequest.

When FetchRequest is directly used within the SwiftUI view, it creates a dependency on Core Data.

This tight coupling can lead to difficulties in testing views without interacting with the actual data storage, as the data retrieval mechanism becomes an integral part of the view.

Although some may argue that we can simply write UI-Tests which will indirectly test everything but it’s more like saying I don’t care if individual parts of my car are not tested I will drive it on the highway anyway

Imagine car industry not testing individual component and directly releases cars like this.

C. Flexibility to change:
Including FetchRequest or writing database functions in the model class ties the model tightly to the Core Data framework, limiting its flexibility for future changes or alternative data storage frameworks.

By keeping the FetchRequest separate, you maintain the freedom to switch to different data retrieval mechanisms like Firebase, realm etc OR adapt to changes to SwiftUI views without modifying the data retrieval logic.

D. Code Reusability:
Including the FetchRequest inside the model impacts reusability.

If another part of the application needs to use the same model but with a different data source (e.g., an API instead of a database), it becomes challenging to reuse the model without also bringing in unnecessary database code.


// User data model
class User: Decodable {
var id: Int
var name: String
var email: String

init(id: Int, name: String, email: String) {
self.id = id
self.name = name
self.email = email
}

// Database calling code
func saveToDatabase() {
// Code to save the user data to a Core Data database
}
}

// let's say we have two different parts of the application
// that need to use the User model
// one part that interacts with a Core Data database
// another part that communicates with a remote API.

// Part of the application using Core Data
let user = User(id: 1, name: "CodeCat15", email: "CodeCat15@gmail.com")
user.saveToDatabase()

// Part of the application using API
let user = User(id: 2, name: "Bruce", email: "Bruce@example.com")
// We cannot use saveToDatabase() here because we are not using Core Data,
// but the method is part of the model

In this example, the User model class contains the saveToDatabase() method, which is specific to Core Data.

When trying to use the same model for the API part, we cannot directly call saveToDatabase() as it's not applicable in that context.

Keeping the model & View clean and free from database-specific logic allows for better abstraction and higher-level modeling of the application’s domain.

One could completely ignore the database code, but the above code leads to poor abstraction practices and also breaking single responsibility principle.

E. Violates Single Responsibility principle:
The model has a single responsibility, which is to encapsulate data. The responsibility of the view is to ask for data without knowing where the data is coming from and provide data to it’s UI components.

If one introduces the database code inside the view or data models they are adding additional responsibility to the SwiftUI views and data models.

Database calling code belongs to a different layer, like a data access or repository layer, adhering to the single responsibility principle.

F. Poor Abstraction:
Writing database code inside the data model introduces poor abstractions. The models should not be aware of the specific data persistence mechanism (e.g., database), and database operations should be abstracted into a separate layer.

Suppose we have two models, product and order. The order model contains a collection of product, but since we are writing database code inside the model I guess you already know where this is heading

// Product Model
class Product {
var id: Int
var name: String

init(id: Int, name: String) {
self.id = id
self.name = name
}

func saveToDatabase() {
// Code to save the product data to the database
}
}

// Order Model
class Order {
var id: Int

// collection of product
var products: [Product]

init(id: Int, products: [Product]) {
self.id = id
self.products = products
}

func saveToDatabase() {
// Code to save the order data to the database

self.products.forEach { product in
// access product database operations inside Order
product.saveToDatabase()
}
}
}

While FetchRequest is a convenient way to fetch data in SwiftUI, its direct usage in views and data models can lead to drawbacks that violate essential programming practices.

Separation of concerns, Testability, Single Responsibility, Reusability, Poor Abstractions are compromised when FetchRequest is tightly integrated with views or data models of your application.

To avoid these issues, consider using ViewModels or separate data retrieval layers like repository to keep your SwiftUI codebase clean, maintainable, and in alignment with best programming practices.

I trust that this comprehensive article shows the rationale behind why incorporating FetchRequest directly into views or data models is not advisable from a clean coding perspective.

Should someone choose to pursue this approach, I hope they remain cognizant of these associated drawbacks.

--

--

Codecat15
Codecat15

Responses (2)