# SwiftUI - Decoupling Models

## **Problem:**

How can we avoid breaking an entire app when a model changes?

## **Solution:**

Decouple each model based on their specific domain.

---

### **A Deeper Dive**

When working with data from various sources, a common challenge developers face is the potential for entire sections of an app to break when there's a minor model change. A robust solution to this problem is decoupling models based on their specific domains.

## Models

Here we will represent the user model in 3 different ways:

1. **UserApiV2** - Represents our data source's structure from an API.
    
2. **User** - Acts as an intermediary and generic model, used in the entire app.
    
3. **UserViewData** - Transforms and formats data for SwiftUI view representation.
    

---

### Models definition

Let's get started creating the `UserApiV2` will represent the API model

```swift
struct UserApiV2: Codable { // 1
  let id: Int
  let firstName: String
  let lastName: String
  let dob: String // ISO8601 format
  let phoneNumber: PhoneApiV2
  let address: AddressApiV2
  let email: String
  
  struct PhoneApiV2: Codable { // 1
    let countryPrefix: String
    let number: String
  }
  
  struct AddressApiV2: Codable { // 1
    let street: String
    let city: String
    let state: String
    let postalCode: String
  }
}
```

1. You notice here that all objects are conforming to the `Codable` protocol because this is a requirement that will allow us to encode/decode objects.  
    The good thing here to notice is that this `Codable` conformance is an implementation detail from the API side that will not pollute other user objects.
    

Now create the `User` object that will be used everywhere in the app

```swift
struct User {
  let id: Int
  let firstName: String
  let lastName: String
  let dob: String
  let phoneNumber: Phone
  let address: Address
  let email: String
  
  struct Phone {
    let countryPrefix: String
    let number: String
  }
  
  struct Address {
    let street: String
    let city: String
    let state: String
    let postalCode: String
  }
}
```

* The important thing to note here is that no more `Codable` conformance and it's a good point, we don't want to leak implementation details from infrastructure (API, database, ...) in our model app.
    
* The `User` object keeps the same property name but feel free you change it if you want.
    
* The `User` object keeps the same object structure but you can adapt it as you want.
    
* The `User` object keeps all properties of the `UserApiV2` object, because it needs them all (but if some are not neccessary you can omit them and come back later to add them if needed).
    
* So this `User` model is the model of your app, It belongs to you and is your responsibility to shape it as you want it to be.
    

Now create the `UserViewData` model that will be used in the SwiftUI view.

```swift
struct UserViewData {
  let name: String
  let birthDate: String
  let phoneNumber: String
  let address: String
  let email: String
}
```

* This model doesn't contain any logic, it just displays already formatted data that are required for the design.  
    This is for this reason that all properties are of type `String`
    

---

### Load User interface

First I will create an interface `UserLoading` that will abstract the `User` load using `protocol`

```swift
protocol UserLoading {
  func fetchUser() async throws -> User
}
```

* We will always use this interface to fetch the user, which allows us to use different implementations (API, database, ...)
    

---

## Models creation

### Create `UserApiV2` model

Now we need to create the `UserApiV2` model.  
We will mock the backend response using a local **JSON**

```swift
struct MockUrlSessionUserLoading: UserLoading {
  func getUser() async throws -> User {
    let url = Bundle.main.url(forResource: "user", withExtension: "json")! // 1
    let data = try Data(contentsOf: url)
    let userApiV2 = try JSONDecoder().decode(UserApiV2.self, from: data) 
    return userApiV2.toUser() // 2
  }
}
```

1. We need to create the **JSON** file **user.json** that be read locally.
    
2. We must create the `toUser()` function that will convert the `UserApiV2` object to the `User` object we want to return.
    

Start creating the **user.json** file  
Create a new file  
In the **Other** section choose **Empty** and call it **user.json**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1694517013915/904dc571-a505-4554-a958-675b5a2aeea7.png align="center")

In the **user.json** copy and paste this code:

```plaintext
{
    "id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "dob": "2023-09-10T10:00:00Z",
    "phoneNumber": {
        "countryPrefix": "+1",
        "number": "1234567890"
    },
    "address": {
        "street": "123 Apple St.",
        "city": "Cupertino",
        "state": "CA",
        "postalCode": "95014"
    },
    "email": "john.doe@example.com"
}
```

Now create the `toUser()` function that will be an extension of the `UserApiV2` type

