skip to Main Content

I have a view which I use throughout my app on various pages. The view shows an image and a label, in this case the city name.

import SwiftUI
import Kingfisher

struct CityView: View {
    @EnvironmentObject private var cityViewModel: CityViewModel
    
    @Binding var city: City
    
    var body: some View {
        NavigationLink(destination: CityDetailView(cityId: city.id)) {
            VStack {
                KFImage(URL(string: city.photoUrl ?? ""))
                    .resizable()
                    .background(Color.white)
                    
                Text(city.name)
            }
        }
    }
}

Now, if the API response sends a nil image, I want to call a separate endpoint to fetch the image url. This endpoint is currently set up to fetch a stock photo url and return it.

Right now, I have two different pages:

  1. A page that shows a list of random cities
  2. A profile page that shows a list of cities that the user has saved

For the page that shows a list of random cities, I have the following view model:

class CityViewModel: ObservableObject {
    let cityService: CityServiceProtocol
    
    @Published var cities: [City] = []

    func fetchPhoto(cityId: Int) async {
        do {
            let data = try await cityService.getPhoto(cityId: cityId)
            guard let photoUrl = data.data?.photoUrl else { return }
            
            // Update the image url
            if let index = self.cities.firstIndex(where: { $0.id == cityId }) {
                self.cities[index].photoUrl = photoUrl
            }
        } catch {
            print("Failed")
        }
    }
}

As you can see, I already have the fetchPhoto() function here which updates the city image url once returned.

For the profile page that shows a list of cities that the user has saved, I have the following view model:

class ProfileViewModel: ObservableObject {
    let profileService: ProfileServiceProtocol
    
    @Published var savedCities: [City] = []

    // excluded function that fetches saved cities from the api
}

Now I’ve run into a problem where, if the user navigates to their profile to view their saved cities, and a city doesn’t have an image url, it doesn’t currently fetch the image from the api.

If I want to keep this structure of using two view models, would I have to copy the fetchPhoto() function from the CityViewModel to the ProfileViewModel, or is it possible to have one function that updates both view models?

2

Answers


  1. You could have one model store object that is shared between the views. The async funcs could be in a controller struct (usually in the environment so it can be mocked for previews) and can be called from both views (in .task(id:)) and pass in the model object for it to update. Eg

    struct Controller: ControllerProtocol {
        
        func fetchPhoto(cityId: City.ID, model: Model) async {
    
        func fetchCities(model: Model) async {
    
    
    Login or Signup to reply.
  2. The logic could look something like this.

    struct City {
        var id: Int
        var photoUrl: URL
    }
    
    struct Photo {
        var data: PhotoData?
    
        struct PhotoData {
             var photoUrl: URL
        }
    }
    
    struct SomeService: Service {
        func getPhoto(cityId: Int) async throws -> Photo {
            Photo()
         }
    }
    
    protocol Service {
        func getPhoto(cityId: Int) async throws -> Photo
    }
    
     class ViewModel<T: Service>: ObservableObject {
         let service: T
         @Published var cities: [City] = []
    
         init(service: T) {
            self.service = service
         }
    
        func fetchPhoto(cityId: Int) async {
            do {
                let photo = try await service.getPhoto(cityId: cityId)
                 guard let photoUrl = photo.data?.photoUrl else { return }
    
                // Update the image url
                 if let index = self.cities.firstIndex(where: { $0.id == cityId }) {
                    self.cities[index].photoUrl = photoUrl
                }
            } catch {
                print("Failed")
            }
        }
    }
    
    @ObservedObject var vm = ViewModel(service: SomeService())
    

    Even if you had to make a second protocol to support it. If you post the two protocols there may be a way to merge them though.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search