如何使用MVVM实现登录?iOS Swift MVVM代码实践指导咨询
Hey there! Let's break down how to pass data from your ServiceCall class to a ViewModel in MVVM, along with some Swift-specific best practices to keep your code clean and maintainable.
First, remember the MVVM golden rule: separate concerns. Your ServiceCall should only handle network/data operations, while the ViewModel manages business logic, state, and acts as the middleman between the Service and View. We’ll use either closures or Combine (Apple’s reactive framework) to pass data between them—both are solid choices for Swift.
Login Model First, let’s polish your Login model to support JSON parsing (critical for network work):
// Optimized Login Model with Codable for easy JSON parsing struct Login: Codable { let appVersion: String? let deviceID: String? let deviceOS: String? let password: String? // Add other required fields here... }
2.1 Build a Reusable Auth Service
Create a dedicated service class for authentication—this keeps network logic isolated:
// AuthService: Handles all login-related network operations class AuthService { // Define a completion type to standardize success/failure responses typealias LoginCompletion = (Result<Login, Error>) -> Void func performLogin(with credentials: Login, completion: @escaping LoginCompletion) { // Replace this with your actual URLSession/Alamofire code DispatchQueue.global().asyncAfter(deadline: .now() + 1) { // Simulate successful response (use server-parsed data in production) completion(.success(credentials)) // Example failure case: // completion(.failure(NetworkError.invalidCredentials)) } } } // Custom error type for clearer error handling enum NetworkError: Error, LocalizedError { case invalidResponse case decodingFailed case invalidCredentials var errorDescription: String? { switch self { case .invalidResponse: return "Server returned an invalid response" case .decodingFailed: return "Failed to parse server data" case .invalidCredentials: return "Wrong username or password" } } }
2.2 Build the Login ViewModel
The ViewModel will hold the service reference, handle data from the service, and expose observable state for the View:
import Combine class LoginViewModel: ObservableObject { // Observable properties for the View to react to @Published var loginResponse: Login? @Published var errorMessage: String? @Published var isLoading = false // Hold the service (use dependency injection for testability) private let authService: AuthService private var cancellables = Set<AnyCancellable>() // Inject service (default to real service, pass mock for testing) init(authService: AuthService = AuthService()) { self.authService = authService } // Public method for the View to trigger login func login(with credentials: Login) { isLoading = true authService.performLogin(with: credentials) { [weak self] result in DispatchQueue.main.async { // Always update UI on main thread self?.isLoading = false switch result { case .success(let loginData): self?.loginResponse = loginData self?.errorMessage = nil // Add business logic here: e.g., save token to Keychain case .failure(let error): self?.errorMessage = error.localizedDescription self?.loginResponse = nil } } } } }
2.3 Example SwiftUI View
The View only handles UI and user input, delegating all logic to the ViewModel:
struct LoginView: View { @StateObject private var viewModel = LoginViewModel() @State private var appVersion = "1.0.0" @State private var deviceID = "abc-123-xyz" @State private var deviceOS = "iOS 17" @State private var password = "secure123" var body: some View { VStack(spacing: 16) { TextField("App Version", text: $appVersion) .textFieldStyle(.roundedBorder) TextField("Device ID", text: $deviceID) .textFieldStyle(.roundedBorder) TextField("Device OS", text: $deviceOS) .textFieldStyle(.roundedBorder) SecureField("Password", text: $password) .textFieldStyle(.roundedBorder) Button(action: triggerLogin) { if viewModel.isLoading { ProgressView() } else { Text("Login") .font(.headline) .foregroundColor(.white) .padding() .background(Color.blue) .cornerRadius(8) } } // Show success/error messages if let response = viewModel.loginResponse { Text("Login Success! App Version: \(response.appVersion ?? "N/A")") .foregroundColor(.green) } if let error = viewModel.errorMessage { Text(error) .foregroundColor(.red) .multilineTextAlignment(.center) } } .padding() } private func triggerLogin() { let credentials = Login( appVersion: appVersion, deviceID: deviceID, deviceOS: deviceOS, password: password ) viewModel.login(with: credentials) } }
- Dependency Injection: Inject the service into the ViewModel instead of hardcoding it. This lets you swap in a mock service for unit testing (e.g., a
MockAuthServicethat returns predefined data without network calls). - Use Combine for Reactive Updates: For UIKit, use
AnyCancellableto subscribe to ViewModel’s@Publishedproperties and update UI elements (like labels or activity indicators) automatically. - Keep Models Light: Models should only store data—no business logic. Use
struct(value types) instead ofclasswhenever possible for better performance and immutability. - Avoid Retain Cycles: Always use
[weak self]in closures passed to services to prevent memory leaks. - Centralize Error Handling: Custom error types (like
NetworkError) make it easier to handle specific failure cases in the ViewModel and show user-friendly messages. - Use
ObservableObjectfor SwiftUI:@StateObjectand@Publishedare built for SwiftUI to automatically update the UI when ViewModel state changes.
If you prefer reactive programming over closures, rewrite the service to return a Publisher:
class AuthService { func performLogin(with credentials: Login) -> AnyPublisher<Login, Error> { // Replace with real URLSession.dataTaskPublisher in production return Future { promise in DispatchQueue.global().asyncAfter(deadline: .now() + 1) { promise(.success(credentials)) } } .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } } // Update ViewModel's login method: func login(with credentials: Login) { isLoading = true authService.performLogin(with: credentials) .sink(receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.errorMessage = error.localizedDescription } }, receiveValue: { [weak self] loginData in self?.loginResponse = loginData }) .store(in: &cancellables) }
内容的提问来源于stack exchange,提问作者Erent

