feat: Introduce comprehensive API debugging tools, enhance decoding error handling, and simplify API model definitions.

This commit is contained in:
Ho Ngoc Hai
2026-01-16 18:44:08 +07:00
parent 462e1d0861
commit 420d98e140
7 changed files with 1731 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
//
// APIResponse.swift
// AppClientBaseSwift
//
// API response wrapper models
// Các model wrapper cho API response
//
import Foundation
// MARK: - API Response Wrapper
// Wrapper Response API
/// Generic API response wrapper
/// Wrapper response API chung
struct APIResponse<T: Decodable>: Decodable {
/// Success status
/// Trng thái thành công
let success: Bool
/// Response data
/// D liu response
let data: T?
/// Error message if any
/// Thông báo li nếu có
let error: String?
/// Pagination info if any
/// Thông tin phân trang nếu có
let pagination: Pagination?
}
// MARK: - Pagination
// Phân trang
/// Pagination information
/// Thông tin phân trang
struct Pagination: Decodable {
let currentPage: Int?
let pageSize: Int?
let totalPages: Int?
let totalItems: Int?
enum CodingKeys: String, CodingKey {
case currentPage
case pageSize
case totalPages
case totalItems
}
}
// MARK: - Simple Response
// Response đơn gin
/// Simple success/error response
/// Response thành công/li đơn gin
struct SimpleResponse: Decodable {
let success: Bool
let message: String?
let error: String?
}
// MARK: - List Response
// Response danh sách
/// Generic list response wrapper
/// Wrapper response danh sách
struct ListResponse<T: Decodable>: Decodable {
let success: Bool
let data: [T]?
let error: String?
let pagination: Pagination?
}

View File