```swift
extension UserApiV2 {
  func toUser() -> User {
    return User(
      id: self.id,
      firstName: self.firstName,
      lastName: self.lastName,
      dob: self.dob,
      phoneNumber: .init(
        countryPrefix: self.phoneNumber.countryPrefix,
        number: self.phoneNumber.number
      ),
      address: .init(
        street: self.address.street,
        city: self.address.city,
        state: self.address.state,
        postalCode: self.address.postalCode
      ),
      email: self.email
    )
  }
}
```

* Here conversion is very straightforward, all the properties are identical (that is not always the case)
    

---

## ViewModel creation

Let's create the view model that will be injected into the SwiftUI view

```swift
class UserViewModel: ObservableObject {
  @Published var userViewData = emptyUserViewData()
  
  private let userLoader: UserLoading
  
  init(userLoader: UserLoading) { // 1
    self.userLoader = userLoader
  }
  
  func getUser() {
    Task {
      do {
        let user = try await userLoader.getUser() // 2
        userViewData = user.toUserViewData() // 3
      } catch {
        // Handle any errors
      }
    }
  }
}

private func emptyUserViewData() -> UserViewData {
    UserViewData(name: "", birthDate: "", phoneNumber: "", address: "", email: "")
}
```

1. I use dependency injection to use the `UserLoading` .
    
2. I get the user from the `userLoader` via the `getUser()` function defined in the interface.
    
3. I converted the `User` object to the `UserViewData` object to be able to update the UI using the `toUserViewData()` function.
    

Create the `toUserViewData()` function using `extension` on the `User` object

```swift
extension User {
  func toUserViewData() -> UserViewData {
    return UserViewData(
      name: "\(self.firstName) \(self.lastName)", // 1
      birthDate: DateFormatter.displayDateFormatter.string(from: ISO8601DateFormatter().date(from: self.dob) ?? Date()), // 2
      phoneNumber: "\(self.phoneNumber.countryPrefix) \(self.phoneNumber.number)", // 3
      address: "\(self.address.street)\n\(self.address.city), \(self.address.state) \(self.address.postalCode)", // 4
      email: self.email
    )
  }
}

private extension DateFormatter {
  static let displayDateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter
  }()
}
```

1. For the name, I group the first and last name.
    
2. The date is transformed into a readable format.
    
3. The phone number is composed using the country prefix and number.
    
4. The address is created with street, city, state and postal code and returned to the line when needed.
    

The UI will update every time this model changes

---

## View Creation

Create the `UserView`

```swift
struct UserView: View {
  @ObservedObject var viewModel: UserViewModel
  
  var body: some View {
    Form {
      Text(viewModel.userViewData.name) // 1
      Text(viewModel.userViewData.birthDate) // 1
      Text(viewModel.userViewData.phoneNumber) // 1
      Text(viewModel.userViewData.address) // 1
      Text(viewModel.userViewData.email) // 1
    }
    .onAppear {
      viewModel.getUser()
    }
  }
}
```

1. This is very simple, not need to handle the logic in the SwiftUI view, the heavy work has been already done. It just needs to render the view with the provided formatted data.
    

Let's create a preview to see if everything is working fine:

```swift
struct UserView_Previews: PreviewProvider {
  static var previews: some View {
    let userLoader = MockUrlSessionUserLoading()
    let viewModel = UserViewModel(userLoader: userLoader)
    UserView(viewModel: viewModel)
  }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1694519156781/0bdabf9f-c099-44ab-961c-9c070d3298f6.png align="center")

The UI rendered as expected everything is working fine

---

Now we will expend the problem and imagine you have a new requirement:

**"The cached user must be used in the remote get failed"**

We now potentially use a model that has been used for the persistence of the `User`  
We use **Realm** here to demonstrate the benefit of decoupling our model

## Create UserRealm model

Start by creating all the types required to be able to persist the model `UserRealm`, `PhoneRealm` and `AdressRealm`

```swift
class UserRealm: Object {
  @objc dynamic var id: Int = 0
  @objc dynamic var firstName: String = ""
  @objc dynamic var lastName: String = ""
  @objc dynamic var dob: String = ""
  @objc dynamic var phone: PhoneRealm?
  @objc dynamic var address: AddressRealm?
  @objc dynamic var emailAddress: String = ""
  
