mirror of
https://github.com/efremidze/swift-architecture-skill.git
synced 2026-09-14 20:49:06 +08:00
247 lines
15 KiB
JSON
247 lines
15 KiB
JSON
{
|
|
"version": 1,
|
|
"cases": [
|
|
{
|
|
"id": "clean-domain-ui-import",
|
|
"notes": "Expected smell: domain use case imports SwiftUI type.\nExpected direction: return plain domain/presentation data and map to UI in presentation layer.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this Clean Architecture snippet.\n\nContext:\n- Team wants strict dependency rule\n- Domain layer should not depend on UI frameworks\n\n```swift\nimport Foundation\nimport SwiftUI\n\nstruct OrderSummaryUseCase {\n func makeBadgeText(total: Double) -> Text {\n Text(\"Total: \\(total)\")\n }\n}\n```\n\nRequest:\n1. Identify the dependency issue.\n2. Provide a boundary-correct fix.\n3. Keep recommendation practical for a staged refactor.\n",
|
|
"required_keywords": [
|
|
"domain",
|
|
"swiftui",
|
|
"dependency rule",
|
|
"presentation"
|
|
],
|
|
"forbidden_keywords": [
|
|
"this is clean architecture",
|
|
"text in usecase is fine"
|
|
]
|
|
},
|
|
{
|
|
"id": "mvvm-uikit-navigation",
|
|
"notes": "Expected smell: ViewModel directly owns UIKit navigation objects.\nExpected direction: move navigation execution to coordinator/router while keeping ViewModel value-oriented.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this code and suggest architecture fixes.\n\nContext:\n- Existing pattern: MVVM\n- UI stack: mixed SwiftUI/UIKit\n- Goal: keep ViewModel testable and framework-independent\n\n```swift\nimport Foundation\nimport UIKit\n\n@MainActor\nfinal class FeedViewModel {\n private let api: FeedAPI\n private weak var navigationController: UINavigationController?\n\n init(api: FeedAPI, navigationController: UINavigationController?) {\n self.api = api\n self.navigationController = navigationController\n }\n\n func didTapCompose() {\n let compose = ComposeViewController()\n navigationController?.pushViewController(compose, animated: true)\n }\n}\n```\n\nRequest:\n1. Identify architecture smells.\n2. Recommend an MVVM-consistent fix.\n3. Show a small refactor plan (2-4 steps).\n",
|
|
"required_keywords": [
|
|
"uikit",
|
|
"viewmodel",
|
|
"coordinator",
|
|
"navigation"
|
|
],
|
|
"forbidden_keywords": [
|
|
"no issues",
|
|
"looks good",
|
|
"keep as is"
|
|
]
|
|
},
|
|
{
|
|
"id": "reactive-nested-subscriptions",
|
|
"notes": "Expected smell: subscribe-inside-subscribe with weak cancellation semantics.\nExpected direction: compose operators (`map` + `switchToLatest`) and keep lifecycle-bound cancellation.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this reactive implementation.\n\nContext:\n- Search feature with Combine\n- Requirement: replace stale requests, avoid leaks\n\n```swift\nimport Combine\n\nfinal class SearchPresenter {\n private let query = PassthroughSubject<String, Never>()\n private var cancellables = Set<AnyCancellable>()\n\n init(service: SearchService) {\n query.sink { value in\n service.search(value).sink(\n receiveCompletion: { _ in },\n receiveValue: { results in\n print(results)\n }\n ).store(in: &self.cancellables)\n }\n .store(in: &cancellables)\n }\n}\n```\n\nRequest:\n1. Identify architecture/reactive smells.\n2. Recommend a better stream composition pattern.\n3. Mention cancellation behavior.\n",
|
|
"required_keywords": [
|
|
"nested subscription",
|
|
"switchtolatest",
|
|
"cancellation",
|
|
"lifecycle"
|
|
],
|
|
"required_any": [
|
|
[
|
|
"nested subscription",
|
|
"nested sink",
|
|
"subscribe-inside-subscribe",
|
|
"subscribe inside subscribe"
|
|
],
|
|
[
|
|
"switchtolatest",
|
|
"switchToLatest",
|
|
"switch to latest"
|
|
],
|
|
[
|
|
"cancellation",
|
|
"cancel"
|
|
],
|
|
[
|
|
"lifecycle",
|
|
"deinit",
|
|
"dealloc",
|
|
"teardown"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"no problem",
|
|
"nested sink is fine"
|
|
]
|
|
},
|
|
{
|
|
"id": "swiftui-view-does-network",
|
|
"notes": "Expected smell: view performs data fetching directly and bypasses DI.\nExpected direction: move effects to ViewModel/service boundary with explicit error/cancel behavior.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this SwiftUI feature.\n\nContext:\n- Team wants MVVM boundaries\n- App uses async/await and dependency injection\n\n```swift\nimport SwiftUI\n\nstruct ProfileView: View {\n @State private var name = \"\"\n\n var body: some View {\n VStack {\n Text(name)\n Button(\"Load\") {\n Task {\n let user = try? await URLSession.shared\n .data(from: URL(string: \"https://example.com/user\")!)\n name = \"Loaded\"\n }\n }\n }\n }\n}\n```\n\nRequest:\n1. Identify smells.\n2. Propose a better architecture boundary.\n3. Mention async error-handling and cancellation expectations.\n",
|
|
"required_keywords": [
|
|
"viewmodel",
|
|
"dependency injection",
|
|
"error",
|
|
"cancellation"
|
|
],
|
|
"forbidden_keywords": [
|
|
"urlsession.shared is fine",
|
|
"keep network calls in view",
|
|
"no change needed"
|
|
]
|
|
},
|
|
{
|
|
"id": "tca-singleton-dependency",
|
|
"notes": "Expected smell: reducer calling global singleton directly.\nExpected direction: inject analytics via TCA dependency system for testability.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this TCA reducer.\n\nContext:\n- Existing architecture: TCA\n- Testing requirement: deterministic TestStore tests\n\n```swift\nimport ComposableArchitecture\n\n@Reducer\nstruct SettingsFeature {\n @ObservableState\n struct State: Equatable {\n var isEnabled = false\n }\n\n enum Action: Equatable {\n case toggleChanged(Bool)\n }\n\n var body: some ReducerOf<Self> {\n Reduce { state, action in\n switch action {\n case let .toggleChanged(value):\n state.isEnabled = value\n Analytics.shared.track(\"toggle\", [\"value\": value])\n return .none\n }\n }\n }\n}\n```\n\nRequest:\n1. Identify architectural smell(s).\n2. Propose the TCA-correct dependency approach.\n3. Explain testing impact.\n",
|
|
"required_keywords": [
|
|
"singleton",
|
|
"@dependency",
|
|
"teststore",
|
|
"side effect"
|
|
],
|
|
"forbidden_keywords": [
|
|
"analytics.shared is acceptable",
|
|
"no architecture issue"
|
|
]
|
|
},
|
|
{
|
|
"id": "mvp-strong-view-reference",
|
|
"notes": "Expected smell: Presenter holds a strong reference to the View protocol, creating a retain cycle when the ViewController owns the Presenter.\nExpected direction: view reference must be weak; the View protocol must be class-constrained.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this MVP module.\n\nContext:\n- UIKit codebase migrating from MVC\n- Goal: testable Presenter with a fully passive View\n\n```swift\nimport UIKit\n\nprotocol ProfileView {\n func showName(_ name: String)\n func showError(_ message: String)\n}\n\nfinal class ProfilePresenter {\n var view: ProfileView\n private let service: UserService\n\n init(view: ProfileView, service: UserService) {\n self.view = view\n self.service = service\n }\n\n func load() async {\n do {\n let user = try await service.fetchUser()\n view.showName(user.name)\n } catch {\n view.showError(error.localizedDescription)\n }\n }\n}\n\nfinal class ProfileViewController: UIViewController, ProfileView {\n private let presenter: ProfilePresenter\n\n init(service: UserService) {\n self.presenter = ProfilePresenter(view: self, service: service)\n super.init(nibName: nil, bundle: nil)\n }\n\n required init?(coder: NSCoder) { fatalError() }\n\n func showName(_ name: String) {}\n func showError(_ message: String) {}\n}\n```\n\nRequest:\n1. Identify the memory management smell.\n2. Recommend the correct MVP ownership and reference pattern.\n3. Show the minimal fix.\n",
|
|
"required_keywords": [
|
|
"retain cycle",
|
|
"weak",
|
|
"anyobject",
|
|
"presenter"
|
|
],
|
|
"required_any": [
|
|
[
|
|
"retain cycle",
|
|
"strong cycle",
|
|
"reference cycle",
|
|
"circular reference"
|
|
],
|
|
[
|
|
"weak",
|
|
"weak var",
|
|
"weakly"
|
|
],
|
|
[
|
|
"anyobject",
|
|
"class-constrained",
|
|
"class constraint"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"no issue",
|
|
"this is fine",
|
|
"strong is acceptable"
|
|
]
|
|
},
|
|
{
|
|
"id": "mvi-missing-request-id",
|
|
"notes": "Expected smell: Store dispatches async effects without tracking a request ID, so stale responses can overwrite newer state.\nExpected direction: add a latestRequestID counter to State and guard reducer callbacks so only the most recent response is applied.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this MVI store.\n\nContext:\n- SwiftUI feature with a search input\n- Users can trigger rapid sequential loads\n- Goal: deterministic unidirectional state, no stale overwrites\n\n```swift\nimport Foundation\n\nenum SearchAction {\n case search(String)\n case loaded([String])\n case failed(Error)\n}\n\nstruct SearchState {\n var results: [String] = []\n var isLoading = false\n var error: Error? = nil\n}\n\n@MainActor\nfinal class SearchStore: ObservableObject {\n @Published private(set) var state = SearchState()\n private let service: SearchService\n\n init(service: SearchService) {\n self.service = service\n }\n\n func send(_ action: SearchAction) {\n switch action {\n case let .search(query):\n state.isLoading = true\n state.error = nil\n Task {\n do {\n let results = try await service.search(query)\n send(.loaded(results))\n } catch {\n send(.failed(error))\n }\n }\n case let .loaded(results):\n state.isLoading = false\n state.results = results\n case let .failed(error):\n state.isLoading = false\n state.error = error\n }\n }\n}\n```\n\nRequest:\n1. Identify the concurrency smell.\n2. Explain why rapid sequential searches can corrupt state.\n3. Recommend the MVI-correct fix with a request ID guard.\n",
|
|
"required_keywords": [
|
|
"stale",
|
|
"request id",
|
|
"latestRequestID",
|
|
"cancellation"
|
|
],
|
|
"required_any": [
|
|
[
|
|
"stale",
|
|
"stale response",
|
|
"out-of-order",
|
|
"race"
|
|
],
|
|
[
|
|
"request id",
|
|
"requestID",
|
|
"latestRequestID",
|
|
"generation counter"
|
|
],
|
|
[
|
|
"cancellation",
|
|
"cancel",
|
|
"task cancellation"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"no issue",
|
|
"this is safe",
|
|
"task order is guaranteed"
|
|
]
|
|
},
|
|
{
|
|
"id": "coordinator-navigation-in-viewcontroller",
|
|
"notes": "Expected smell: ViewController owns and executes navigation transitions directly, coupling screen logic to flow logic.\nExpected direction: extract navigation responsibility to a Coordinator; ViewController delegates via a protocol or closure.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this UIKit navigation pattern.\n\nContext:\n- Multi-screen UIKit app\n- Goal: reusable flows, deep-link support, testable routing\n\n```swift\nimport UIKit\n\nfinal class FeedViewController: UIViewController {\n private let service: FeedService\n private var items: [FeedItem] = []\n\n init(service: FeedService) {\n self.service = service\n super.init(nibName: nil, bundle: nil)\n }\n\n required init?(coder: NSCoder) { fatalError() }\n\n func didSelectItem(_ item: FeedItem) {\n let detail = DetailViewController(item: item)\n navigationController?.pushViewController(detail, animated: true)\n }\n\n func didTapCompose() {\n let compose = ComposeViewController(service: service)\n let nav = UINavigationController(rootViewController: compose)\n present(nav, animated: true)\n }\n\n func didTapProfile() {\n let profile = ProfileViewController()\n navigationController?.pushViewController(profile, animated: true)\n }\n}\n```\n\nRequest:\n1. Identify the architecture smell.\n2. Explain why this blocks reuse and deep linking.\n3. Show how a Coordinator would own these transitions instead.\n",
|
|
"required_keywords": [
|
|
"coordinator",
|
|
"navigation",
|
|
"decouple",
|
|
"deep link"
|
|
],
|
|
"required_any": [
|
|
[
|
|
"coordinator",
|
|
"flow coordinator",
|
|
"app coordinator"
|
|
],
|
|
[
|
|
"decouple",
|
|
"decoupled",
|
|
"separation"
|
|
],
|
|
[
|
|
"deep link",
|
|
"deep linking",
|
|
"reusable flow"
|
|
],
|
|
[
|
|
"delegate",
|
|
"closure",
|
|
"callback"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"no issue",
|
|
"navigationcontroller in viewcontroller is fine",
|
|
"this is standard"
|
|
]
|
|
},
|
|
{
|
|
"id": "viper-retain-cycle",
|
|
"notes": "Expected smell: strong bidirectional references in module wiring.\nExpected direction: apply weak references and explicit ownership boundaries.\n",
|
|
"prompt": "Use `swift-architecture-skill` to review this VIPER wiring.\n\nContext:\n- Large UIKit codebase\n- Concern: memory leaks during repeated navigation\n\n```swift\nprotocol ProfileView: AnyObject {}\n\nfinal class ProfilePresenter {\n var view: ProfileView?\n let router: ProfileRouter\n\n init(router: ProfileRouter) {\n self.router = router\n }\n}\n\nfinal class ProfileRouter {\n var presenter: ProfilePresenter?\n}\n\nfunc build() {\n let router = ProfileRouter()\n let presenter = ProfilePresenter(router: router)\n router.presenter = presenter\n}\n```\n\nRequest:\n1. Identify architectural smell(s).\n2. Recommend VIPER-correct ownership rules.\n3. Provide concrete fix guidance.\n",
|
|
"required_keywords": [
|
|
"retain cycle",
|
|
"weak",
|
|
"ownership",
|
|
"router"
|
|
],
|
|
"required_any": [
|
|
[
|
|
"retain cycle",
|
|
"strong cycle",
|
|
"reference cycle"
|
|
],
|
|
[
|
|
"weak",
|
|
"weakly"
|
|
],
|
|
[
|
|
"ownership",
|
|
"owns",
|
|
"ownership rules"
|
|
],
|
|
[
|
|
"router"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"no leak",
|
|
"this is fine"
|
|
]
|
|
}
|
|
]
|
|
}
|