@@ -0,0 +1,306 @@
//
// APITestView.swift
// AppClientBaseSwift
//
// Debug view for testing API responses
// View debug đ test API responses
//
import SwiftUI
// MARK: - API Test View
// View test API
/// Debug view to test API endpoints
/// View debug đ test các API endpoints
struct APITestView: View {
@State private var testResult: String = "Chưa có kết quả"
@State private var isLoading: Bool = false
@State private var rawJSON: String = ""
@State private var accessToken: String = ""
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Access Token Input
VStack(alignment: .leading, spacing: 8) {
Text("Access Token")
.font(.headline)
TextEditor(text: $accessToken)
.frame(height: 100)
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.font(.system(.caption, design: .monospaced))
Text("Paste access token từ Keychain hoặc sau khi login thành công")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
// Test Buttons
VStack(spacing: 12) {
Button("Test /users/me Endpoint") {
Task {
await testUsersMeEndpoint()
}
}
.buttonStyle(TestButtonStyle(color: .blue))
Button("Test với Mock Token") {
Task {
await testWithMockToken()
}
}
.buttonStyle(TestButtonStyle(color: .orange))
Button("Clear Results") {
testResult = "Đã xóa kết quả"
rawJSON = ""
}
.buttonStyle(TestButtonStyle(color: .red))
}
.padding()
.disabled(isLoading)
// Loading Indicator
if isLoading {
ProgressView("Đang test API...")
.padding()
}
// Test Result
VStack(alignment: .leading, spacing: 8) {
Text("Kết quả:")
.font(.headline)
ScrollView {
Text(testResult)
.font(.system(.caption, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.frame(maxHeight: 200)
}
.padding()
// Raw JSON
if !rawJSON.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text("Raw JSON Response:")
.font(.headline)
ScrollView {
Text(rawJSON)
.font(.system(.caption, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(8)
}
.frame(maxHeight: 300)
Button("Copy JSON") {
UIPasteboard.general.string = rawJSON
testResult = "✅ Đã copy JSON vào clipboard!"
}
.buttonStyle(TestButtonStyle(color: .green))
}
.padding()
}
// Instructions
VStack(alignment: .leading, spacing: 8) {
Text("Hướng dẫn:")
.font(.headline)
VStack(alignment: .leading, spacing: 4) {
Text("1. Login thành công trước")
Text("2. Access token sẽ tự động được lấy từ Keychain")
Text("3. Nhấn 'Test /users/me Endpoint'")
Text("4. Xem Raw JSON để biết server trả về gì")
Text("5. So sánh với User model trong code")
}
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
.padding()
}
}
.navigationTitle("API Debugger")
.navigationBarTitleDisplayMode(.inline)
}
.onAppear {
// Load current access token
if let token = AuthManager.shared.accessToken {
accessToken = token
}
}
}
// MARK: - Test Methods
/// Test /users/me endpoint
private func testUsersMeEndpoint() async {
isLoading = true
testResult = "🔄 Đang gọi API..."
do {
// Build URL
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + "/users/me") else {
testResult = "❌ Invalid URL"
isLoading = false
return
}
// Create request
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// Use token from input or Keychain
let token = accessToken.isEmpty ? AuthManager.shared.accessToken : accessToken
if let token = token, !token.isEmpty {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
testResult = """
📤 Request Details:
URL: \(url.absoluteString)
Method: GET
Token: \(token?.prefix(20) ?? "None")...
⏳ Waiting for response...
"""
// Perform request
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
testResult = "❌ Invalid response"
isLoading = false
return
}
// Get raw JSON
if let jsonString = String(data: data, encoding: .utf8) {
rawJSON = jsonString.prettyPrintedJSON ?? jsonString
}
// Parse result
testResult = """
📥 Response Details:
Status Code: \(httpResponse.statusCode)
"""
if httpResponse.statusCode == 200 {
testResult += "✅ Status: SUCCESS\n\n"
// Try to decode as User
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
do {
let user = try decoder.decode(User.self, from: data)
testResult += """
✅ Decoded User Successfully:
ID: \(user.id)
Email: \(user.email)
Name: \(user.name)
Avatar: \(user.avatarUrl ?? "nil")
Phone: \(user.phoneNumber ?? "nil")
Email Verified: \(user.isEmailVerified)
"""
} catch let decodingError {
testResult += """
❌ Decoding Failed:
\(decodingError.localizedDescription)
💡 Check Raw JSON below để xem field nào bị thiếu hoặc sai tên
"""
}
} else if httpResponse.statusCode == 401 {
testResult += "❌ Status: UNAUTHORIZED (Token invalid or expired)\n"
} else {
testResult += "❌ Status: ERROR\n"
if let errorMessage = String(data: data, encoding: .utf8) {
testResult += "Error: \(errorMessage)\n"
}
}
} catch {
testResult = """
❌ Request Failed:
\(error.localizedDescription)
"""
rawJSON = ""
}
isLoading = false
}
/// Test with mock token
private func testWithMockToken() async {
testResult = """
⚠️ Testing với mock token...
Lưu ý: Request này sẽ fail với 401 Unauthorized
Mục đích: Kiểm tra server response format khi unauthorized
"""
accessToken = "mock_token_for_testing"
await testUsersMeEndpoint()
}
}
// MARK: - Test Button Style
struct TestButtonStyle: ButtonStyle {
let color: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(
color.opacity(configuration.isPressed ? 0.7 : 1.0)
)
.cornerRadius(10)
}
}
// MARK: - String Extension for Pretty JSON
extension 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: - Preview
#Preview {
APITestView()
}

View File