  convenience init(id: Int, firstName: String, lastName: String, dob: String, phone: PhoneRealm, address: AddressRealm, emailAddress: String) {
    self.init()
    self.id = id
    self.firstName = firstName
    self.lastName = lastName
    self.dob = dob
    self.phone = phone
    self.address = address
    self.emailAddress = emailAddress
  }
  
  required override init() {
    super.init()
  }
}

class PhoneRealm: Object {
  @objc dynamic var countryPrefix: String = ""
  @objc dynamic var number: String = ""
  
  convenience init(countryPrefix: String, number: String) {
    self.init()
    self.countryPrefix = countryPrefix
    self.number = number
  }
  
  required override init() {
    super.init()
  }
}

class AddressRealm: Object {
  @objc dynamic var street: String = ""
  @objc dynamic var city: String = ""
  @objc dynamic var state: String = ""
  @objc dynamic var postalCode: String = ""
  
  convenience init(street: String, city: String, state: String, postalCode: String) {
    self.init()
    self.street = street
    self.city = city
    self.state = state
    self.postalCode = postalCode
  }
  
  required override init() {
    super.init()
  }
}
```

We also need to create the `extension` that will allow us to convert `UserRealm` to `User`

```swift
extension UserRealm {
  func toUser() -> User {
    return User(
      id: self.id,
      firstName: self.firstName,
      lastName: self.lastName,
      dob: self.dob,
      phoneNumber: User.Phone(
        countryPrefix: self.phone?.countryPrefix ?? "",
        number: self.phone?.number ?? ""
      ),
      address: User.Address(
        street: self.address?.street ?? "",
        city: self.address?.city ?? "",
        state: self.address?.state ?? "",
        postalCode: self.address?.postalCode ?? ""
      ),
      email: self.emailAddress
    )
  }
}
```

Create the `MockRealmUserLoading` will fake the query in the database

```swift
struct MockRealmUserLoading: UserLoading {
  func getUser() async throws -> User {
    let mockPhone = PhoneRealm(countryPrefix: "+1", number: "1234567890")
    let mockAddress = AddressRealm(street: "123 Apple St", city: "Tech Town", state: "Silicon Valley", postalCode: "95014")
    let mockRealmUser = UserRealm(id: 1, firstName: "John", lastName: "Doe", dob: "1990-01-01T00:00:00Z", phone: mockPhone, address: mockAddress, emailAddress: "john.doe@example.com")
    
    return mockRealmUser.toUser()
  }
}
```

Now the requirement is to start from remote and if failed return the user locally  
We can create a new object `RemoteUserLoadingWithLocalFallback` that will compose those types:

```swift
class RemoteUserLoadingWithLocalFallback: UserLoading {
  let primary: UserLoading
  let secondary: UserLoading
  
  init(primary: UserLoading, secondary: UserLoading) {
    self.primary = primary
    self.secondary = secondary
  }
  
  func getUser() async throws -> User {
    do {
      return try await primary.getUser()
    } catch {
      return try await secondary.getUser()
    }
  }
}
```

The app still compiles.  
The live preview still works when updating the composition using the `RemoteUserLoadingWithLocalFallback`

```swift
struct UserView_Previews: PreviewProvider {
  static var previews: some View {
    let userRemoteLoader = MockUrlSessionUserLoading()
    let userLocalLoader = MockRealmUserLoading()
    let userLoader = RemoteUserLoadingWithLocalFallback(
      primary: userRemoteLoader,
      secondary: userLocalLoader
    )
    let viewModel = UserViewModel(userLoader: userLoader)
    UserView(viewModel: viewModel)
  }
}
```

Of course, you can decide to replace frameworks:  
**URLSession -&gt; Alamofire**  
**Realm -&gt; CoreData**  
You just need to create new types `AlamofireUserLoading` and `CoreDataUserLoading` that will conform to the `UserLoading` interface and injecting those new types will be an easy change because the entire app is not coupled with any framework, they are just details of the implementation

---

## Conclusion

I hope that you enjoy the power of the domain-specific model.  
Through domain-specific modeling (`UserAPIV2`, `UserRealm`), we have created a flexible structure that not only preserves the integrity of our SwiftUI app (`User`) but also makes it adaptable to different data sources (`UserDataView`).  
Whether we're fetching data from an API or a local Realm database, the foundation remains unaffected.  
This modular approach ensures that our app can scale with changing requirements without significant refactoring, ensuring a robust and resilient architecture.

That's it !! 🎉
