// 可复用的网络层封装
actor NetworkClient {
static let shared = NetworkClient()
private let session: URLSession
private let baseURL: URL
private let decoder: JSONDecoder
init(baseURL: String = "https://api.example.com") {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 300
config.requestCachePolicy = .returnCacheDataElseLoad
self.session = URLSession(configuration: config)
self.baseURL = URL(string: baseURL)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder = decoder
}
// GET 请求
func get<T: Decodable>(
_ path: String,
queryItems: [URLQueryItem]? = nil
) async throws -> T {
var components = URLComponents(url: baseURL.appendingPathComponent(path),
resolvingAgainstBaseURL: true)!
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.setValue("application/json", forHTTPHeaderField: "Accept")
addAuthHeader(to: &request)
return try await execute(request)
}
// POST 请求
func post<T: Decodable, B: Encodable>(
_ path: String,
body: B
) async throws -> T {
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body)
addAuthHeader(to: &request)
return try await execute(request)
}
// PUT / PATCH / DELETE 类似...
// 文件上传(multipart)
func upload<T: Decodable>(
_ path: String,
fileData: Data,
fileName: String,
mimeType: String
) async throws -> T {
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = "POST"
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)",
forHTTPHeaderField: "Content-Type")
var body = Data()
body.append("--\(boundary)\r\n")
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
body.append("Content-Type: \(mimeType)\r\n\r\n")
body.append(fileData)
body.append("\r\n--\(boundary)--\r\n")
request.httpBody = body
return try await execute(request)
}
// 执行请求 + 带重试
private func execute<T: Decodable>(_ request: URLRequest,
retryCount: Int = 3) async throws -> T {
var lastError: Error?
for attempt in 1...retryCount {
do {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
return try decoder.decode(T.self, from: data)
case 401:
throw NetworkError.unauthorized
case 404:
throw NetworkError.notFound
case 429:
// 限流:等待后重试
try await Task.sleep(for: .seconds(Double(attempt)))
continue
case 500...599:
throw NetworkError.serverError
default:
throw NetworkError.httpError(httpResponse.statusCode)
}
} catch is CancellationError {
throw NetworkError.cancelled
} catch let error as NetworkError {
throw error // 业务错误不重试
} catch {
lastError = error
if attempt < retryCount {
try await Task.sleep(for: .seconds(1))
}
}
}
throw lastError ?? NetworkError.unknown
}
private func addAuthHeader(to request: inout URLRequest) {
if let token = KeychainManager.shared.get("accessToken") {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
}
7.2 Codable - JSON 序列化
swift复制代码
// 基础 Codable 模型
struct Article: Codable, Identifiable {
let id: Int
let title: String
let content: String
let author: Author
let tags: [String]
let publishedAt: Date
var isLiked: Bool
var likeCount: Int
// 字段名映射(JSON snake_case → Swift camelCase)
// 使用 decoder.keyDecodingStrategy = .convertFromSnakeCase 时可省略
enum CodingKeys: String, CodingKey {
case id, title, content, author, tags
case publishedAt = "published_at"
case isLiked = "is_liked"
case likeCount = "like_count"
}
}
struct Author: Codable {
let id: Int
let name: String
let avatarURL: URL?
let bio: String?
}
// 通用分页响应包装
struct PagedResponse<T: Codable>: Codable {
let data: [T]
let total: Int
let page: Int
let perPage: Int
let totalPages: Int
var hasMore: Bool { page < totalPages }
}
// 自定义解码(处理复杂 JSON)
struct FlexibleArticle: Codable {
let id: Int
let title: String
let status: ArticleStatus
// 自定义解码:status 可能是 String 或 Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
// 尝试解码为字符串
if let statusStr = try? container.decode(String.self, forKey: .status) {
status = ArticleStatus(rawValue: statusStr) ?? .draft
} else if let statusInt = try? container.decode(Int.self, forKey: .status) {
status = statusInt == 1 ? .published : .draft
} else {
status = .draft
}
}
}
enum ArticleStatus: String, Codable {
case draft = "draft"
case published = "published"
case archived = "archived"
}
7.3 UserDefaults - 键值存储
swift复制代码
// 类型安全的 UserDefaults 封装
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
var container: UserDefaults = .standard
var wrappedValue: Value {
get { container.object(forKey: key) as? Value ?? defaultValue }
set { container.set(newValue, forKey: key) }
}
var projectedValue: Self { self }
}
// 集中管理 App 配置
enum AppStorage {
@UserDefault("hasCompletedOnboarding", defaultValue: false)
static var hasCompletedOnboarding: Bool
@UserDefault("selectedTheme", defaultValue: "system")
static var selectedTheme: String
@UserDefault("notificationsEnabled", defaultValue: true)
static var notificationsEnabled: Bool
@UserDefault("lastSyncDate", defaultValue: Date.distantPast)
static var lastSyncDate: Date
static func reset() {
UserDefaults.standard.removePersistentDomain(
forName: Bundle.main.bundleIdentifier!
)
}
}
// 存储 Codable 对象
extension UserDefaults {
func setCodable<T: Codable>(_ value: T, forKey key: String) {
let data = try? JSONEncoder().encode(value)
set(data, forKey: key)
}
func codable<T: Codable>(forKey key: String) -> T? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(T.self, from: data)
}
}
// SwiftUI 中使用(@AppStorage = 对 UserDefaults 的封装)
struct SettingsView: View {
@AppStorage("isDarkMode") var isDarkMode = false
@AppStorage("fontSize") var fontSize = 16.0
@AppStorage("selectedLanguage") var selectedLanguage = "zh-Hans"
var body: some View {
Form {
Toggle("深色模式", isOn: $isDarkMode)
Slider(value: $fontSize, in: 12...24, step: 1) {
Text("字体大小:\(Int(fontSize))pt")
}
}
}
}
7.4 SwiftData - 现代本地数据库(iOS 17)
swift复制代码
import SwiftData
// 定义数据模型
@Model
class Task {
var id: UUID
var title: String
var notes: String
var isCompleted: Bool
var priority: TaskPriority
var dueDate: Date?
var createdAt: Date
// 一对多关系
@Relationship(deleteRule: .cascade)
var subtasks: [SubTask] = []
// 多对一关系
var project: Project?
init(title: String, priority: TaskPriority = .medium) {
self.id = UUID()
self.title = title
self.notes = ""
self.isCompleted = false
self.priority = priority
self.createdAt = Date()
}
}
@Model
class SubTask {
var title: String
var isCompleted: Bool
var parentTask: Task?
init(title: String) {
self.title = title
self.isCompleted = false
}
}
enum TaskPriority: Int, Codable {
case low = 0
case medium = 1
case high = 2
var label: String {
switch self {
case .low: "低"
case .medium: "中"
case .high: "高"
}
}
}
// App 入口配置数据容器
@main
struct iOSDemosApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// 自动创建 SQLite 数据库,支持迁移
.modelContainer(for: [Task.self, SubTask.self, Project.self])
}
}
// 视图中的 CRUD 操作
struct TaskListView: View {
@Environment(\.modelContext) private var context
// 查询:自动监听数据变化,实时更新
@Query(sort: \Task.createdAt, order: .reverse)
private var allTasks: [Task]
// 带过滤的查询
@Query(
filter: #Predicate<Task> { task in
task.priority == .high && !task.isCompleted
},
sort: \Task.dueDate
)
private var urgentTasks: [Task]
@State private var newTaskTitle = ""
var body: some View {
List {
Section("紧急任务") {
ForEach(urgentTasks) { TaskRow(task: $0) }
}
Section("所有任务(\(allTasks.count))") {
ForEach(allTasks) { task in
TaskRow(task: task)
.swipeActions {
Button(role: .destructive) {
context.delete(task) // 删除
} label: { Label("删除", systemImage: "trash") }
}
}
}
}
.toolbar {
Button("添加测试任务") {
let task = Task(title: "任务 \(allTasks.count + 1)", priority: .medium)
context.insert(task) // 插入
}
}
}
}
// 动态查询(响应用户筛选)
struct FilterableTaskView: View {
@State private var selectedPriority: TaskPriority?
@State private var showCompletedOnly = false
var body: some View {
DynamicTaskList(
priority: selectedPriority,
completedOnly: showCompletedOnly
)
}
}
struct DynamicTaskList: View {
@Environment(\.modelContext) private var context
let priority: TaskPriority?
let completedOnly: Bool
// 使用 FetchDescriptor 动态查询
var tasks: [Task] {
let predicate = #Predicate<Task> { task in
(priority == nil || task.priority == priority!)
&& (!completedOnly || task.isCompleted)
}
let descriptor = FetchDescriptor<Task>(
predicate: predicate,
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return (try? context.fetch(descriptor)) ?? []
}
var body: some View {
List(tasks) { TaskRow(task: $0) }
}
}
7.5 WebSocket 实时通信
swift复制代码
// WebSocket 管理器
@Observable
class WebSocketManager {
var messages: [ChatMessage] = []
var connectionState = ConnectionState.disconnected
private var task: URLSessionWebSocketTask?
private let session = URLSession.shared
enum ConnectionState { case connecting, connected, disconnected }
func connect(url: URL) {
connectionState = .connecting
task = session.webSocketTask(with: url)
task?.resume()
connectionState = .connected
receiveMessage()
}
private func receiveMessage() {
task?.receive { [weak self] result in
guard let self else { return }
switch result {
case .success(.string(let text)):
if let message = try? JSONDecoder().decode(ChatMessage.self,
from: Data(text.utf8)) {
DispatchQueue.main.async { self.messages.append(message) }
}
self.receiveMessage() // 继续监听
case .failure:
self.handleDisconnect()
default: break
}
}
}
func send(_ text: String) async throws {
try await task?.send(.string(text))
}
func disconnect() {
task?.cancel(with: .goingAway, reason: nil)
connectionState = .disconnected
}
private func handleDisconnect() {
connectionState = .disconnected
// 3秒后重连
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.connect(url: URL(string: "wss://api.example.com/ws")!)
}
}
}