You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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.


1. Core Concept: Decouple Services & ViewModels

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.

2. Step-by-Step Implementation with Your 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)
    }
}
3. MVVM Best Practices for Swift
  • 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 MockAuthService that returns predefined data without network calls).
  • Use Combine for Reactive Updates: For UIKit, use AnyCancellable to subscribe to ViewModel’s @Published properties 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 of class whenever 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 ObservableObject for SwiftUI: @StateObject and @Published are built for SwiftUI to automatically update the UI when ViewModel state changes.
4. Alternative: Combine-Based Service

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:18:32