Transitioning from MVVM to MV: A Journey of Simplifying App Architecture

Michael Kolkov
Mobile App Circular
3 min readAug 22, 2023

--

Greetings! This is the NYCoworker iOS team. We’re excited to let you know that we’ve recently launched our public beta program. We’re also eager to take you behind the scenes and share the technical challenges we encountered before the launch.

All iOS developers are familiar with the popular approach of building iOS apps using SwiftUI in the MVVM architecture. However, as apps get bigger and new business logic appears, the drawbacks of this architecture become more apparent. This is why transitioning to the MV architecture becomes more relevant for teams.

Our job was to work with a list of locations that we got from a database and used throughout the app. We wanted to find the best way to keep this data and make it simple to get or change whenever we wanted.

We had a service class that handled all the backend tasks related to the location’s model. To make this class available everywhere in the app, we used a singleton approach.

class LocationService {
private var db = Firestore.firestore()

static let shared = LocationService()

///Fetching all locations from Firebase
///- returns: set of locations
func fetchLocations(completion: @escaping (Result<[Location], Error>) -> Void) async {
do {
var query: Query!
query = db.collection(Endpoints.locations.rawValue).order(by: "locationPriority", descending: true)
let docs = try await query.getDocuments()
let fetchedLocations = docs.documents.compactMap { doc -> Location? in
try? doc.data(as: Location.self)
}
completion(.success(fetchedLocations))
}
catch {
completion(.failure(error))
}
}

To ensure the Model-View pattern, we designed a class called LocationStore, which also conforms to the ObservableObject protocol. This class allows us to perform operations on the locations array at the view level.

@MainActor
class LocationStore: ObservableObject {

@Published var locations: [Location] = []
@Published var reviews: [Review] = []
@Published var favoriteLocations: [Location] = []

var hotels: [Location] {
return locations.filter({ $0.locationType == .hotel})
}

var libraries: [Location] {
return locations.filter({ $0.locationType == .library})
}

var publicSpaces: [Location] {
return locations.filter({ $0.locationType == .publicSpace})
}

var cafes: [Location] {
return locations.filter({ $0.locationType == .cafe})
}

func fetchLocations(completion: @escaping(Result<Void, Error>) -> Void) async {
await LocationService.shared.fetchLocations(completion: { [weak self] result in
switch result {
case .success(let data):
DispatchQueue.main.async {
self?.locations = data
completion(.success(()))
}
case .failure(let error):
completion(.failure(error))
}
})
}

As you may notice, we created a function within our LocationStore to populate the locations array with data. This allowed us to directly pass the success completion result to the view.

Remember to utilize [weak self] when dealing with asynchronous functions. This practice will safeguard you from encountering strong reference cycles.

After operations above, we will switch to our sample view in which we’ll designate LocationStore as a StateObject. We’ll also invoke the function using the .task modifier.

struct LocationsView: View {
@StateObject private var locationStore = LocationStore()
var body: some View {
NavigationView {
locationsList()
.task {
await fetchAllLocations()
}
}
}
}
  private func fetchAllLocations() async {
await locationStore.fetchLocations(completion: { result in
switch result {
case .success:
print("Locations are loaded \(locationStore.locations.count)")
case .failure(let error):
print(error.localizedDescription)
}
})
}

You are all set! Now you can create a custom error handler and directly display errors within the View itself, without needing to invoke special functions within the ViewModel. Additionally, you can include extra logic for location-related data, such as reviews for a specific place.

Conclusion

We streamlined the app’s structure by eliminating the unnecessary layer between the network and the model. This will help us avoid code complexity down the road. Our plan is to extend this approach to other modules of the app, like notifications and reviews, in the future. Stay tuned for the upcoming chapters of our journey in developing NYCoworker for iOS!

--

--