Gần đây tôi đã gặp phải một thách thức thú vị liên quan đến giải mã JSON trong Swift. Giống như nhiều nhà phát triển, khi phải đối mặt với một phản ứng JSON lớn, phức tạp, bản năng đầu tiên của tôi là tiếp cận các công cụ "sửa chữa nhanh". , các bộ chuyển đổi JSON-to-Swift khác nhau, và thậm chí các mô hình AI hiện đại - sẽ xử lý một cấu trúc dữ liệu hỗn loạn, lặp đi lặp lại. Quicktype Thành thật mà nói, tôi hoàn toàn ngất xỉu. Lời bài hát: The "Flat" JSON Nightmare Vấn đề phát sinh khi bạn gặp phải một API cổ xưa hoặc một phản ứng có cấu trúc kém sử dụng các thuộc tính được đánh số “flat” thay vì chuỗi sạch. { "meals": [ { "idMeal": "52771", "strMeal": "Spicy Arrabiata Penne", "strInstructions": "Bring a large pot of water to a boil...", "strMealThumb": "https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg", "strIngredient1": "penne rigate", "strIngredient2": "olive oil", "strIngredient3": "garlic", "strIngredient4": "chopped tomatoes", "strIngredient5": "red chilli flakes", // ... this continues up to strIngredient20 "strMeasure1": "1 pound", "strMeasure2": "1/4 cup", "strMeasure3": "3 cloves", // ... this continues up to strMeasure20 } ] } Tại sao chuyển đổi trực tuyến thất bại Khi tôi kết nối điều này vào các công cụ chuyển đổi tiêu chuẩn, kết quả là một cơn ác mộng bảo trì. struct Meal: Codable { let idMeal: String let strMeal: String let strInstructions: String? let strMealThumb: String? // The repetitive property nightmare let strIngredient1: String? let strIngredient2: String? let strIngredient3: String? // ... let strIngredient20: String? let strMeasure1: String? let strMeasure2: String? let strMeasure3: String? // ... let strMeasure20: String? } Hãy thành thật, mã được tạo ra bởi các công cụ trực tuyến đó thuộc về "thùng rác" cho bất kỳ dự án nghiêm túc nào.Không chỉ là không thể mở rộng, nhưng hãy tưởng tượng cái nhìn trên khuôn mặt của nhà phát triển cao cấp của bạn trong một bài đánh giá PR khi họ thấy hơn 40 tài sản tùy chọn.Đó là một cơn ác mộng bảo trì và một cú đánh vào danh tiếng chuyên nghiệp của bạn. Tôi quyết định kiểm soát quá trình giải mã để làm cho nó sạch sẽ, Swifty, và - quan trọng nhất - Dưới đây là cách tôi cấu trúc giải pháp và tại sao nó hoạt động. production-ready Vũ khí bí mật: Tại sao chúng ta sử dụng một cấu trúc cho mã hóa Trong 99% các hướng dẫn Swift, bạn sẽ thấy được định nghĩa là một Enums là tuyệt vời khi bạn biết từng phím duy nhất tại thời điểm biên dịch. nhưng trong trường hợp của chúng tôi, chúng tôi có một JSON "phẳng" với các phím như , ... lên đến Viết một bản thảo với 40 trường hợp không chỉ là nhàm chán - đó là kỹ thuật tồi tệ. Thay vào CodingKeys enum strIngredient1 strIngredient2 20 struct 1) Vi phạm các yêu cầu của Nghị định thư Để phù hợp với Một người đàn ông phải xử lý cả hai và giá trị.Bằng cách sử dụng một struct, chúng ta có thể vượt qua Nhập file vào initializer tại runtime. CodingKey String Int bất kỳ struct CodingKeys: CodingKey { let stringValue: String var intValue: Int? init?(stringValue: String) { self.stringValue = stringValue } // This allows us to map any raw string from the JSON to our logic init(rawValue: String) { self.stringValue = rawValue } init?(intValue: Int) { return nil } // We don't need integer keys here } Mapping “Ugly” Keys to Clean Names (Tạm dịch: Bản đồ các phím “Ugly” để làm sạch tên gọi) Bạn không cần phải tuân thủ các quy ước đặt tên API bên trong ứng dụng của bạn. lưu ý cách tôi sử dụng Điều này giữ cho phần còn lại của logic giải mã có thể đọc được trong khi giữ cho các phím API "bẩn" bị cô lập bên trong cấu trúc này. static var static var name = CodingKeys(rawValue: "strMeal") static var thumb = CodingKeys(rawValue: "strMealThumb") static var instructions = CodingKeys(rawValue: "strInstructions") Sức mạnh của Dynamic Key Generation Đây là phần làm cho phương pháp này vượt trội hơn bất kỳ mã nào được tạo ra bởi AI. Chúng tôi đã tạo ra các chức năng tĩnh sử dụng Để tạo ra chìa khóa trên máy bay. string interpolation static func strIngredient(_ index: Int) -> Self { CodingKeys(rawValue: "strIngredient\(index)") } static func strMeasure(_ index: Int) -> Self { CodingKeys(rawValue: "strMeasure\(index)") } Thay vì hardcoding , , v.v., bây giờ chúng ta có một "công ty chìa khóa." Khi chúng ta cuộn qua Trong initializer của chúng tôi, chúng tôi chỉ đơn giản gọi các chức năng này. nó sạch sẽ, nó có thể tái sử dụng, và nó khó hơn đáng kể để thực hiện một typpo hơn so với việc viết 40 trường hợp riêng lẻ. strIngredient1 strIngredient2 1...20 Xây dựng một mô hình thực sự có ý nghĩa JSON gốc xử lý một thành phần và đo lường của nó như hai người lạ sống trong các ngôi nhà khác nhau. Trong ứng dụng của chúng tôi, có một cặp.Bằng cách niêm phong một cấu trúc chuyên dụng, chúng tôi sửa chữa kiến trúc dữ liệu tại nguồn: struct Ingredient: Decodable, Hashable { let id: Int let name: String let measure: String } Tại sao và the ? Hashable id Hashable id Tôi đã thêm một SwiftUI sử dụng chỉ số vòng tròn. Tại sao? Bởi vì SwiftUI hiện đại xem như và yêu cầu dữ liệu nhận dạng.Bằng cách tuân thủ Chúng tôi đảm bảo: id List ForEach Hashable SwiftUI sẽ không bị nhầm lẫn nếu hai thành phần khác nhau có cùng tên (như hai loại khác nhau của “Salt”). Hiệu suất: Các nguồn dữ liệu khó hiểu yêu thích các đối tượng có thể băm. Cách làm sạch mùi “API” Trước khi chúng ta đến với bộ khởi tạo, hãy xem cách chúng ta định nghĩa các thuộc tính chính của chúng tôi.Chúng tôi không chỉ sao chép những gì API cung cấp cho chúng tôi; chúng tôi đang dịch nó thành . Clean Swift let name: String let thumb: URL? let instructions: String let ingredients: [Ingredient] Tạm biệt str Prefix: We dropped the Hungarian notation. tên là tốt hơn so với strMeal. Các loại thích hợp: Chúng tôi giải mã hình thu nhỏ trực tiếp vào một URL?.Nếu API gửi một liên kết bị hỏng hoặc một chuỗi trống, bộ giải mã của chúng tôi xử lý nó một cách lịch sự trong giai đoạn phân tích, không muộn hơn trong dạng xem. The Smart Initializer: “Data Bouncer” của chúng tôi Thay vì mù quáng chấp nhận mọi khóa mà JSON cung cấp, tùy chỉnh của chúng tôi hoạt động như một kẻ phản đối tại một câu lạc bộ - chỉ có dữ liệu hợp lệ được nhập vào. init(from:) init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // 1. Decode simple properties using our clean aliases self.name = try container.decode(String.self, forKey: .name) self.thumb = try? container.decode(URL.self, forKey: .thumb) self.instructions = try container.decode(String.self, forKey: .instructions) // 2. The Dynamic Decoding Loop var ingredients: [Ingredient] = [] for index in 1...20 { // We use 'try?' because some keys might be null or missing if let name = try? container.decode(String.self, forKey: .strIngredient(index)), let measure = try? container.decode(String.self, forKey: .strMeasure(index)), !name.isEmpty, !measure.isEmpty { // We only save it if the name AND measure are valid and non-empty ingredients.append(Ingredient(id: index, name: name, measure: measure)) } } self.ingredients = ingredients } Kết quả cuối cùng: Clean, Swifty và UI-Ready Sau khi tất cả những gì làm việc đằng sau hậu trường, hãy nhìn vào những gì chúng tôi đã đạt được. Chúng tôi đã biến một cơn ác mộng JSON "phẳng" thành một mô hình mà là một niềm vui để sử dụng. Đây là những gì phần còn lại của ứng dụng của bạn thấy bây giờ: struct MealDetail { let name: String let instructions: String let thumb: URL? let ingredients: [Ingredient] } Đơn giản trong UI Bởi vì chúng tôi đã thực hiện việc nâng nặng trong giai đoạn giải mã - lọc các giá trị trống và nhóm các thành phần - mã SwiftUI của chúng tôi trở nên cực kỳ sạch sẽ. Lời bài hát: The Cherry On Top: Making Mocking Easy Bạn có thể đã nhận thấy một tác dụng phụ nhỏ: khi chúng ta định nghĩa một thói quen , Swift ngừng tạo ra bộ khởi tạo thành viên mặc định. Điều này có thể làm cho các bài kiểm tra đơn vị viết hoặc SwiftUI Previews hơi khó chịu. init(from: Decoder) Để khắc phục điều này và giữ cho cơ sở mã của chúng tôi "thử nghiệm thân thiện", chúng tôi có thể thêm phần mở rộng đơn giản này. Điều này cho phép chúng tôi tạo dữ liệu "Mock" cho UI của chúng tôi mà không cần tệp JSON. extension MealDetail { // Restoring the ability to create manual instances for Mocks and Tests init(name: String, thumb: URL?, instructions: String, ingredients: [Ingredient]) { self.name = name self.thumb = thumb self.instructions = instructions self.ingredients = ingredients } } Bây giờ, tạo một preview là đơn giản như: let mock = MealDetail(name: "Pasta", thumb: nil, instructions: "Cook it.", ingredients: []) Kết luận Lần sau bạn phải đối mặt với một API hỗn loạn, hãy nhớ: Các công cụ trực tuyến và AI có thể cung cấp cho bạn một giải pháp “copy-paste” nhanh chóng, nhưng chúng thường dẫn đến nợ kỹ thuật. Để thực hiện, bạn tạo mã là: don’t let the backend dictate your frontend architecture. Decodable Đọc: Tên tài sản rõ ràng, dựa trên ý định. Robust: lọc ra dữ liệu rỗng hoặc bị hỏng tại nguồn. Duy trì: Dễ dàng kiểm tra và dễ hiển thị trong UI. Happy coding, và giữ cho các mô hình của bạn sạch sẽ! Mã đầy đủ tại đây: https://github.com/PavelAndreev13/NetworkLayer