Swift iOS项目MVVM实现代码正确性验证请求
Great job getting a working MVVM setup for your iOS project! Let’s walk through how your current code aligns with MVVM principles and break down actionable improvements to make it more idiomatic and maintainable.
What You’re Doing Right
First, let’s highlight the positives—you’ve already nailed the core separation of concerns that MVVM is built on:
- Isolated network logic: Your
servicestruct keeps API call code out of the view controller, which is exactly where it should be. - Proper data modeling: You’ve defined
StudentDataandStudentdetailto decode JSON responses, which is the correct way to represent raw API data. - Basic ViewModel layer: You created a
ViewModelto transform model data for the view, which hits the core purpose of MVVM.
Areas to Refine for Better MVVM Compliance
While your code works, there are several adjustments to make it fit MVVM’s intended workflow and Swift best practices:
1. View Controller Shouldn’t Manage Data Fetching
Right now, your ViewController is directly instantiating the service, handling the completion block, and creating the ViewModel. In MVVM, the View (ViewController) should only observe changes from the ViewModel—it shouldn’t handle data fetching or ViewModel initialization directly.
2. Service Layer Needs Polish
- Naming: Swift conventions require type names to start with a capital letter (rename
servicetoStudentService). - Return complete models: Instead of splitting
studentnameandclassinto separate arrays, return the full[Studentdetail]array. This keeps data intact and avoids unnecessary manipulation in the service. - Error handling: Your current service doesn’t properly pass errors to the caller—use a
Resulttype to handle success/failure explicitly.
3. ViewModel Should Own Data Logic
Your current ViewModel is just a thin wrapper around Model with renamed properties. In MVVM, the ViewModel should:
- Handle data fetching (via the service)
- Expose observable properties so the View can react to changes automatically
- Contain any business logic (e.g., formatting data for display)
4. Redundant Model Layer
Your Model class is unnecessary—you already have Studentdetail which represents the raw student data. You can use Studentdetail directly in your ViewModel instead of creating a duplicate model.
5. Swift Naming Conventions
Follow standard Swift rules:
- Use camelCase for properties (e.g.,
NameLabel→nameLabel,CustName→customerName) - Type names use PascalCase (e.g.,
service→StudentService)
Improved Code Example
Here’s how to refactor your code to align with MVVM best practices:
StudentService (Refactored)
import Foundation struct StudentService { static let shared = StudentService() func fetchStudents(completion: @escaping (Result<[Studentdetail], Error>) -> Void) { guard let url = URL(string: DefaultData.Base.url) else { completion(.failure(NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid API URL"]))) return } URLSession.shared.dataTask(with: url) { data, _, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NSError(domain: "NoData", code: -2, userInfo: [NSLocalizedDescriptionKey: "No data received from API"]))) return } do { let studentData = try JSONDecoder().decode(StudentData.self, from: data) completion(.success(studentData.studentdetails)) } catch { completion(.failure(error)) } }.resume() } }
StudentViewModel (With Combine for Observability)
import Foundation import Combine class StudentViewModel: ObservableObject { // Observable properties the View will bind to @Published var studentName: String? @Published var studentClass: String? private var cancellables = Set<AnyCancellable>() private let service: StudentService // Inject service for testability (optional but recommended) init(service: StudentService = .shared) { self.service = service } func loadFirstStudent() { service.fetchStudents() .receive(on: DispatchQueue.main) // Ensure UI updates happen on main thread .sink(receiveCompletion: { [weak self] completion in if case .failure(let error) = completion { print("Failed to load students: \(error.localizedDescription)") } }, receiveValue: { [weak self] students in guard let firstStudent = students.first else { return } self?.studentName = firstStudent.studentname self?.studentClass = firstStudent.studentdetailClass }) .store(in: &cancellables) } }
ViewController (Simplified, Only Handles Binding)
import UIKit import Combine class ViewController: UIViewController { @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var classLabel: UILabel! private let viewModel = StudentViewModel() private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() setupBindings() viewModel.loadFirstStudent() } private func setupBindings() { // Bind ViewModel properties to UI elements automatically viewModel.$studentName .assign(to: \.text, on: nameLabel) .store(in: &cancellables) viewModel.$studentClass .assign(to: \.text, on: classLabel) .store(in: &cancellables) } }
Simplified Model & DefaultData
// Model.swift import Foundation struct StudentData: Codable { let studentdetails: [Studentdetail] } struct Studentdetail: Codable { let school, studentname, studentdetailClass, board: String enum CodingKeys: String, CodingKey { case school, studentname case studentdetailClass = "class" case board } } // DefaultData.swift import Foundation enum DefaultData { enum Base { static let url = "https://next.json-generator.com/api/json/get/4kobjnVE8" } }
Key Takeaways from the Refactor
- Clean separation of concerns: The View only handles UI updates, ViewModel manages data and logic, Service handles network calls, Model defines data structures.
- Automatic UI updates: Using Combine’s
@Publishedlets the View react to ViewModel changes without manual completion block handling. - Testability: Injecting the service into the ViewModel makes it easy to mock network calls for unit testing.
内容的提问来源于stack exchange,提问作者Shanmugarajeshwaran R