@@ -0,0 +1,404 @@
//
// APIUsageExamples.swift
// AppClientBaseSwift
//
// Examples of using APIResponse wrapper
// Ví d s dng APIResponse wrapper
//
import Foundation
/*
HƯNG DN S DNG API RESPONSE WRAPPER
======================================
Server ca bn tr v response vi format:
{
"success": true,
"data": { ... } hoc [ ... ],
"error": null,
"pagination": null
}
Tt c API calls phi s dng wrapper tương ng.
*/
// MARK: - Example 1: Single Object Response
// Ví d 1: Response tr v 1 object
/*
GET /api/v1/users/me
Response:
{
"success": true,
"data": {
"id": "...",
"email": "...",
"name": "..."
},
"error": null,
"pagination": null
}
*/
func exampleFetchUser() async throws -> User {
// Cách 1: Dùng unwrap() helper (recommended)
let response: APIResponse<User> = try await APIService.shared.get(
endpoint: "/users/me"
)
let user = try response.unwrap()
return user
// Cách 2: Manual check
// let response: APIResponse<User> = try await APIService.shared.get(endpoint: "/users/me")
// guard response.success, let user = response.data else {
// throw APIError.serverError(statusCode: 400, message: response.error ?? "Failed")
// }
// return user
}
// MARK: - Example 2: List/Array Response
// Ví d 2: Response tr v danh sách
/*
GET /api/v1/users?page=1
Response:
{
"success": true,
"data": [
{ "id": "1", "email": "...", "name": "..." },
{ "id": "2", "email": "...", "name": "..." }
],
"error": null,
"pagination": {
"currentPage": 1,
"pageSize": 20,
"totalPages": 5,
"totalItems": 100
}
}
*/
func exampleFetchUsers() async throws -> [User] {
// Dùng ListResponse wrapper
let response: ListResponse<User> = try await APIService.shared.get(
endpoint: "/users?page=1"
)
// Unwrap đ ly array
let users = try response.unwrap()
// Có th access pagination info
if let pagination = response.pagination {
print("Page \(pagination.currentPage ?? 0) of \(pagination.totalPages ?? 0)")
print("Total items: \(pagination.totalItems ?? 0)")
}
return users
}
// MARK: - Example 3: POST Request with Response Data
// Ví d 3: POST request có response data
/*
POST /api/v1/posts
Body: { "title": "...", "content": "..." }
Response:
{
"success": true,
"data": {
"id": "post_123",
"title": "...",
"content": "...",
"createdAt": "..."
},
"error": null,
"pagination": null
}
*/
struct Post: Codable {
let id: String
let title: String
let content: String
let createdAt: Date?
}
struct CreatePostRequest: Encodable {
let title: String
let content: String
}
func exampleCreatePost(title: String, content: String) async throws -> Post {
let request = CreatePostRequest(title: title, content: content)
let response: APIResponse<Post> = try await APIService.shared.post(
endpoint: "/posts",
body: request
)
return try response.unwrap()
}
// MARK: - Example 4: DELETE/UPDATE Requests
// Ví d 4: DELETE/UPDATE requests
/*
DELETE /api/v1/posts/123
Response:
{
"success": true,
"data": null,
"error": null,
"pagination": null
}
*/
func exampleDeletePost(id: String) async throws {
// Dùng SimpleResponse khi không cn data
let response: SimpleResponse = try await APIService.shared.delete(
endpoint: "/posts/\(id)"
)
if !response.success {
throw APIError.serverError(
statusCode: 400,
message: response.error ?? response.message ?? "Delete failed"
)
}
}
// MARK: - Example 5: PUT/PATCH Request
// Ví d 5: PUT/PATCH request
/*
PUT /api/v1/users/profile
Body: { "name": "New Name", "phoneNumber": "..." }
Response:
{
"success": true,
"data": {
"id": "...",
"email": "...",
"name": "New Name",
...
},
"error": null,
"pagination": null
}
*/
struct UpdateProfileRequest: Encodable {
let name: String?
let phoneNumber: String?
}
func exampleUpdateProfile(name: String?, phoneNumber: String?) async throws -> User {
let request = UpdateProfileRequest(name: name, phoneNumber: phoneNumber)
let response: APIResponse<User> = try await APIService.shared.put(
endpoint: "/users/profile",
body: request
)
return try response.unwrap()
}
// MARK: - Example 6: Error Handling
// Ví d 6: X lý li
func exampleWithErrorHandling() async {
do {
let response: APIResponse<User> = try await APIService.shared.get(
endpoint: "/users/me"
)
let user = try response.unwrap()
print("Success: \(user.email)")
} catch let error as APIError {
switch error {
case .decodingError(let decodingError):
print("❌ Decoding error: \(decodingError)")
// Server response structure doesn't match expected model
case .unauthorized:
print("❌ Unauthorized - token invalid or expired")
// Need to login again
case .networkError(let networkError):
print("❌ Network error: \(networkError)")
// No internet connection or server unreachable
case .serverError(let statusCode, let message):
print("❌ Server error \(statusCode): \(message ?? "Unknown")")
// Server returned error response
case .noData:
print("❌ No data received")
default:
print("❌ Unknown error: \(error)")
}
} catch {
print("❌ Unexpected error: \(error)")
}
}
// MARK: - Example 7: Nested Objects
// Ví d 7: Objects lng nhau
/*
GET /api/v1/posts/123/comments
Response:
{
"success": true,
"data": [
{
"id": "comment_1",
"content": "Great post!",
"author": {
"id": "user_1",
"name": "John Doe",
"avatarUrl": "..."
},
"createdAt": "..."
}
],
"error": null,
"pagination": { ... }
}
*/
struct Comment: Codable {
let id: String
let content: String
let author: CommentAuthor
let createdAt: Date?
}
struct CommentAuthor: Codable {
let id: String
let name: String
let avatarUrl: String?
}
func exampleFetchComments(postId: String) async throws -> [Comment] {
let response: ListResponse<Comment> = try await APIService.shared.get(
endpoint: "/posts/\(postId)/comments"
)
return try response.unwrap()
}
// MARK: - Example 8: Search/Filter with Query Parameters
// Ví d 8: Search/Filter vi query parameters
/*
GET /api/v1/posts?search=swift&category=tutorial&page=1&pageSize=20
*/
func exampleSearchPosts(
search: String?,
category: String?,
page: Int = 1,
pageSize: Int = 20
) async throws -> [Post] {
var queryItems: [String] = []
if let search = search {
queryItems.append("search=\(search)")
}
if let category = category {
queryItems.append("category=\(category)")
}
queryItems.append("page=\(page)")
queryItems.append("pageSize=\(pageSize)")
let queryString = queryItems.joined(separator: "&")
let endpoint = "/posts?\(queryString)"
let response: ListResponse<Post> = try await APIService.shared.get(
endpoint: endpoint
)
return try response.unwrap()
}
// MARK: - Example 9: Upload File (if needed)
// Ví d 9: Upload file (nếu cn)
/*
POST /api/v1/users/avatar
Content-Type: multipart/form-data
Note: Cn implement multipart form data nếu cn upload file
Hin ti APIService ch h tr JSON
*/
// MARK: - Example 10: Custom Headers
// Ví d 10: Custom headers
func exampleWithCustomHeaders() async throws -> User {
// Nếu cn thêm headers đc bit
let response: APIResponse<User> = try await APIService.shared.request(
endpoint: "/users/me",
method: .get,
body: nil as String?,
headers: [
"X-Custom-Header": "custom-value",
"X-Request-ID": UUID().uuidString
]
)
return try response.unwrap()
}
// MARK: - Best Practices
// Các best practices
/*
DO - Nên làm:
1. Luôn dùng APIResponse<T> hoc ListResponse<T>
2. Dùng unwrap() helper đ code ngn gn
3. Handle errors properly vi do-catch
4. Log errors trong development
5. Validate request data trưc khi gi API
6. Cache data khi cn (như User data)
DON'T - Không nên làm:
1. Không decode trc tiếp thành Model mà không wrapper
2. Không ignore errors
3. Không hardcode URLs, dùng Constants
4. Không lưu sensitive data vào UserDefaults (dùng Keychain)
5. Không block main thread vi synchronous calls
*/
// MARK: - Testing Tips
// Tips test API
/*
Test APIs trong development:
1. Dùng APITestView đ test tng endpoint
2. Check Console logs vi DebugLogger
3. Verify JSON response structure
4. Test error cases (401, 404, 500, etc.)
5. Test vi slow network / offline
Mock data cho UI testing:
#if DEBUG
func mockFetchUser() async -> User {
return User.sample
}
#endif
*/

