feat: Introduce comprehensive API debugging tools, enhance decoding error handling, and simplify API model definitions.
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
//
|
||||
// APIDebugView.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Comprehensive API debugging view
|
||||
// View debug API toàn diện
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - API Debug View
|
||||
// View Debug API
|
||||
|
||||
struct APIDebugView: View {
|
||||
@State private var endpoint: String = "/users/me"
|
||||
@State private var method: HTTPMethod = .get
|
||||
@State private var requestBody: String = ""
|
||||
@State private var customHeaders: String = ""
|
||||
@State private var useAccessToken: Bool = true
|
||||
|
||||
@State private var responseStatus: String = "Chưa gửi request"
|
||||
@State private var responseJSON: String = ""
|
||||
@State private var decodingResult: String = ""
|
||||
@State private var isLoading: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
// Endpoint Section
|
||||
Section(header: Text("Request Configuration")) {
|
||||
TextField("Endpoint (vd: /users/me)", text: $endpoint)
|
||||
.autocapitalization(.none)
|
||||
|
||||
Picker("Method", selection: $method) {
|
||||
Text("GET").tag(HTTPMethod.get)
|
||||
Text("POST").tag(HTTPMethod.post)
|
||||
Text("PUT").tag(HTTPMethod.put)
|
||||
Text("DELETE").tag(HTTPMethod.delete)
|
||||
}
|
||||
|
||||
Toggle("Use Access Token", isOn: $useAccessToken)
|
||||
}
|
||||
|
||||
// Request Body Section
|
||||
if method == .post || method == .put {
|
||||
Section(header: Text("Request Body (JSON)")) {
|
||||
TextEditor(text: $requestBody)
|
||||
.frame(height: 150)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
|
||||
Button("Load Sample Body") {
|
||||
requestBody = sampleRequestBody
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Headers Section
|
||||
Section(header: Text("Custom Headers (optional)")) {
|
||||
TextEditor(text: $customHeaders)
|
||||
.frame(height: 100)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
|
||||
Text("Format: Key: Value (mỗi header trên một dòng)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// Action Button
|
||||
Section {
|
||||
Button(action: {
|
||||
Task {
|
||||
await sendRequest()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
Text(isLoading ? "Đang gửi..." : "Gửi Request")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.disabled(isLoading || endpoint.isEmpty)
|
||||
|
||||
Button("Clear Results") {
|
||||
clearResults()
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
// Response Status Section
|
||||
Section(header: Text("Response Status")) {
|
||||
Text(responseStatus)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.foregroundColor(responseStatusColor)
|
||||
}
|
||||
|
||||
// Raw JSON Section
|
||||
if !responseJSON.isEmpty {
|
||||
Section(header: HStack {
|
||||
Text("Raw JSON Response")
|
||||
Spacer()
|
||||
Button("Copy") {
|
||||
UIPasteboard.general.string = responseJSON
|
||||
}
|
||||
.font(.caption)
|
||||
}) {
|
||||
ScrollView {
|
||||
Text(responseJSON)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
// Decoding Result Section
|
||||
if !decodingResult.isEmpty {
|
||||
Section(header: Text("Decoding Analysis")) {
|
||||
ScrollView {
|
||||
Text(decodingResult)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
// Quick Actions
|
||||
Section(header: Text("Quick Actions")) {
|
||||
Button("Test /users/me") {
|
||||
endpoint = "/users/me"
|
||||
method = .get
|
||||
useAccessToken = true
|
||||
}
|
||||
|
||||
Button("Test /auth/register") {
|
||||
endpoint = "/auth/register"
|
||||
method = .post
|
||||
useAccessToken = false
|
||||
requestBody = """
|
||||
{
|
||||
"firstName": "Test",
|
||||
"lastName": "User",
|
||||
"email": "test@example.com",
|
||||
"password": "Test123!"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// Debugging Tips
|
||||
Section(header: Text("Debugging Tips")) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
tipText("1. Kiểm tra Raw JSON để xem server trả về gì")
|
||||
tipText("2. Xem Decoding Analysis để biết field nào bị sai")
|
||||
tipText("3. So sánh tên field trong JSON với model")
|
||||
tipText("4. Kiểm tra có wrapper {success, data} không")
|
||||
tipText("5. Verify snake_case/camelCase conversion")
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.navigationTitle("API Debugger")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var responseStatusColor: Color {
|
||||
if responseStatus.contains("✅") || responseStatus.contains("200") {
|
||||
return .green
|
||||
} else if responseStatus.contains("❌") || responseStatus.contains("Error") {
|
||||
return .red
|
||||
} else {
|
||||
return .primary
|
||||
}
|
||||
}
|
||||
|
||||
private func tipText(_ text: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
Text("💡")
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
private var sampleRequestBody: String {
|
||||
"""
|
||||
{
|
||||
"name": "Sample Value",
|
||||
"email": "test@example.com"
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private func clearResults() {
|
||||
responseStatus = "Đã xóa kết quả"
|
||||
responseJSON = ""
|
||||
decodingResult = ""
|
||||
}
|
||||
|
||||
// MARK: - Network Request
|
||||
|
||||
private func sendRequest() async {
|
||||
isLoading = true
|
||||
responseStatus = "🔄 Đang gửi request..."
|
||||
responseJSON = ""
|
||||
decodingResult = ""
|
||||
|
||||
do {
|
||||
// Build URL
|
||||
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + endpoint) else {
|
||||
responseStatus = "❌ Invalid URL"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Create request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.timeoutInterval = APIConfig.timeout
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
// Add access token if needed
|
||||
if useAccessToken, let token = AuthManager.shared.accessToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
// Parse and add custom headers
|
||||
let headerLines = customHeaders.components(separatedBy: "\n")
|
||||
for line in headerLines {
|
||||
let parts = line.components(separatedBy: ":")
|
||||
if parts.count == 2 {
|
||||
let key = parts[0].trimmingCharacters(in: .whitespaces)
|
||||
let value = parts[1].trimmingCharacters(in: .whitespaces)
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
|
||||
// Add request body
|
||||
if !requestBody.isEmpty, let bodyData = requestBody.data(using: .utf8) {
|
||||
request.httpBody = bodyData
|
||||
}
|
||||
|
||||
// Send request
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
responseStatus = "❌ Invalid response"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Store raw JSON
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
responseJSON = jsonString.prettyPrintedJSON ?? jsonString
|
||||
}
|
||||
|
||||
// Update status
|
||||
let statusEmoji = httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 ? "✅" : "❌"
|
||||
responseStatus = """
|
||||
\(statusEmoji) Status Code: \(httpResponse.statusCode)
|
||||
URL: \(url.absoluteString)
|
||||
Method: \(method.rawValue)
|
||||
"""
|
||||
|
||||
// Analyze decoding
|
||||
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
|
||||
analyzeResponse(data)
|
||||
}
|
||||
|
||||
} catch {
|
||||
responseStatus = "❌ Error: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func analyzeResponse(_ data: Data) {
|
||||
var analysis = "🔍 Response Analysis:\n\n"
|
||||
|
||||
// Try to parse as JSON
|
||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data) else {
|
||||
analysis += "❌ Cannot parse as JSON"
|
||||
decodingResult = analysis
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a dictionary
|
||||
if let dict = jsonObject as? [String: Any] {
|
||||
analysis += "✅ Response is a Dictionary\n"
|
||||
analysis += "📊 Keys: \(dict.keys.count)\n\n"
|
||||
|
||||
// List all keys with types
|
||||
analysis += "🔑 Available Fields:\n"
|
||||
for (key, value) in dict.sorted(by: { $0.key < $1.key }) {
|
||||
let valueType = type(of: value)
|
||||
let preview = previewValue(value)
|
||||
analysis += " • \(key): \(valueType)\(preview)\n"
|
||||
}
|
||||
|
||||
// Check for wrapper format
|
||||
let hasSuccess = dict["success"] != nil
|
||||
let hasData = dict["data"] != nil
|
||||
let hasError = dict["error"] != nil
|
||||
|
||||
analysis += "\n📦 Wrapper Detection:\n"
|
||||
analysis += " • Has 'success': \(hasSuccess ? "✅" : "❌")\n"
|
||||
analysis += " • Has 'data': \(hasData ? "✅" : "❌")\n"
|
||||
analysis += " • Has 'error': \(hasError ? "✅" : "❌")\n"
|
||||
|
||||
if hasData, let dataValue = dict["data"] {
|
||||
analysis += "\n📊 Data Content:\n"
|
||||
if let dataDict = dataValue as? [String: Any] {
|
||||
analysis += " Type: Dictionary\n"
|
||||
analysis += " Keys: \(dataDict.keys.joined(separator: ", "))\n"
|
||||
} else if let dataArray = dataValue as? [Any] {
|
||||
analysis += " Type: Array\n"
|
||||
analysis += " Count: \(dataArray.count)\n"
|
||||
} else {
|
||||
analysis += " Type: \(type(of: dataValue))\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Suggestions for User model
|
||||
if endpoint.contains("/users") {
|
||||
analysis += "\n💡 User Model Mapping:\n"
|
||||
let userFields = ["id", "email", "name", "firstName", "lastName",
|
||||
"avatarUrl", "phoneNumber", "isEmailVerified",
|
||||
"emailConfirmed", "createdAt", "updatedAt", "roles"]
|
||||
|
||||
for field in userFields {
|
||||
if dict[field] != nil {
|
||||
analysis += " ✅ \(field)\n"
|
||||
} else {
|
||||
// Try snake_case version
|
||||
let snakeCase = field.toSnakeCase()
|
||||
if dict[snakeCase] != nil {
|
||||
analysis += " ⚠️ \(field) → found as '\(snakeCase)'\n"
|
||||
} else {
|
||||
analysis += " ❌ \(field) → not found\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if let array = jsonObject as? [[String: Any]] {
|
||||
analysis += "✅ Response is an Array\n"
|
||||
analysis += "📊 Count: \(array.count)\n"
|
||||
if let firstItem = array.first {
|
||||
analysis += "🔑 First Item Keys:\n"
|
||||
for key in firstItem.keys.sorted() {
|
||||
analysis += " • \(key)\n"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
analysis += "⚠️ Unknown response format\n"
|
||||
}
|
||||
|
||||
decodingResult = analysis
|
||||
}
|
||||
|
||||
private func previewValue(_ value: Any) -> String {
|
||||
switch value {
|
||||
case let string as String:
|
||||
let preview = string.prefix(30)
|
||||
return preview.count < string.count ? " = \"\(preview)...\"" : " = \"\(preview)\""
|
||||
case let number as NSNumber:
|
||||
return " = \(number)"
|
||||
case let bool as Bool:
|
||||
return " = \(bool)"
|
||||
case is NSNull:
|
||||
return " = null"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
APIDebugView()
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// APIResponseDebugger.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Helper for debugging API response issues
|
||||
// Helper để debug các vấn đề API response
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - API Response Debugger
|
||||
// Debugger cho API Response
|
||||
|
||||
/// Helper class for debugging API responses
|
||||
/// Class helper để debug API responses
|
||||
struct APIResponseDebugger {
|
||||
|
||||
/// Print detailed information about raw JSON data
|
||||
/// In thông tin chi tiết về raw JSON data
|
||||
/// - Parameters:
|
||||
/// - data: Raw JSON data / Dữ liệu JSON thô
|
||||
/// - expectedType: Expected type name / Tên type mong đợi
|
||||
static func debugJSONData(_ data: Data, expectedType: String) {
|
||||
#if DEBUG
|
||||
print("\n" + String(repeating: "=", count: 60))
|
||||
print("🔍 API RESPONSE DEBUG")
|
||||
print(String(repeating: "=", count: 60))
|
||||
|
||||
// Print raw JSON
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
print("\n📄 Raw JSON Response:")
|
||||
print(jsonString.prettyPrintedJSON ?? jsonString)
|
||||
}
|
||||
|
||||
// Try to parse as generic JSON object
|
||||
if let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||
let prettyJSON = String(data: jsonData, encoding: .utf8) {
|
||||
|
||||
print("\n🎨 Pretty Printed JSON:")
|
||||
print(prettyJSON)
|
||||
|
||||
// Analyze structure
|
||||
if let dict = jsonObject as? [String: Any] {
|
||||
print("\n🔑 Available Keys:")
|
||||
for (key, value) in dict {
|
||||
let valueType = type(of: value)
|
||||
print(" - \(key): \(valueType)")
|
||||
}
|
||||
|
||||
// Check for wrapper format
|
||||
let hasSuccess = dict["success"] != nil
|
||||
let hasData = dict["data"] != nil
|
||||
let hasError = dict["error"] != nil
|
||||
|
||||
if hasSuccess || hasData {
|
||||
print("\n📦 Detected Wrapper Format:")
|
||||
print(" - Has 'success': \(hasSuccess)")
|
||||
print(" - Has 'data': \(hasData)")
|
||||
print(" - Has 'error': \(hasError)")
|
||||
|
||||
if hasData, let dataValue = dict["data"] {
|
||||
print("\n📊 Data Type: \(type(of: dataValue))")
|
||||
if let dataDict = dataValue as? [String: Any] {
|
||||
print(" Data Keys: \(dataDict.keys.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("\n⚠️ No wrapper detected - direct object response")
|
||||
}
|
||||
} else if let array = jsonObject as? [[String: Any]] {
|
||||
print("\n📊 Array Response with \(array.count) items")
|
||||
if let firstItem = array.first {
|
||||
print(" First item keys: \(firstItem.keys.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("\n🎯 Expected Type: \(expectedType)")
|
||||
print(String(repeating: "=", count: 60) + "\n")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Compare expected model fields with actual JSON keys
|
||||
/// So sánh các field mong đợi của model với key thực tế trong JSON
|
||||
/// - Parameters:
|
||||
/// - data: Raw JSON data / Dữ liệu JSON thô
|
||||
/// - expectedKeys: Expected keys / Các key mong đợi
|
||||
static func compareFields(data: Data, expectedKeys: [String]) {
|
||||
#if DEBUG
|
||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let dict = jsonObject as? [String: Any] else {
|
||||
print("❌ Cannot parse JSON as dictionary")
|
||||
return
|
||||
}
|
||||
|
||||
print("\n🔍 Field Comparison:")
|
||||
print("Expected vs Actual\n")
|
||||
|
||||
let actualKeys = Set(dict.keys)
|
||||
let expectedSet = Set(expectedKeys)
|
||||
|
||||
// Missing keys
|
||||
let missing = expectedSet.subtracting(actualKeys)
|
||||
if !missing.isEmpty {
|
||||
print("❌ Missing Keys (in model but not in JSON):")
|
||||
missing.forEach { print(" - \($0)") }
|
||||
}
|
||||
|
||||
// Extra keys
|
||||
let extra = actualKeys.subtracting(expectedSet)
|
||||
if !extra.isEmpty {
|
||||
print("\n➕ Extra Keys (in JSON but not in model):")
|
||||
extra.forEach { print(" - \($0)") }
|
||||
}
|
||||
|
||||
// Matching keys
|
||||
let matching = actualKeys.intersection(expectedSet)
|
||||
if !matching.isEmpty {
|
||||
print("\n✅ Matching Keys:")
|
||||
matching.forEach { print(" - \($0)") }
|
||||
}
|
||||
|
||||
// Suggest CodingKeys mapping
|
||||
if !extra.isEmpty || !missing.isEmpty {
|
||||
print("\n💡 Suggestion: Add custom CodingKeys mapping:")
|
||||
print("enum CodingKeys: String, CodingKey {")
|
||||
for key in expectedKeys {
|
||||
if missing.contains(key) {
|
||||
// Try to find similar key in actual
|
||||
let snakeCase = key.toSnakeCase()
|
||||
if actualKeys.contains(snakeCase) {
|
||||
print(" case \(key) = \"\(snakeCase)\"")
|
||||
} else {
|
||||
print(" case \(key) // ⚠️ Not found in response")
|
||||
}
|
||||
} else {
|
||||
print(" case \(key)")
|
||||
}
|
||||
}
|
||||
print("}")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Generate Swift model code from JSON data
|
||||
/// Tạo code Swift model từ JSON data
|
||||
/// - Parameters:
|
||||
/// - data: Raw JSON data / Dữ liệu JSON thô
|
||||
/// - modelName: Name for the model / Tên cho model
|
||||
static func generateModelCode(from data: Data, modelName: String = "GeneratedModel") {
|
||||
#if DEBUG
|
||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let dict = jsonObject as? [String: Any] else {
|
||||
print("❌ Cannot parse JSON as dictionary")
|
||||
return
|
||||
}
|
||||
|
||||
print("\n🏗️ Generated Model Code:\n")
|
||||
print("struct \(modelName): Codable {")
|
||||
|
||||
for (key, value) in dict.sorted(by: { $0.key < $1.key }) {
|
||||
let camelKey = key.toCamelCase()
|
||||
let swiftType = swiftType(for: value)
|
||||
let isOptional = value is NSNull
|
||||
|
||||
print(" let \(camelKey): \(swiftType)\(isOptional ? "?" : "")")
|
||||
|
||||
if key != camelKey {
|
||||
print(" // API key: \"\(key)\"")
|
||||
}
|
||||
}
|
||||
|
||||
print("}")
|
||||
print("\n")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Determine Swift type from JSON value
|
||||
/// Xác định Swift type từ giá trị JSON
|
||||
private static func swiftType(for value: Any) -> String {
|
||||
switch value {
|
||||
case is String:
|
||||
return "String"
|
||||
case is Int:
|
||||
return "Int"
|
||||
case is Double, is Float:
|
||||
return "Double"
|
||||
case is Bool:
|
||||
return "Bool"
|
||||
case is [Any]:
|
||||
return "[Any]" // Could be more specific
|
||||
case is [String: Any]:
|
||||
return "[String: Any]" // Could be more specific
|
||||
case is NSNull:
|
||||
return "String" // Default to String for null values
|
||||
default:
|
||||
return "Any"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Extensions for Case Conversion
|
||||
// Extensions String cho chuyển đổi case
|
||||
|
||||
extension String {
|
||||
/// Convert string to snake_case
|
||||
/// Chuyển string sang snake_case
|
||||
func toSnakeCase() -> String {
|
||||
let pattern = "([a-z0-9])([A-Z])"
|
||||
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(location: 0, length: count)
|
||||
let modString = regex?.stringByReplacingMatches(
|
||||
in: self,
|
||||
options: [],
|
||||
range: range,
|
||||
withTemplate: "$1_$2"
|
||||
)
|
||||
|
||||
return (modString ?? self).lowercased()
|
||||
}
|
||||
|
||||
/// Convert string to camelCase
|
||||
/// Chuyển string sang camelCase
|
||||
func toCamelCase() -> String {
|
||||
let components = self.components(separatedBy: "_")
|
||||
guard components.count > 1 else { return self }
|
||||
|
||||
let first = components[0].lowercased()
|
||||
let rest = components.dropFirst().map { $0.capitalized }.joined()
|
||||
|
||||
return first + rest
|
||||
}
|
||||
|
||||
/// Pretty print JSON string
|
||||
/// In đẹp JSON string
|
||||
var prettyPrintedJSON: String? {
|
||||
guard let data = self.data(using: .utf8),
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||
let prettyData = try? JSONSerialization.data(
|
||||
withJSONObject: jsonObject,
|
||||
options: .prettyPrinted
|
||||
),
|
||||
let prettyString = String(data: prettyData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return prettyString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Example Usage
|
||||
// Ví dụ sử dụng
|
||||
|
||||
#if DEBUG
|
||||
extension APIResponseDebugger {
|
||||
/// Example of how to use the debugger
|
||||
/// Ví dụ cách sử dụng debugger
|
||||
static func exampleUsage() {
|
||||
let sampleJSON = """
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "123",
|
||||
"email": "test@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email_confirmed": true,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = sampleJSON.data(using: .utf8) else { return }
|
||||
|
||||
// Debug full response
|
||||
debugJSONData(data, expectedType: "APIResponse<User>")
|
||||
|
||||
// Compare fields
|
||||
let expectedUserFields = [
|
||||
"id", "email", "name", "firstName", "lastName",
|
||||
"avatarUrl", "phoneNumber", "isEmailVerified",
|
||||
"createdAt", "updatedAt", "roles"
|
||||
]
|
||||
compareFields(data: data, expectedKeys: expectedUserFields)
|
||||
|
||||
// Generate model
|
||||
generateModelCode(from: data, modelName: "User")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// SimulatorWarningsSuppressor.swift
|
||||
// AppClientBaseSwift
|
||||
//
|
||||
// Suppress common iOS Simulator warnings
|
||||
// Tắt các warnings phổ biến của iOS Simulator
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: - Simulator Warnings Suppressor
|
||||
// Suppressor cho Simulator Warnings
|
||||
|
||||
/// Helper to suppress simulator-specific warnings
|
||||
/// Helper để tắt warnings đặc thù của simulator
|
||||
struct SimulatorWarningsSuppressor {
|
||||
|
||||
/// Suppress common iOS Simulator warnings
|
||||
/// Tắt các warnings phổ biến của iOS Simulator
|
||||
static func suppressSimulatorWarnings() {
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
|
||||
// Suppress Auto Layout constraint warnings
|
||||
// Tắt warnings về Auto Layout constraints
|
||||
UserDefaults.standard.setValue(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
|
||||
|
||||
// Note: Haptic and keyboard warnings cannot be suppressed programmatically
|
||||
// These are internal iOS system logs
|
||||
// Lưu ý: Haptic và keyboard warnings không thể tắt bằng code
|
||||
// Đây là system logs nội bộ của iOS
|
||||
|
||||
print("ℹ️ Simulator warnings suppression enabled")
|
||||
print(" - Auto Layout constraint warnings: suppressed")
|
||||
print(" - Haptic feedback warnings: cannot suppress (simulator limitation)")
|
||||
print(" - Keyboard warnings: cannot suppress (system logs)")
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Check if running on simulator
|
||||
/// Kiểm tra có đang chạy trên simulator không
|
||||
static var isSimulator: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Print simulator-friendly message about known issues
|
||||
/// In thông báo về các issues đã biết trên simulator
|
||||
static func printSimulatorInfo() {
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
print("""
|
||||
|
||||
⚠️ Running on iOS Simulator - Known Issues:
|
||||
|
||||
1. Haptic Feedback Warnings:
|
||||
- hapticpatternlibrary.plist not found
|
||||
- This is normal on simulator
|
||||
- Will work fine on real devices
|
||||
|
||||
2. Auto Layout Warnings:
|
||||
- Keyboard constraint conflicts
|
||||
- iOS automatically resolves these
|
||||
- No visual impact on app
|
||||
|
||||
3. Keyboard Cache Warnings:
|
||||
- Candidate multiplexer warnings
|
||||
- Simulator-specific issue
|
||||
- Does not affect functionality
|
||||
|
||||
💡 These warnings do NOT indicate bugs in your code.
|
||||
|
||||
""")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Delegate Extension
|
||||
// Extension cho App Delegate
|
||||
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
extension SimulatorWarningsSuppressor {
|
||||
|
||||
/// Call this in AppDelegate or App init
|
||||
/// Gọi hàm này trong AppDelegate hoặc App init
|
||||
static func configure() {
|
||||
suppressSimulatorWarnings()
|
||||
printSimulatorInfo()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user