Swift ObservableObject implementation with generic inference

I must admit my knowledge of swift is limited, and I cannot wrap my head around this problem.

I've defined this protocol, so I can use different auth providers in my app.

protocol AuthRepository {
    associatedtype AuthData
    associatedtype AuthResponseData
    associatedtype RegistrationData
    associatedtype RegistrationResponseData
    
    func login(with data: AuthData) async throws -> AuthResponseData?
    
    func register(with data: RegistrationData) async throws -> RegistrationResponseData?
}

and an implementation for my server

struct MyServerAuthData {
    let email: String
    let password: String
}

struct  MyServerAuthResponseData {
    let token: String
}

struct  MyServerRegistrationData {
    let email: String
    let password: String
    let name: String
}

actor AuthRepositoryImpl: AuthRepository {
    func login(with data: MyServerAuthData) async throws ->  MyServerAuthResponseData? {
        ...
    }
    
    func register(with data:  MyServerRegistrationData) async throws -> Void? {
        ...
    }
}

To use across the app, I've created this ViewModel

@MainActor
final class AuthViewModel<T: AuthRepository>: ObservableObject {
    private let repository: T

    init(repository: T) {
        self.repository = repository
    }

    func login(data: T.AuthData) async throws -> T.AuthResponseData? {
        try await repository.login(with: data)
    }

    func register(with data: T.RegistrationData) async throws {
        try await repository.register(with: data)
    }
}

defined in the app as

@main
struct MyApp: App {
    @StateObject var authViewModel = AuthViewModel(repository: AuthRepositoryImpl())

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(self.authViewModel)
        }
    }
}

and consumed as

@EnvironmentObject private var authViewModel: AuthViewModel<AuthRepositoryImpl>

But with this code, the whole concept of having a generic implementation for the auth repository is useless, because changing the AuthRepostory will need to search and replace AuthViewModel<AuthRepositoryImpl> across all the app.

I've experienced this directly creating a MockAuthImpl to use with #Preview, and the preview crashed because it defines AuthViewModel(repository: MockAuthImpl()) but the view expects AuthViewModel<AuthRepositoryImpl>.

There is a better way to do that?

@Artecoop The way you would structure this is to make AuthRepository a class instead of a protocol, and have subclasses implement the method.

For example:

class AuthRepository {
    func login(with data: MyServerAuthData) async throws -> MyServerAuthResponseData? {
        fatalError("must be implemented by subclass")
    }
    
    func register(with data: MyServerRegistrationData) async throws -> MyServerRegistrationResponseData? {
        fatalError("must be implemented by subclass")
    }
}


class AuthRepositoryImpl: AuthRepository {
    override func login(with data: MyServerAuthData) async throws -> MyServerAuthResponseData? {
        return MyServerAuthResponseData(token: "token 12345")
    }
    
    override func register(with data: MyServerRegistrationData) async throws -> MyServerRegistrationResponseData? {
        return MyServerRegistrationResponseData(userId: "12345")
    }
}

@Observable
final class AuthViewModel {
    private let repository: AuthRepository

    init(repository: AuthRepository) {
        self.repository = repository
    }

    func login(data: MyServerAuthData) async throws -> MyServerAuthResponseData? {
        try await repository.login(with: data)
    }

    func register(with data: MyServerRegistrationData) async throws -> MyServerRegistrationResponseData? {
        try await repository.register(with: data)
    }
}

class MockAuthRepositoryImpl: AuthRepository {
    override func login(with data: MyServerAuthData) async throws -> MyServerAuthResponseData? {
        return MyServerAuthResponseData(token: "987654")
    }
    
    override func register(with data: MyServerRegistrationData) async throws -> MyServerRegistrationResponseData? {
        return MyServerRegistrationResponseData(userId: "987654")
    }
}

struct ContentView: View {
    @State var authViewModel = AuthViewModel(repository: AuthRepositoryImpl())

    var body: some View {
        NavigationStack {
            NavigationLink("Detail View") {
                DetailView()
            }
        }
        .environment(authViewModel)
    }
}

struct DetailView: View {
    @Environment(AuthViewModel.self) private var authViewModel

    @State private var token = ""
    var body: some View {
        Text("Token: \(token)")
            .task {
                if let token = try? await authViewModel.login(data: .init(email: "", password: "")) {
                    self.token = token.token
                }
            }
           
    }
}

#Preview {
    DetailView()
        .environment(AuthViewModel(repository: MockAuthRepositoryImpl()))
}
Swift ObservableObject implementation with generic inference
 
 
Q