mirror of
https://github.com/efremidze/swift-architecture-skill.git
synced 2026-09-14 20:49:06 +08:00
4b97007864
Co-authored-by: Claude <noreply@anthropic.com>
369 lines
20 KiB
JSON
369 lines
20 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"
|
|
]
|
|
},
|
|
{
|
|
"id": "recommendation-simple-settings-tca-overkill",
|
|
"notes": "Expected recommendation: explicit TCA request should be fit-checked and likely marked mismatch/overkill for a tiny settings screen unless the app already uses TCA.\nExpected direction: recommend MVVM as closest fit, while noting TCA is acceptable if existing codebase convention requires it.\n",
|
|
"prompt": "Use `swift-architecture-skill` to recommend an architecture.\n\nContext:\n- New SwiftUI settings screen\n- Two toggles and one Save button\n- No child features, no async effects beyond saving preferences\n- Team has not adopted TCA yet and does not want new dependencies unless needed\n\nRequest:\nWe planned to use TCA. Is that the right call? Give a fit result and a practical alternative if it is overkill.\n",
|
|
"required_any": [
|
|
[
|
|
"mismatch",
|
|
"overkill",
|
|
"not the best fit"
|
|
],
|
|
[
|
|
"MVVM",
|
|
"mvvm"
|
|
],
|
|
[
|
|
"dependency",
|
|
"new dependency",
|
|
"framework dependency"
|
|
],
|
|
[
|
|
"fit result",
|
|
"fit:"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"use TCA because it was requested",
|
|
"no trade-off",
|
|
"always use TCA"
|
|
]
|
|
},
|
|
{
|
|
"id": "recommendation-live-feed-persistence-combination",
|
|
"notes": "Expected recommendation: combine patterns with explicit ownership, such as Clean Architecture for domain/data boundaries plus Reactive or MVVM/Reactive for stream presentation.\nExpected direction: identify primary and secondary playbooks rather than choosing Reactive alone.\n",
|
|
"prompt": "Use `swift-architecture-skill` to recommend architecture for this feature.\n\nContext:\n- SwiftUI live activity feed\n- WebSocket updates, offline cache, pagination, and pull-to-refresh\n- Data layer must be replaceable because the backend API is unstable\n- Team is comfortable with MVVM and Combine, but not TCA\n\nRequest:\nChoose the architecture and explain which playbook owns presentation, streams, and data boundaries.\n",
|
|
"required_any": [
|
|
[
|
|
"Clean Architecture",
|
|
"clean architecture"
|
|
],
|
|
[
|
|
"Reactive",
|
|
"Combine",
|
|
"stream"
|
|
],
|
|
[
|
|
"primary",
|
|
"secondary",
|
|
"owns"
|
|
],
|
|
[
|
|
"repository",
|
|
"data boundary",
|
|
"use case"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"Reactive alone",
|
|
"only use Reactive",
|
|
"TCA is required"
|
|
]
|
|
},
|
|
{
|
|
"id": "recommendation-navigation-heavy-mvvm-coordinator",
|
|
"notes": "Expected recommendation: preserve MVVM for screen state and add Coordinator as a secondary navigation concern.\nExpected direction: avoid replacing MVVM wholesale when the main smell is navigation ownership.\n",
|
|
"prompt": "Use `swift-architecture-skill` to recommend a refactor path.\n\nContext:\n- Existing mixed UIKit/SwiftUI app uses MVVM successfully for screen state\n- The main pain is deep links and reusable onboarding flows\n- ViewModels currently contain push/present calls\n- Team wants minimal churn\n\nRequest:\nShould we replace MVVM, or combine it with another pattern? Include the selected reference files and migration path.\n",
|
|
"required_any": [
|
|
[
|
|
"MVVM",
|
|
"mvvm"
|
|
],
|
|
[
|
|
"Coordinator",
|
|
"coordinator"
|
|
],
|
|
[
|
|
"primary",
|
|
"secondary",
|
|
"navigation concern"
|
|
],
|
|
[
|
|
"migration",
|
|
"incremental",
|
|
"minimal churn"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"replace MVVM entirely",
|
|
"rewrite everything",
|
|
"switch to VIPER"
|
|
]
|
|
},
|
|
{
|
|
"id": "recommendation-user-insists-viper-swiftui",
|
|
"notes": "Expected recommendation: validate the explicit VIPER request, mark mismatch for SwiftUI-first feature, recommend MVVM/MVI/TCA depending on constraints, and proceed only with risks if user insists.\n",
|
|
"prompt": "Use `swift-architecture-skill` to fit-check a requested architecture.\n\nContext:\n- New SwiftUI-only checkout screen\n- Moderate form state and async validation\n- No UIKit module boundaries\n- Team requested VIPER because they used it years ago\n\nRequest:\nIs VIPER a good fit here? If not, recommend the closest alternative and explain the risk if we insist on VIPER.\n",
|
|
"required_any": [
|
|
[
|
|
"mismatch",
|
|
"poor fit",
|
|
"not a good fit"
|
|
],
|
|
[
|
|
"SwiftUI",
|
|
"swiftui"
|
|
],
|
|
[
|
|
"MVVM",
|
|
"MVI",
|
|
"TCA"
|
|
],
|
|
[
|
|
"risk",
|
|
"boilerplate",
|
|
"UIKit-native"
|
|
]
|
|
],
|
|
"forbidden_keywords": [
|
|
"VIPER is an excellent fit",
|
|
"no risk",
|
|
"use VIPER without caveats"
|
|
]
|
|
}
|
|
]
|
|
}
|