code-abyss 1.6.16 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/SKILL.md +24 -16
- package/skills/domains/ai/SKILL.md +2 -2
- package/skills/domains/ai/prompt-and-eval.md +279 -0
- package/skills/domains/architecture/SKILL.md +2 -3
- package/skills/domains/architecture/security-arch.md +87 -0
- package/skills/domains/data-engineering/SKILL.md +188 -26
- package/skills/domains/development/SKILL.md +1 -4
- package/skills/domains/devops/SKILL.md +3 -5
- package/skills/domains/devops/performance.md +63 -0
- package/skills/domains/devops/testing.md +97 -0
- package/skills/domains/frontend-design/SKILL.md +12 -3
- package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
- package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/skills/domains/frontend-design/engineering.md +287 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
- package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
- package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
- package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/skills/domains/infrastructure/SKILL.md +174 -34
- package/skills/domains/mobile/SKILL.md +211 -21
- package/skills/domains/orchestration/SKILL.md +1 -0
- package/skills/domains/security/SKILL.md +4 -6
- package/skills/domains/security/blue-team.md +57 -0
- package/skills/domains/security/red-team.md +54 -0
- package/skills/domains/security/threat-intel.md +50 -0
- package/skills/orchestration/multi-agent/SKILL.md +195 -46
- package/skills/run_skill.js +134 -0
- package/skills/tools/gen-docs/SKILL.md +6 -4
- package/skills/tools/gen-docs/scripts/doc_generator.js +349 -0
- package/skills/tools/verify-change/SKILL.md +8 -6
- package/skills/tools/verify-change/scripts/change_analyzer.js +270 -0
- package/skills/tools/verify-module/SKILL.md +6 -4
- package/skills/tools/verify-module/scripts/module_scanner.js +145 -0
- package/skills/tools/verify-quality/SKILL.md +5 -3
- package/skills/tools/verify-quality/scripts/quality_checker.js +276 -0
- package/skills/tools/verify-security/SKILL.md +7 -5
- package/skills/tools/verify-security/scripts/security_scanner.js +133 -0
- package/skills/__pycache__/run_skill.cpython-312.pyc +0 -0
- package/skills/domains/COVERAGE_PLAN.md +0 -232
- package/skills/domains/ai/model-evaluation.md +0 -790
- package/skills/domains/ai/prompt-engineering.md +0 -703
- package/skills/domains/architecture/compliance.md +0 -299
- package/skills/domains/architecture/data-security.md +0 -184
- package/skills/domains/data-engineering/data-pipeline.md +0 -762
- package/skills/domains/data-engineering/data-quality.md +0 -894
- package/skills/domains/data-engineering/stream-processing.md +0 -791
- package/skills/domains/development/dart.md +0 -963
- package/skills/domains/development/kotlin.md +0 -834
- package/skills/domains/development/php.md +0 -659
- package/skills/domains/development/swift.md +0 -755
- package/skills/domains/devops/e2e-testing.md +0 -914
- package/skills/domains/devops/performance-testing.md +0 -734
- package/skills/domains/devops/testing-strategy.md +0 -667
- package/skills/domains/frontend-design/build-tools.md +0 -743
- package/skills/domains/frontend-design/performance.md +0 -734
- package/skills/domains/frontend-design/testing.md +0 -699
- package/skills/domains/infrastructure/gitops.md +0 -735
- package/skills/domains/infrastructure/iac.md +0 -855
- package/skills/domains/infrastructure/kubernetes.md +0 -1018
- package/skills/domains/mobile/android-dev.md +0 -979
- package/skills/domains/mobile/cross-platform.md +0 -795
- package/skills/domains/mobile/ios-dev.md +0 -931
- package/skills/domains/security/secrets-management.md +0 -834
- package/skills/domains/security/supply-chain.md +0 -931
- package/skills/domains/security/threat-modeling.md +0 -828
- package/skills/run_skill.py +0 -153
- package/skills/tests/README.md +0 -225
- package/skills/tests/SUMMARY.md +0 -362
- package/skills/tests/__init__.py +0 -3
- package/skills/tests/__pycache__/test_change_analyzer.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_doc_generator.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_module_scanner.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_quality_checker.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_security_scanner.cpython-312.pyc +0 -0
- package/skills/tests/test_change_analyzer.py +0 -558
- package/skills/tests/test_doc_generator.py +0 -538
- package/skills/tests/test_module_scanner.py +0 -376
- package/skills/tests/test_quality_checker.py +0 -516
- package/skills/tests/test_security_scanner.py +0 -426
- package/skills/tools/gen-docs/scripts/__pycache__/doc_generator.cpython-312.pyc +0 -0
- package/skills/tools/gen-docs/scripts/doc_generator.py +0 -520
- package/skills/tools/verify-change/scripts/__pycache__/change_analyzer.cpython-312.pyc +0 -0
- package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
- package/skills/tools/verify-module/scripts/__pycache__/module_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
- package/skills/tools/verify-quality/scripts/__pycache__/quality_checker.cpython-312.pyc +0 -0
- package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
- package/skills/tools/verify-security/scripts/__pycache__/security_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-security/scripts/security_scanner.py +0 -374
|
@@ -1,931 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ios-dev
|
|
3
|
-
description: iOS 开发。SwiftUI、UIKit、Combine、MVVM、VIPER、Auto Layout、iOS架构。当用户提到 iOS 开发、SwiftUI、UIKit、Combine 时使用。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 🍎 iOS 开发 · iOS Development
|
|
7
|
-
|
|
8
|
-
## SwiftUI 基础
|
|
9
|
-
|
|
10
|
-
### View 组件
|
|
11
|
-
```swift
|
|
12
|
-
import SwiftUI
|
|
13
|
-
|
|
14
|
-
struct ContentView: View {
|
|
15
|
-
var body: some View {
|
|
16
|
-
VStack(spacing: 20) {
|
|
17
|
-
Text("Hello, SwiftUI")
|
|
18
|
-
.font(.largeTitle)
|
|
19
|
-
.foregroundColor(.blue)
|
|
20
|
-
|
|
21
|
-
Image(systemName: "star.fill")
|
|
22
|
-
.resizable()
|
|
23
|
-
.frame(width: 50, height: 50)
|
|
24
|
-
|
|
25
|
-
Button("Tap Me") {
|
|
26
|
-
print("Button tapped")
|
|
27
|
-
}
|
|
28
|
-
.buttonStyle(.borderedProminent)
|
|
29
|
-
}
|
|
30
|
-
.padding()
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### State 管理
|
|
36
|
-
```swift
|
|
37
|
-
struct CounterView: View {
|
|
38
|
-
@State private var count = 0
|
|
39
|
-
@State private var isOn = false
|
|
40
|
-
|
|
41
|
-
var body: some View {
|
|
42
|
-
VStack {
|
|
43
|
-
Text("Count: \(count)")
|
|
44
|
-
.font(.title)
|
|
45
|
-
|
|
46
|
-
HStack {
|
|
47
|
-
Button("-") { count -= 1 }
|
|
48
|
-
Button("+") { count += 1 }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
Toggle("Enable", isOn: $isOn)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Binding 双向绑定
|
|
58
|
-
```swift
|
|
59
|
-
struct ParentView: View {
|
|
60
|
-
@State private var text = ""
|
|
61
|
-
|
|
62
|
-
var body: some View {
|
|
63
|
-
VStack {
|
|
64
|
-
Text("Input: \(text)")
|
|
65
|
-
ChildView(text: $text)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
struct ChildView: View {
|
|
71
|
-
@Binding var text: String
|
|
72
|
-
|
|
73
|
-
var body: some View {
|
|
74
|
-
TextField("Enter text", text: $text)
|
|
75
|
-
.textFieldStyle(.roundedBorder)
|
|
76
|
-
.padding()
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## SwiftUI 高级
|
|
82
|
-
|
|
83
|
-
### ObservableObject
|
|
84
|
-
```swift
|
|
85
|
-
class UserViewModel: ObservableObject {
|
|
86
|
-
@Published var username = ""
|
|
87
|
-
@Published var isLoading = false
|
|
88
|
-
@Published var users: [User] = []
|
|
89
|
-
|
|
90
|
-
func fetchUsers() async {
|
|
91
|
-
isLoading = true
|
|
92
|
-
defer { isLoading = false }
|
|
93
|
-
|
|
94
|
-
do {
|
|
95
|
-
let url = URL(string: "https://api.example.com/users")!
|
|
96
|
-
let (data, _) = try await URLSession.shared.data(from: url)
|
|
97
|
-
users = try JSONDecoder().decode([User].self, from: data)
|
|
98
|
-
} catch {
|
|
99
|
-
print("Error: \(error)")
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
struct UserListView: View {
|
|
105
|
-
@StateObject private var viewModel = UserViewModel()
|
|
106
|
-
|
|
107
|
-
var body: some View {
|
|
108
|
-
List(viewModel.users) { user in
|
|
109
|
-
Text(user.name)
|
|
110
|
-
}
|
|
111
|
-
.task {
|
|
112
|
-
await viewModel.fetchUsers()
|
|
113
|
-
}
|
|
114
|
-
.overlay {
|
|
115
|
-
if viewModel.isLoading {
|
|
116
|
-
ProgressView()
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Environment
|
|
124
|
-
```swift
|
|
125
|
-
// 自定义 Environment Key
|
|
126
|
-
struct ThemeKey: EnvironmentKey {
|
|
127
|
-
static let defaultValue = Theme.light
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
extension EnvironmentValues {
|
|
131
|
-
var theme: Theme {
|
|
132
|
-
get { self[ThemeKey.self] }
|
|
133
|
-
set { self[ThemeKey.self] = newValue }
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 使用
|
|
138
|
-
struct RootView: View {
|
|
139
|
-
@State private var theme = Theme.dark
|
|
140
|
-
|
|
141
|
-
var body: some View {
|
|
142
|
-
ContentView()
|
|
143
|
-
.environment(\.theme, theme)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
struct ContentView: View {
|
|
148
|
-
@Environment(\.theme) var theme
|
|
149
|
-
|
|
150
|
-
var body: some View {
|
|
151
|
-
Text("Hello")
|
|
152
|
-
.foregroundColor(theme.textColor)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Custom ViewModifier
|
|
158
|
-
```swift
|
|
159
|
-
struct CardModifier: ViewModifier {
|
|
160
|
-
func body(content: Content) -> some View {
|
|
161
|
-
content
|
|
162
|
-
.padding()
|
|
163
|
-
.background(Color.white)
|
|
164
|
-
.cornerRadius(10)
|
|
165
|
-
.shadow(radius: 5)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
extension View {
|
|
170
|
-
func cardStyle() -> some View {
|
|
171
|
-
modifier(CardModifier())
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 使用
|
|
176
|
-
Text("Card Content")
|
|
177
|
-
.cardStyle()
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## UIKit 集成
|
|
181
|
-
|
|
182
|
-
### UIViewController 包装
|
|
183
|
-
```swift
|
|
184
|
-
import UIKit
|
|
185
|
-
import SwiftUI
|
|
186
|
-
|
|
187
|
-
struct CameraView: UIViewControllerRepresentable {
|
|
188
|
-
@Binding var image: UIImage?
|
|
189
|
-
@Environment(\.dismiss) var dismiss
|
|
190
|
-
|
|
191
|
-
func makeUIViewController(context: Context) -> UIImagePickerController {
|
|
192
|
-
let picker = UIImagePickerController()
|
|
193
|
-
picker.sourceType = .camera
|
|
194
|
-
picker.delegate = context.coordinator
|
|
195
|
-
return picker
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
|
199
|
-
|
|
200
|
-
func makeCoordinator() -> Coordinator {
|
|
201
|
-
Coordinator(self)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
|
205
|
-
let parent: CameraView
|
|
206
|
-
|
|
207
|
-
init(_ parent: CameraView) {
|
|
208
|
-
self.parent = parent
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
212
|
-
if let image = info[.originalImage] as? UIImage {
|
|
213
|
-
parent.image = image
|
|
214
|
-
}
|
|
215
|
-
parent.dismiss()
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### UIView 包装
|
|
222
|
-
```swift
|
|
223
|
-
struct MapView: UIViewRepresentable {
|
|
224
|
-
@Binding var region: MKCoordinateRegion
|
|
225
|
-
|
|
226
|
-
func makeUIView(context: Context) -> MKMapView {
|
|
227
|
-
let mapView = MKMapView()
|
|
228
|
-
mapView.delegate = context.coordinator
|
|
229
|
-
return mapView
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
func updateUIView(_ uiView: MKMapView, context: Context) {
|
|
233
|
-
uiView.setRegion(region, animated: true)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
func makeCoordinator() -> Coordinator {
|
|
237
|
-
Coordinator(self)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
class Coordinator: NSObject, MKMapViewDelegate {
|
|
241
|
-
var parent: MapView
|
|
242
|
-
|
|
243
|
-
init(_ parent: MapView) {
|
|
244
|
-
self.parent = parent
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Auto Layout
|
|
251
|
-
```swift
|
|
252
|
-
class CustomViewController: UIViewController {
|
|
253
|
-
let titleLabel = UILabel()
|
|
254
|
-
let button = UIButton()
|
|
255
|
-
|
|
256
|
-
override func viewDidLoad() {
|
|
257
|
-
super.viewDidLoad()
|
|
258
|
-
setupUI()
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
func setupUI() {
|
|
262
|
-
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
263
|
-
button.translatesAutoresizingMaskIntoConstraints = false
|
|
264
|
-
|
|
265
|
-
view.addSubview(titleLabel)
|
|
266
|
-
view.addSubview(button)
|
|
267
|
-
|
|
268
|
-
NSLayoutConstraint.activate([
|
|
269
|
-
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
|
|
270
|
-
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
271
|
-
|
|
272
|
-
button.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
|
|
273
|
-
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
274
|
-
button.widthAnchor.constraint(equalToConstant: 200),
|
|
275
|
-
button.heightAnchor.constraint(equalToConstant: 44)
|
|
276
|
-
])
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
## Combine 响应式
|
|
282
|
-
|
|
283
|
-
### Publisher 基础
|
|
284
|
-
```swift
|
|
285
|
-
import Combine
|
|
286
|
-
|
|
287
|
-
class DataService {
|
|
288
|
-
func fetchData() -> AnyPublisher<[Item], Error> {
|
|
289
|
-
URLSession.shared
|
|
290
|
-
.dataTaskPublisher(for: URL(string: "https://api.example.com/items")!)
|
|
291
|
-
.map(\.data)
|
|
292
|
-
.decode(type: [Item].self, decoder: JSONDecoder())
|
|
293
|
-
.eraseToAnyPublisher()
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
class ViewModel: ObservableObject {
|
|
298
|
-
@Published var items: [Item] = []
|
|
299
|
-
@Published var error: Error?
|
|
300
|
-
|
|
301
|
-
private var cancellables = Set<AnyCancellable>()
|
|
302
|
-
private let service = DataService()
|
|
303
|
-
|
|
304
|
-
func load() {
|
|
305
|
-
service.fetchData()
|
|
306
|
-
.receive(on: DispatchQueue.main)
|
|
307
|
-
.sink(
|
|
308
|
-
receiveCompletion: { [weak self] completion in
|
|
309
|
-
if case .failure(let error) = completion {
|
|
310
|
-
self?.error = error
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
receiveValue: { [weak self] items in
|
|
314
|
-
self?.items = items
|
|
315
|
-
}
|
|
316
|
-
)
|
|
317
|
-
.store(in: &cancellables)
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Operators
|
|
323
|
-
```swift
|
|
324
|
-
// Map & Filter
|
|
325
|
-
let numbers = [1, 2, 3, 4, 5].publisher
|
|
326
|
-
numbers
|
|
327
|
-
.map { $0 * 2 }
|
|
328
|
-
.filter { $0 > 5 }
|
|
329
|
-
.sink { print($0) }
|
|
330
|
-
.store(in: &cancellables)
|
|
331
|
-
|
|
332
|
-
// Debounce (搜索防抖)
|
|
333
|
-
@Published var searchText = ""
|
|
334
|
-
|
|
335
|
-
$searchText
|
|
336
|
-
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
|
337
|
-
.removeDuplicates()
|
|
338
|
-
.sink { text in
|
|
339
|
-
self.performSearch(text)
|
|
340
|
-
}
|
|
341
|
-
.store(in: &cancellables)
|
|
342
|
-
|
|
343
|
-
// CombineLatest
|
|
344
|
-
Publishers.CombineLatest($username, $password)
|
|
345
|
-
.map { username, password in
|
|
346
|
-
!username.isEmpty && password.count >= 6
|
|
347
|
-
}
|
|
348
|
-
.assign(to: &$isValid)
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Subject
|
|
352
|
-
```swift
|
|
353
|
-
class EventBus {
|
|
354
|
-
static let shared = EventBus()
|
|
355
|
-
|
|
356
|
-
let userLoggedIn = PassthroughSubject<User, Never>()
|
|
357
|
-
let dataUpdated = CurrentValueSubject<[Item], Never>([])
|
|
358
|
-
|
|
359
|
-
private init() {}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 发送
|
|
363
|
-
EventBus.shared.userLoggedIn.send(user)
|
|
364
|
-
|
|
365
|
-
// 订阅
|
|
366
|
-
EventBus.shared.userLoggedIn
|
|
367
|
-
.sink { user in
|
|
368
|
-
print("User logged in: \(user.name)")
|
|
369
|
-
}
|
|
370
|
-
.store(in: &cancellables)
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
## MVVM 架构
|
|
374
|
-
|
|
375
|
-
### Model
|
|
376
|
-
```swift
|
|
377
|
-
struct User: Codable, Identifiable {
|
|
378
|
-
let id: Int
|
|
379
|
-
let name: String
|
|
380
|
-
let email: String
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
struct LoginRequest: Codable {
|
|
384
|
-
let username: String
|
|
385
|
-
let password: String
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
struct LoginResponse: Codable {
|
|
389
|
-
let token: String
|
|
390
|
-
let user: User
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### Repository
|
|
395
|
-
```swift
|
|
396
|
-
protocol UserRepository {
|
|
397
|
-
func login(username: String, password: String) async throws -> LoginResponse
|
|
398
|
-
func fetchProfile() async throws -> User
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
class UserRepositoryImpl: UserRepository {
|
|
402
|
-
private let apiClient: APIClient
|
|
403
|
-
|
|
404
|
-
init(apiClient: APIClient = .shared) {
|
|
405
|
-
self.apiClient = apiClient
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
func login(username: String, password: String) async throws -> LoginResponse {
|
|
409
|
-
let request = LoginRequest(username: username, password: password)
|
|
410
|
-
return try await apiClient.post("/auth/login", body: request)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
func fetchProfile() async throws -> User {
|
|
414
|
-
try await apiClient.get("/user/profile")
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### ViewModel
|
|
420
|
-
```swift
|
|
421
|
-
@MainActor
|
|
422
|
-
class LoginViewModel: ObservableObject {
|
|
423
|
-
@Published var username = ""
|
|
424
|
-
@Published var password = ""
|
|
425
|
-
@Published var isLoading = false
|
|
426
|
-
@Published var error: String?
|
|
427
|
-
@Published var isLoggedIn = false
|
|
428
|
-
|
|
429
|
-
private let repository: UserRepository
|
|
430
|
-
|
|
431
|
-
init(repository: UserRepository = UserRepositoryImpl()) {
|
|
432
|
-
self.repository = repository
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
var isValid: Bool {
|
|
436
|
-
!username.isEmpty && password.count >= 6
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
func login() async {
|
|
440
|
-
guard isValid else { return }
|
|
441
|
-
|
|
442
|
-
isLoading = true
|
|
443
|
-
error = nil
|
|
444
|
-
|
|
445
|
-
do {
|
|
446
|
-
let response = try await repository.login(username: username, password: password)
|
|
447
|
-
TokenManager.shared.save(response.token)
|
|
448
|
-
isLoggedIn = true
|
|
449
|
-
} catch {
|
|
450
|
-
self.error = error.localizedDescription
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
isLoading = false
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### View
|
|
459
|
-
```swift
|
|
460
|
-
struct LoginView: View {
|
|
461
|
-
@StateObject private var viewModel = LoginViewModel()
|
|
462
|
-
|
|
463
|
-
var body: some View {
|
|
464
|
-
VStack(spacing: 20) {
|
|
465
|
-
TextField("Username", text: $viewModel.username)
|
|
466
|
-
.textFieldStyle(.roundedBorder)
|
|
467
|
-
|
|
468
|
-
SecureField("Password", text: $viewModel.password)
|
|
469
|
-
.textFieldStyle(.roundedBorder)
|
|
470
|
-
|
|
471
|
-
if let error = viewModel.error {
|
|
472
|
-
Text(error)
|
|
473
|
-
.foregroundColor(.red)
|
|
474
|
-
.font(.caption)
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
Button("Login") {
|
|
478
|
-
Task {
|
|
479
|
-
await viewModel.login()
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
.disabled(!viewModel.isValid || viewModel.isLoading)
|
|
483
|
-
.buttonStyle(.borderedProminent)
|
|
484
|
-
|
|
485
|
-
if viewModel.isLoading {
|
|
486
|
-
ProgressView()
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
.padding()
|
|
490
|
-
.fullScreenCover(isPresented: $viewModel.isLoggedIn) {
|
|
491
|
-
HomeView()
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
## VIPER 架构
|
|
498
|
-
|
|
499
|
-
```
|
|
500
|
-
View ←→ Presenter ←→ Interactor
|
|
501
|
-
↓ ↓
|
|
502
|
-
Router Entity
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### Entity
|
|
506
|
-
```swift
|
|
507
|
-
struct Article: Codable {
|
|
508
|
-
let id: Int
|
|
509
|
-
let title: String
|
|
510
|
-
let content: String
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Interactor
|
|
515
|
-
```swift
|
|
516
|
-
protocol ArticleInteractorProtocol {
|
|
517
|
-
func fetchArticles() async throws -> [Article]
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
class ArticleInteractor: ArticleInteractorProtocol {
|
|
521
|
-
private let repository: ArticleRepository
|
|
522
|
-
|
|
523
|
-
init(repository: ArticleRepository) {
|
|
524
|
-
self.repository = repository
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
func fetchArticles() async throws -> [Article] {
|
|
528
|
-
try await repository.fetchArticles()
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Presenter
|
|
534
|
-
```swift
|
|
535
|
-
@MainActor
|
|
536
|
-
protocol ArticlePresenterProtocol: ObservableObject {
|
|
537
|
-
var articles: [Article] { get }
|
|
538
|
-
var isLoading: Bool { get }
|
|
539
|
-
func loadArticles()
|
|
540
|
-
func didSelectArticle(_ article: Article)
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
@MainActor
|
|
544
|
-
class ArticlePresenter: ArticlePresenterProtocol {
|
|
545
|
-
@Published var articles: [Article] = []
|
|
546
|
-
@Published var isLoading = false
|
|
547
|
-
|
|
548
|
-
private let interactor: ArticleInteractorProtocol
|
|
549
|
-
private let router: ArticleRouterProtocol
|
|
550
|
-
|
|
551
|
-
init(interactor: ArticleInteractorProtocol, router: ArticleRouterProtocol) {
|
|
552
|
-
self.interactor = interactor
|
|
553
|
-
self.router = router
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
func loadArticles() {
|
|
557
|
-
Task {
|
|
558
|
-
isLoading = true
|
|
559
|
-
do {
|
|
560
|
-
articles = try await interactor.fetchArticles()
|
|
561
|
-
} catch {
|
|
562
|
-
print("Error: \(error)")
|
|
563
|
-
}
|
|
564
|
-
isLoading = false
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
func didSelectArticle(_ article: Article) {
|
|
569
|
-
router.navigateToDetail(article)
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
### View
|
|
575
|
-
```swift
|
|
576
|
-
struct ArticleListView<Presenter: ArticlePresenterProtocol>: View {
|
|
577
|
-
@ObservedObject var presenter: Presenter
|
|
578
|
-
|
|
579
|
-
var body: some View {
|
|
580
|
-
List(presenter.articles, id: \.id) { article in
|
|
581
|
-
Button(article.title) {
|
|
582
|
-
presenter.didSelectArticle(article)
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
.task {
|
|
586
|
-
presenter.loadArticles()
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### Router
|
|
593
|
-
```swift
|
|
594
|
-
protocol ArticleRouterProtocol {
|
|
595
|
-
func navigateToDetail(_ article: Article)
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
class ArticleRouter: ArticleRouterProtocol {
|
|
599
|
-
weak var viewController: UIViewController?
|
|
600
|
-
|
|
601
|
-
func navigateToDetail(_ article: Article) {
|
|
602
|
-
let detailVC = ArticleDetailBuilder.build(article: article)
|
|
603
|
-
viewController?.navigationController?.pushViewController(detailVC, animated: true)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
## 网络层
|
|
609
|
-
|
|
610
|
-
### APIClient
|
|
611
|
-
```swift
|
|
612
|
-
class APIClient {
|
|
613
|
-
static let shared = APIClient()
|
|
614
|
-
private let baseURL = "https://api.example.com"
|
|
615
|
-
|
|
616
|
-
func get<T: Decodable>(_ path: String) async throws -> T {
|
|
617
|
-
try await request(path, method: "GET")
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
func post<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
|
621
|
-
try await request(path, method: "POST", body: body)
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
private func request<T: Decodable, B: Encodable>(_ path: String, method: String, body: B? = nil as String?) async throws -> T {
|
|
625
|
-
guard let url = URL(string: baseURL + path) else {
|
|
626
|
-
throw APIError.invalidURL
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
var request = URLRequest(url: url)
|
|
630
|
-
request.httpMethod = method
|
|
631
|
-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
632
|
-
|
|
633
|
-
if let token = TokenManager.shared.token {
|
|
634
|
-
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if let body = body {
|
|
638
|
-
request.httpBody = try JSONEncoder().encode(body)
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
let (data, response) = try await URLSession.shared.data(for: request)
|
|
642
|
-
|
|
643
|
-
guard let httpResponse = response as? HTTPURLResponse else {
|
|
644
|
-
throw APIError.invalidResponse
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
guard (200...299).contains(httpResponse.statusCode) else {
|
|
648
|
-
throw APIError.httpError(httpResponse.statusCode)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return try JSONDecoder().decode(T.self, from: data)
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
enum APIError: Error {
|
|
656
|
-
case invalidURL
|
|
657
|
-
case invalidResponse
|
|
658
|
-
case httpError(Int)
|
|
659
|
-
}
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
## 数据持久化
|
|
663
|
-
|
|
664
|
-
### UserDefaults
|
|
665
|
-
```swift
|
|
666
|
-
class SettingsManager {
|
|
667
|
-
static let shared = SettingsManager()
|
|
668
|
-
|
|
669
|
-
@UserDefault(key: "isDarkMode", defaultValue: false)
|
|
670
|
-
var isDarkMode: Bool
|
|
671
|
-
|
|
672
|
-
@UserDefault(key: "language", defaultValue: "en")
|
|
673
|
-
var language: String
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
@propertyWrapper
|
|
677
|
-
struct UserDefault<T> {
|
|
678
|
-
let key: String
|
|
679
|
-
let defaultValue: T
|
|
680
|
-
|
|
681
|
-
var wrappedValue: T {
|
|
682
|
-
get {
|
|
683
|
-
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
|
|
684
|
-
}
|
|
685
|
-
set {
|
|
686
|
-
UserDefaults.standard.set(newValue, forKey: key)
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
### Keychain
|
|
693
|
-
```swift
|
|
694
|
-
class KeychainManager {
|
|
695
|
-
static let shared = KeychainManager()
|
|
696
|
-
|
|
697
|
-
func save(_ value: String, forKey key: String) {
|
|
698
|
-
let data = value.data(using: .utf8)!
|
|
699
|
-
let query: [String: Any] = [
|
|
700
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
701
|
-
kSecAttrAccount as String: key,
|
|
702
|
-
kSecValueData as String: data
|
|
703
|
-
]
|
|
704
|
-
|
|
705
|
-
SecItemDelete(query as CFDictionary)
|
|
706
|
-
SecItemAdd(query as CFDictionary, nil)
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
func get(forKey key: String) -> String? {
|
|
710
|
-
let query: [String: Any] = [
|
|
711
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
712
|
-
kSecAttrAccount as String: key,
|
|
713
|
-
kSecReturnData as String: true
|
|
714
|
-
]
|
|
715
|
-
|
|
716
|
-
var result: AnyObject?
|
|
717
|
-
SecItemCopyMatching(query as CFDictionary, &result)
|
|
718
|
-
|
|
719
|
-
guard let data = result as? Data else { return nil }
|
|
720
|
-
return String(data: data, encoding: .utf8)
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
### Core Data
|
|
726
|
-
```swift
|
|
727
|
-
class CoreDataManager {
|
|
728
|
-
static let shared = CoreDataManager()
|
|
729
|
-
|
|
730
|
-
lazy var persistentContainer: NSPersistentContainer = {
|
|
731
|
-
let container = NSPersistentContainer(name: "Model")
|
|
732
|
-
container.loadPersistentStores { _, error in
|
|
733
|
-
if let error = error {
|
|
734
|
-
fatalError("Core Data error: \(error)")
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return container
|
|
738
|
-
}()
|
|
739
|
-
|
|
740
|
-
var context: NSManagedObjectContext {
|
|
741
|
-
persistentContainer.viewContext
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
func save() {
|
|
745
|
-
if context.hasChanges {
|
|
746
|
-
try? context.save()
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// 使用
|
|
752
|
-
let user = User(context: CoreDataManager.shared.context)
|
|
753
|
-
user.name = "John"
|
|
754
|
-
CoreDataManager.shared.save()
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
## 性能优化
|
|
758
|
-
|
|
759
|
-
### LazyVStack
|
|
760
|
-
```swift
|
|
761
|
-
// 大列表优化
|
|
762
|
-
ScrollView {
|
|
763
|
-
LazyVStack {
|
|
764
|
-
ForEach(items) { item in
|
|
765
|
-
ItemRow(item: item)
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
### Task 优先级
|
|
772
|
-
```swift
|
|
773
|
-
Task(priority: .high) {
|
|
774
|
-
await loadCriticalData()
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
Task(priority: .background) {
|
|
778
|
-
await syncData()
|
|
779
|
-
}
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
### Image 缓存
|
|
783
|
-
```swift
|
|
784
|
-
class ImageCache {
|
|
785
|
-
static let shared = ImageCache()
|
|
786
|
-
private var cache = NSCache<NSString, UIImage>()
|
|
787
|
-
|
|
788
|
-
func get(forKey key: String) -> UIImage? {
|
|
789
|
-
cache.object(forKey: key as NSString)
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
func set(_ image: UIImage, forKey key: String) {
|
|
793
|
-
cache.setObject(image, forKey: key as NSString)
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
struct CachedAsyncImage: View {
|
|
798
|
-
let url: URL
|
|
799
|
-
@State private var image: UIImage?
|
|
800
|
-
|
|
801
|
-
var body: some View {
|
|
802
|
-
Group {
|
|
803
|
-
if let image = image {
|
|
804
|
-
Image(uiImage: image)
|
|
805
|
-
.resizable()
|
|
806
|
-
} else {
|
|
807
|
-
ProgressView()
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
.task {
|
|
811
|
-
if let cached = ImageCache.shared.get(forKey: url.absoluteString) {
|
|
812
|
-
image = cached
|
|
813
|
-
} else {
|
|
814
|
-
let (data, _) = try? await URLSession.shared.data(from: url)
|
|
815
|
-
if let data = data, let downloaded = UIImage(data: data) {
|
|
816
|
-
ImageCache.shared.set(downloaded, forKey: url.absoluteString)
|
|
817
|
-
image = downloaded
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
## 测试
|
|
826
|
-
|
|
827
|
-
### Unit Test
|
|
828
|
-
```swift
|
|
829
|
-
import XCTest
|
|
830
|
-
@testable import MyApp
|
|
831
|
-
|
|
832
|
-
class LoginViewModelTests: XCTestCase {
|
|
833
|
-
var viewModel: LoginViewModel!
|
|
834
|
-
var mockRepository: MockUserRepository!
|
|
835
|
-
|
|
836
|
-
override func setUp() {
|
|
837
|
-
mockRepository = MockUserRepository()
|
|
838
|
-
viewModel = LoginViewModel(repository: mockRepository)
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
func testLoginSuccess() async {
|
|
842
|
-
mockRepository.loginResult = .success(LoginResponse(token: "token", user: User(id: 1, name: "Test", email: "test@example.com")))
|
|
843
|
-
|
|
844
|
-
viewModel.username = "test"
|
|
845
|
-
viewModel.password = "password"
|
|
846
|
-
|
|
847
|
-
await viewModel.login()
|
|
848
|
-
|
|
849
|
-
XCTAssertTrue(viewModel.isLoggedIn)
|
|
850
|
-
XCTAssertNil(viewModel.error)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
func testLoginFailure() async {
|
|
854
|
-
mockRepository.loginResult = .failure(APIError.httpError(401))
|
|
855
|
-
|
|
856
|
-
viewModel.username = "test"
|
|
857
|
-
viewModel.password = "wrong"
|
|
858
|
-
|
|
859
|
-
await viewModel.login()
|
|
860
|
-
|
|
861
|
-
XCTAssertFalse(viewModel.isLoggedIn)
|
|
862
|
-
XCTAssertNotNil(viewModel.error)
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
class MockUserRepository: UserRepository {
|
|
867
|
-
var loginResult: Result<LoginResponse, Error>!
|
|
868
|
-
|
|
869
|
-
func login(username: String, password: String) async throws -> LoginResponse {
|
|
870
|
-
try loginResult.get()
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
func fetchProfile() async throws -> User {
|
|
874
|
-
User(id: 1, name: "Test", email: "test@example.com")
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
### UI Test
|
|
880
|
-
```swift
|
|
881
|
-
class LoginUITests: XCTestCase {
|
|
882
|
-
var app: XCUIApplication!
|
|
883
|
-
|
|
884
|
-
override func setUp() {
|
|
885
|
-
app = XCUIApplication()
|
|
886
|
-
app.launch()
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
func testLoginFlow() {
|
|
890
|
-
let usernameField = app.textFields["Username"]
|
|
891
|
-
usernameField.tap()
|
|
892
|
-
usernameField.typeText("testuser")
|
|
893
|
-
|
|
894
|
-
let passwordField = app.secureTextFields["Password"]
|
|
895
|
-
passwordField.tap()
|
|
896
|
-
passwordField.typeText("password123")
|
|
897
|
-
|
|
898
|
-
app.buttons["Login"].tap()
|
|
899
|
-
|
|
900
|
-
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
## 工具清单
|
|
906
|
-
|
|
907
|
-
| 工具 | 用途 |
|
|
908
|
-
|------|------|
|
|
909
|
-
| Xcode | IDE |
|
|
910
|
-
| SwiftLint | 代码规范 |
|
|
911
|
-
| Fastlane | 自动化部署 |
|
|
912
|
-
| CocoaPods | 依赖管理 |
|
|
913
|
-
| Swift Package Manager | 官方依赖管理 |
|
|
914
|
-
| Instruments | 性能分析 |
|
|
915
|
-
| Charles | 网络抓包 |
|
|
916
|
-
| Reveal | UI 调试 |
|
|
917
|
-
|
|
918
|
-
## 最佳实践
|
|
919
|
-
|
|
920
|
-
- ✅ 使用 SwiftUI 优先,UIKit 按需集成
|
|
921
|
-
- ✅ MVVM 架构分离关注点
|
|
922
|
-
- ✅ async/await 替代回调地狱
|
|
923
|
-
- ✅ Combine 处理响应式流
|
|
924
|
-
- ✅ 依赖注入提升可测试性
|
|
925
|
-
- ✅ 使用 @MainActor 确保 UI 线程安全
|
|
926
|
-
- ✅ LazyVStack 优化大列表
|
|
927
|
-
- ✅ 图片缓存减少内存压力
|
|
928
|
-
- ✅ Keychain 存储敏感数据
|
|
929
|
-
- ✅ 单元测试覆盖核心逻辑
|
|
930
|
-
|
|
931
|
-
---
|