# SwiftUI - Integrate UI without a ready backend

## Problem: How to integrate UI with a not ready backend?
### Solution: By decoupling the model returned by the API from the model that manages the UI display !

Simple example with this interface inspired by the Message app:  
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643664264257/vBr0wPhPd.png)

The row that displays a message only needs data in `String` format and a `Bool` to know if there is a new message:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643664780351/44hINmMum.png)

Create the model `MessageUIModel` that meets this need:
```swift
struct MessageUIModel: Identifiable {
  let id: Int
  let initials: String
  let title: String
  let message: String
  let date: String
  let isUnread: Bool
}
```

Create the view model `MessagesViewModel` for this view with an array of `MessageUIModel` as a `@Published` property:
```swift
class MessagesViewModel: ObservableObject {
  @Published var messages = [MessageUIModel]()
  
  func fetch() {
    // Wait for backend to get data from API
  }
}
```

Integration of the view model in the `MessagesView`:
```swift
struct MessagesView: View {
  @StateObject var viewModel = MessagesViewModel()
  
  var body: some View {
    Text("Hello World !!")
  }
}
```

Creation of fake data from the `MessageUIModel` model with hardcoded values and `MessagesViewModel` inheritance :
```swift
class MockHardcodedMessagesViewModel: MessagesViewModel {
  override init() {
    super.init()
    fetch()
  }

  override func fetch() {
    messages = [
      .init(id: 1, initials: "AA", title: "Alexander Anderson", message: "Hello!! How are you?", date: "09:45 AM", isUnread: true),
      .init(id: 2, initials: "J", title: "Jessica", message: "This a short message", date: "Yesterday", isUnread: true),
      .init(id: 3, initials: "?", title: "+33 6 45 67 66 66", message: "Un message en français cool !!", date: "Tuesday", isUnread: false),
      .init(id: 4, initials: "GP", title: "Georges Pitt", message: "This a very long message to test the line limit that must only displays 1 line, and not more this text must be troncated", date: "1/3/22", isUnread: true)
    ]
  }
}
```

We must now display this data in the live preview using the `MockHardcodedMessageViewModel` class:
```swift
struct MessagesView_Previews: PreviewProvider {
  static var previews: some View {
    MessagesView(viewModel: MockHardcodedMessagesViewModel())
      .preferredColorScheme(.dark)
  }
}
```

Creation of the `MessagesView` which allows to display these datas:
```swift
struct MessagesView: View {
  @StateObject var viewModel = MessagesViewModel()
  
  var body: some View {
    NavigationView {
      ScrollView {
        LazyVStack {
          ForEach(viewModel.messages) { message in
            HStack {
              if message.isUnread {
                Circle()
                  .frame(width: 10, height: 10)
              } else {
                Circle()
                  .fill(.clear)
                  .frame(width: 10, height: 10)
              }
              Text(message.initials)
                .font(.title2)
                .bold()
                .frame(width: 50, height: 50)
                .foregroundColor(.white)
                .background(.gray)
                .clipShape(Circle())
              VStack(alignment: .leading, spacing: 6) {
                HStack {
                  Text(message.title)
                    .font(.headline)
                  Spacer()
                  Text(message.date)
                    .font(.callout)
                    .foregroundColor(.secondary)
                }
                Text(message.message)
                  .font(.body)
                  .foregroundColor(.secondary)
                  .lineLimit(1)
              }
            }
          }
          .padding()
        }
      }
      .navigationTitle("Messages")
    }
  }
}
```

You can visualize your fake datas in the live preview 😎:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643666388354/pQtWpMxpB.png)

Having created your UI, the back-end guys give you a payload of what the response **JSON** might look like:
```json
{
  "items": [
    {
      "id": 1,
      "firstname": "Alexander",
      "lastname": "Anderson",
      "message": "Hello!! How are you?",
      "phone_number": {
        "international": "+33 6 56 67 45 66"
      },
      "update_date": "2022-01-31T08:45:33Z",
      "unread_count": 1
    }
  ]
}
```

From this information create a fake **JSON** that will simulate a response from the backend, create a file of type empty and name it **messages.json**:
![ image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643552177558/HeIrtBxTe.png)

Creation of the **JSON** that simulates a response from the API server with the datas used previously:
```json
{
  "items": [
    {
      "id": 1,
      "firstname": "Alexander",
      "lastname": "Anderson",
      "message": "Hello!! How are you?",
      "phone_number": {
        "international": "+33 6 56 67 45 66"
      },
      "update_date": "2022-01-31T08:45:33Z",
      "unread_count": 1
    },
    {
      "id": 2,
      "firstname": "Jessica",
      "lastname": "",
      "message": "This a short message",
      "phone_number": {
        "international": "+33 6 45 67 66 66"
      },
      "update_date": "2022-01-30T17:54:37Z",
      "unread_count": 10
    },
    {
      "id": 3,
      "firstname": "",
      "lastname": "",
      "message": "Un message en français cool !!",
      "phone_number": {
        "international": "+33 6 45 67 99 66"
      },
      "update_date": "2022-01-25T17:12:33Z",
      "unread_count": 0
    },
    {
      "id": 4,
      "firstname": "Georges",
      "lastname": "Pitt",
      "message": "This a very long message to test the line limit that must only displays 1 line",
      "phone_number": {
        "international": "+33 6 45 67 66 98"
      },
      "update_date": "2022-01-03T12:54:23Z",
      "unread_count": 1
    }
  ]
}
```