View File

@@ -0,0 +1,389 @@
//
// APIDebugView.swift
// AppClientBaseSwift
//
// Comprehensive API debugging view
// View debug API toàn din
//
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()
}

View File

@@ -0,0 +1,291 @@
//
// APIResponseDebugger.swift
// AppClientBaseSwift
//
// Helper for debugging API response issues
// Helper đ debug các vn đ 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 liu 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 ca model vi key thc tế trong JSON
/// - Parameters:
/// - data: Raw JSON data / D liu 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
/// To code Swift model t JSON data
/// - Parameters:
/// - data: Raw JSON data / D liu 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 chuyn đi case
extension String {
/// Convert string to snake_case
/// Chuyn 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
/// Chuyn 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 dng
#if DEBUG
extension APIResponseDebugger {
/// Example of how to use the debugger
/// Ví d cách s dng 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

View File

@@ -0,0 +1,94 @@
//
// SimulatorWarningsSuppressor.swift
// AppClientBaseSwift
//
// Suppress common iOS Simulator warnings
// Tt các warnings ph biến ca iOS Simulator
//
import Foundation
import UIKit
// MARK: - Simulator Warnings Suppressor
// Suppressor cho Simulator Warnings
/// Helper to suppress simulator-specific warnings
/// Helper đ tt warnings đc thù ca simulator
struct SimulatorWarningsSuppressor {
/// Suppress common iOS Simulator warnings
/// Tt các warnings ph biến ca iOS Simulator
static func suppressSimulatorWarnings() {
#if DEBUG && targetEnvironment(simulator)
// Suppress Auto Layout constraint warnings
// Tt 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 tt bng code
// Đây là system logs ni b ca 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
/// Kim tra có đang chy 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
/// Gi hàm này trong AppDelegate hoc App init
static func configure() {
suppressSimulatorWarnings()
printSimulatorInfo()
}
}
#endif

View File

@@ -0,0 +1,173 @@
//
// DebugLogger.swift
// AppClientBaseSwift
//
// Debug logging utilities
// Tin ích ghi log debug
//
import Foundation
// MARK: - Debug Logger
// Logger Debug
/// Debug logging utility
/// Tin ích ghi log debug
enum DebugLogger {
/// Log level
/// Mc đ log
enum Level: String {
case verbose = "📝"
case info = ""
case warning = "⚠️"
case error = ""
case success = ""
}
/// Log message with level
/// Ghi log message vi level
/// - Parameters:
/// - level: Log level / Mc đ log
/// - message: Log message / Thông đip log
/// - file: Source file / File ngun
/// - function: Function name / Tên function
/// - line: Line number / S dòng
static func log(
_ level: Level,
_ message: String,
file: String = #file,
function: String = #function,
line: Int = #line
) {
#if DEBUG
let fileName = (file as NSString).lastPathComponent
let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
print("\(level.rawValue) [\(timestamp)] \(fileName):\(line) - \(function)")
print(" \(message)")
#endif
}
/// Log verbose message
/// Ghi log verbose
static func verbose(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(.verbose, message, file: file, function: function, line: line)
}
/// Log info message
/// Ghi log info
static func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(.info, message, file: file, function: function, line: line)
}
/// Log warning message
/// Ghi log warning
static func warning(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(.warning, message, file: file, function: function, line: line)
}
/// Log error message
/// Ghi log error
static func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(.error, message, file: file, function: function, line: line)
}
/// Log success message
/// Ghi log success
static func success(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(.success, message, file: file, function: function, line: line)
}
/// Log JSON data
/// Ghi log d liu JSON
/// - Parameters:
/// - data: JSON data / D liu JSON
/// - title: Optional title / Tiêu đ tùy chn
static func logJSON(_ data: Data, title: String = "JSON Response") {
#if DEBUG
guard let jsonString = String(data: data, encoding: .utf8) else {
error("Unable to convert data to string")
return
}
// Try to pretty print JSON
// Th format JSON đp
if let jsonData = jsonString.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: jsonData),
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
let prettyString = String(data: prettyData, encoding: .utf8) {
print("📄 \(title):")
print(prettyString)
} else {
print("📄 \(title):")
print(jsonString)
}
#endif
}
/// Log API request
/// Ghi log API request
/// - Parameters:
/// - method: HTTP method / Phương thc HTTP
/// - url: Request URL / URL request
/// - headers: Request headers / Headers request
/// - body: Request body / Body request
static func logRequest(
method: String,
url: String,
headers: [String: String]? = nil,
body: Data? = nil
) {
#if DEBUG
print("🚀 API Request:")
print(" Method: \(method)")
print(" URL: \(url)")
if let headers = headers, !headers.isEmpty {
print(" Headers:")
headers.forEach { key, value in
// Hide sensitive data
// n d liu nhy cm
if key.lowercased() == "authorization" {
print(" \(key): Bearer ***")
} else {
print(" \(key): \(value)")
}
}
}
if let body = body {
print(" Body:")
logJSON(body, title: "Request Body")
}
#endif
}
/// Log API response
/// Ghi log API response
/// - Parameters:
/// - statusCode: HTTP status code / Mã trng thái HTTP
/// - data: Response data / D liu response
/// - error: Error if any / Li nếu có
static func logResponse(
statusCode: Int,
data: Data?,
error: Error? = nil
) {
#if DEBUG
if let error = error {
print("❌ API Response Error:")
print(" Status Code: \(statusCode)")
print(" Error: \(error.localizedDescription)")
} else {
let emoji = statusCode >= 200 && statusCode < 300 ? "" : "⚠️"
print("\(emoji) API Response:")
print(" Status Code: \(statusCode)")
}
if let data = data {
logJSON(data, title: "Response Data")
}
#endif
}
}