MVVM_example

MVVM High Level example

Model

struct Meal: Decodable {
    let strMeal: String
    let strMealThumb: String
    let idMeal: String
}

View

This could be part of the ViewController + View

struct CustomListView: View {
	@StateObject var model: CustomViewModel = CustomViewModel()
	var body: some View {
		InternalView(recipe: model.data)
	}
}

Extracted View

struct InternalView: View {
	var recipe: [RecipeData]
}

ViewModel

You can inject any implementation as long as it conforms to protocol.
Great for dependency injection and adding a facade pattern to the init. Now the view controller or View won't have to change every time we change our internal API dependencies to get the concrete type of data. We can always be sure that the interface protocol method signature will stay the same irrespective of internal implementation changes. Abstraction at its best and easily able to loosely coupled the ViewModel , View and Service + Network implementation.
This could be also testable with Mocking CustomViewServiceable.

@MainActor
class CustomViewModel: ObservableObject {
	@Published var data: [MealViewModel] = []
	let service: CustomViewServiceable

	
	init(serviceImplementation: CustomViewServiceable) {
		service = serviceImplementation
	}
	func fetchRecipes() async {
		data = service.getData(serviceType: .privateAPI)
    }
}

read more about observable

Modular - Loosely Coupled

Consumable ViewModel

struct MealViewModel: Identifiable {
    fileprivate let meal: Meal
    init(meal: Meal) {
        self.meal = meal
    }
    var id: String { meal.idMeal }
    var title: String { meal.strMeal }
}

Enum

enum CustomViewServiceType {
	case otherAPI
	case privateAPI
}

Protocol

protocol CustomViewServiceable {
	func getData(serviceType: CustomViewServiceType) -> MealViewModel
}

CustomList Service

The actual magic of keeping it loosely coupled. We can route any input via the protocol conformance getData method and internally can switch endpoints or do anything to create those ViewModel objects and send it back.


class CustomViewService: CustomViewServiceable {
	func getData(serviceType: CustomViewServiceType)  -> MealViewModel {
		 var mealViewModel
		 switch serviceType {
		 case .otherAPI: 
			 let response = getDataOtherAPI()
			 // Parse that response into `MealViewModel` type
		 case .privateAPI: 
			 let response = getDataPrivateAPI()
			 // Parse that response into `MealViewModel` type
		 }
		let mealViewModel = parseNetworkResponseToViewModel(serviceType)
		return mealViewModel
	}

	private func getDataPrivateAPI() -> ResponseType {
		do { 
			 let recipes = try await AsyncNetwork
			 .shared
			 .fetchData(url: Constants.API.mealURL, type: MealModel.self
			 
			 return recipes.meals.map { MealViewModel(meal: $0) }
        } catch { print(error) }
	}
	
	private func getDataOtherAPI() -> ResponseType {
		// Make network request
	}

	private func parseNetworkResponseToViewModel(type: CustomViewServiceType) -> MealViewModel {
		// do conditional ViewModel creation based on Enum and associated response types.
		
	}
}

Generic Network Async / Await

public func fetchData<T: Decodable>(url: String, id: Int? = nil, type: T.Type) async throws -> T {
        guard let url = URL(string: url) else {
            throw NetworkError.invalidURL
        }
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let response = response as? HTTPURLResponse,
              (200..<300).contains(httpResponse.statusCode) else {
            throw NetworkError.requestBad
        }
        guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
            throw NetworkError.requestBadDecoding
        }
        return decodedData
    }

Modular Views

Passing StateObject to Extracted View
Utilize @ObservedObject for passing ViewModel ref in extracted views.

struct ContentView: View {
    @StateObject var vm = ViewModel()
	var body {
		ExtractedView(vm: vm)
	}
}

struct ExtractedView: View {
	@ObservedObject var vm: ImageNetwork

	// do something
	VStack { 
		Text(vm.fetchText())
	}
}

Error Enums

enum NetworkError: Error {
    case requestBad
    case requestDataFailedNetworkError
    case invalidURL
    case requestBadDecoding
    case unknown
}

References

Passing Data between Views in SwiftUI
tutorial-passing-data-between-views-in-swiftui-using-state-and-binding

swift dev journal | passing-data-to-swiftui-views

Pass @StateObject data in views.

HWS | sharing-swiftui-state-with-stateobject