Creation of a model that can parse this **JSON** with `Decodable`:
```
struct Root: Decodable {
  let items: [Message]
  
  struct Message: Decodable {
    let id: Int
    let firstname: String
    let lastname: String
    let message: String
    let phoneNumber: PhoneNumber
    let updateDate: Date
    let unreadCount: Int

    struct PhoneNumber: Decodable {
      let international: String
    }
  }
}
```

Creating an `extension` on the `Bundle` type to make it easier to decode **JSON** from a file:
```swift
extension Bundle {
  func decode<T: Decodable>(_ type: T.Type, from file: String) -> T {
    guard let url = self.url(forResource: file, withExtension: nil) else {
      fatalError("Failed to locate \(file) in bundle.")
    }

    guard let data = try? Data(contentsOf: url) else {
      fatalError("Failed get data from url \(url).")
    }

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    do {
      return try decoder.decode(T.self, from: data)
    } catch {
      fatalError("Failed to decode \(file) from bundle with error: \(error)")
    }
  }
}
```

Creating fake data from this **JSON** with the `MockApiMessagesViewModel` type:
```swift
class MockApiMessagesViewModel: MessagesViewModel {
  override init() {
    super.init()
    fetch()
  }

  override func fetch() {
    let apiMessages = Bundle.main.decode(Root.self, from: "messages.json").items

    messages = apiMessages.map { apiMessage in
      MessageUIModel(
        id: apiMessage.id,
        initials: "AA",
        title: apiMessage.firstname,
        message: apiMessage.message,
        date: ISO8601DateFormatter().string(from: apiMessage.updateDate),
        isUnread: false)
    }
  }
}
```

Use the fake data from the `MockApiMessageViewModel` class in the live preview:
```swift
struct MessagesView_Previews: PreviewProvider {
  static var previews: some View {
    MessagesView(viewModel: MockHardcodedMessagesViewModel())
      .preferredColorScheme(.dark)
    MessagesView(viewModel: MockApiMessagesViewModel())
      .preferredColorScheme(.dark)
  }
}
```

The data displayed are not correctly formatted:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643667234721/uX2Ys7Ihg.png)

Start with the `initials` property which displays the initials of the first name and last name or ? if empty :
```swift 
class MessagesViewModel: ObservableObject {
  ...
  // MARK: - Helpers

  func initials(for message: Root.Message) -> String {
    let firstLetter = String(message.firstname.prefix(1))
    let secondLetter = String(message.lastname.prefix(1))
    
    switch (!firstLetter.isEmpty, !secondLetter.isEmpty) {
    case (true, true):
      return "\(firstLetter)\(secondLetter)"
    case (true, _):
      return firstLetter
    case (_, true):
      return secondLetter
    default:
      return "?"
    }
  }

```

Use the function `initial(for:)` when creating the model `MessageUIModel`:
```swift
class MockApiMessagesViewModel: MessagesViewModel {
    ...
    messages = apiMessages.map { apiMessage in
      MessageUIModel(
        id: apiMessage.id,
        initials: initials(for: apiMessage),
        title: apiMessage.firstname,
        message: apiMessage.message,
        date: ISO8601DateFormatter().string(from: apiMessage.updateDate),
        isUnread: false)
    }
  }
}
```

The initials are correctly formatted:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643667648301/-Aob2hbSs.png)

Do the same to correctly format name, date and manage the visibility of  unread message:
```swift
class MessagesViewModel: ObservableObject {
  ...
  // MARK: - Helpers
  ...
  func isUnread(for message: Root.Message) -> Bool {
    message.unreadCount > 0
  }
  
  func formattedDate(for message: Root.Message ) -> String {
    let formatter = DateFormatter()
    let isToday = Calendar.current.isDateInToday(message.updateDate)
    let isYesterday = Calendar.current.isDateInYesterday(message.updateDate)

    let now = Date()
    let oneWeekAgo = Calendar.current.startOfDay(for: now).addingTimeInterval(-604_800)
    let isLessThanOneWeekAgo = (oneWeekAgo...now).contains(message.updateDate)

    if isToday {
      formatter.timeStyle = .short
    } else if isYesterday {
      return "Yesterday"
    } else if isLessThanOneWeekAgo {
      formatter.dateFormat = "EEEE"
      return formatter.string(from: message.updateDate)
    } else {
      formatter.timeStyle = .none
      formatter.dateStyle = .short
    }

    return formatter.string(from: message.updateDate)
  }
    
  func title(for message: Root.Message) -> String {
    let isFirstname = !message.firstname.isEmpty
    let isLastname = !message.lastname.isEmpty
    
    switch (isFirstname, isLastname) {
    case (true, true):
      return "\(message.firstname) \(message.lastname)"
    case (true, _):
      return message.firstname
    case (_, true):
      return message.lastname
    default:
      return message.phoneNumber.international
    }
  }
}
```

Use `isUnread(for:)`, `formattedDate(for:)`, `title(for:)` when create the `MessageUIModel`:
```swift
class MockApiMessagesViewModel: MessagesViewModel {
    ...
    messages = apiMessages.map { apiMessage in
      MessageUIModel(
        id: apiMessage.id,
        initials: initials(for: apiMessage),
        title: title(for: apiMessage),
        message: apiMessage.message,
        date: formattedDate(for: apiMessage),
        isUnread: isUnread(for: apiMessage))
    }
  ...
}

```

Now your view is displayed correctly (you may need to change the dates in the **JSON** to have consistent dates to display **today**, **yesterday**, ...)
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643667931460/nQh57LJb8.png)

You just have to wait for the backend team to finish the route to retrieve the real **JSON** via an API request.

You can continue to integrate the next UI screen. The backend will not stop you anymore.

That's it!!














