Last active
April 5, 2025 07:29
-
-
Save yosshi4486/a4b79333754bfbe9fed7266aaa3910a0 to your computer and use it in GitHub Desktop.
UIKit+VIPERアーキテクチャでiOSアプリを作るClineへの指示(いいかげん)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 引用・参考リスト | |
// @hicka04 [VIPERアーキテクチャ まとめ](https://qiita.com/hicka04/items/09534b5daffec33b2bec#router) | |
// @mizchi [CLINEに全部賭けろ](https://zenn.dev/mizchi/articles/all-in-on-cline) | |
## 重要 | |
ユーザーはあなたよりプログラミングが得意ですが、時短のためにあなたにコーディングを依頼しています。 | |
2回以上連続でテストを失敗した時は、現在の状況を整理して、一緒に解決方法を考えます。 | |
私は GitHub から学習した広範な知識を持っており、個別のアルゴリズムやライブラリの使い方は私が実装するよりも速いでしょう。テストコードを書いて動作確認しながら、ユーザーに説明しながらコードを書きます。 | |
反面、現在のコンテキストに応じた処理は苦手です。コンテキストが不明瞭な時は、ユーザーに確認します。 | |
## 作業開始準備 | |
`git status` で現在の git のコンテキストを確認します。 | |
もし指示された内容と無関係な変更が多い場合、現在の変更からユーザーに別のタスクとして開始するように提案してください。 | |
無視するように言われた場合は、そのまま続行します。 | |
# コーディングプラクティス | |
## 原則 | |
### UIKitを利用 | |
- SwiftUIではなく、UIKitの技術を利用 | |
- Storyboardを使わずコードレイアウトを利用 | |
- レイアウトはAutolayoutのNSLayoutAnchor/NSLayoutConstraintを使って制約を記述 | |
- 一覧要素はUICollectionViewを使って実装 | |
### 値中心設計 | |
- classよりもstructを優先した設計 | |
- 型安全な実装 | |
- ただしUIKit製のオブジェクトはclassで扱う | |
### VIPER | |
- View/Router/Presenter/Interactorのレイヤーに分かれる | |
- それぞれのフォルダがあり、加えてEntityというドメインオブジェクトのフォルダがある | |
- ViewはUIViewとUIViewControllerのレイヤ | |
- Routerは画面遷移とDIのレイヤ | |
- PresenterはViewから入力を受け取って処理を行うレイヤ | |
- Interactorはビジネスロジックを実装するレイヤ | |
### テスト駆動開発 (TDD) | |
- Red-Green-Refactorサイクル | |
- テストを仕様として扱う | |
- 小さな単位で反復 | |
- 継続的なリファクタリング | |
## 実装パターン | |
### 型定義 | |
```swift | |
// 各レイヤは抽象protocolに依存. 依存性逆転の原則(DIP) | |
protocol GithubListPresenter { } | |
``` | |
### View | |
- 「画面の更新」と「 Presenter へのイベント通知」を担当 | |
- 「画面の更新」 | |
- ラベルの文言変更 | |
- UITableView のリロード | |
- …などなど | |
-「 Presenter へのイベント通知」 | |
- ライフサイクル( viewDidLoad() , viewWillAppear() …など) | |
- ボタンのタップ、セルのタップなど | |
- UIView , UIViewController が該当 | |
- 1ViewControllerに対して1protocolを切る | |
```swift: サンプル | |
import UIKit | |
protocol RepositorySearchResultView: AnyObject { | |
func updateRepositories(_ repositories: [RepositoryEntity]) | |
func showErrorAlert() | |
} | |
class GithubSearchResultViewController : UIViewController { | |
let presenter: GithubSearchResultPresentation | |
private var repositories: [RepositoryEntity] = [] { | |
didSet { | |
Task.detached { @MainActor | |
self.tableView.reloadData() // 画面の更新 | |
} | |
} | |
} | |
init(presenter: GithubSearchResultPresentation) { | |
self.presenter = presenter | |
super.init(nibName: nil, bundle: nil) | |
} | |
} | |
// Viewのプロトコルに準拠する | |
extension RepositoryListViewController: RepositoryListView { | |
func updateRepositories(_ repositories: [RepositoryEntity]) { | |
self.repositories = repositories | |
} | |
} | |
extension RepositorySearchResultViewController: UISearchBarDelegate { | |
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { | |
guard let text = searchBar.text else { return } | |
// Presenterにイベント通知 | |
presenter.searchButtonDidPush(searchText: text) | |
searchBar.resignFirstResponder() | |
} | |
} | |
``` | |
## Presenter | |
- View から受け取ったイベントを元に別のクラスに依頼する | |
- View に対して画面の更新を依頼する | |
- Interactor に対してデータの取得を依頼する | |
- Router に対して画面遷移を依頼する | |
- Presenter が提供するメソッド名は | |
- 「画面の更新が終わった( viewDidLoad )」「ボタンが押された( hogeButtonDidPush )」といった命名にすること | |
- ×「(ボタンが押されたので)詳細画面に遷移する(showDetailView)」 | |
- Presenter には状態を持たせない (ハブに徹して、依頼する) | |
- 画面表示に必要な状態は View に持たせるなど、適切な場所で状態管理をすべき | |
- Presenter に状態を持たせると Presenter のテストコードが書きづらくなる | |
- import UIKit 禁止 | |
- UIがどうなっているかを気にしない | |
```swift: サンプル | |
import Foundation | |
protocol RepositorySearchResultPresentation: AnyObject { | |
func searchButtonDidPush(searchText: String) | |
func didSelect(repository: RepositoryEntity) | |
} | |
class RepositorySearchResultPresenter { | |
// View, Interactor, Routerへのアクセスはprotocolを介して行う | |
// Viewは循環参照にならないよう`weak`プロパティ | |
private weak var view: RepositoryListView? | |
private let router: RepositoryListWireframe | |
private let searchRepositoryInteractor: SearchRepositoryUsecase | |
init(view: RepositorySearchResultView, | |
router: RepositorySearchResultWireframe, | |
searchRepositoryInteractor: SearchRepositoryUsecase) { | |
self.view = view | |
self.router = router | |
self.searchRepositoryInteractor = searchRepositoryInteractor | |
} | |
} | |
// Presenterのプロトコルに準拠する | |
extension RepositorySearchResultPresenter: RepositorySearchResultPresentation { | |
func searchButtonDidPush(searchText: String) { | |
guard !searchText.isEmpty else { return } | |
// Interactorにデータ取得処理を依頼 | |
// `@escaping`がついているクロージャの場合は循環参照にならないよう`[weak self]`でキャプチャ | |
searchRepositoryInteractor.fetchRepositories(keyword: searchText) { [weak self] result in | |
switch result { | |
case .success(let repositories): | |
self?.view?.updateRepositories(repositories) | |
case .failure: | |
self?.view?.showErrorAlert() | |
} | |
} | |
} | |
func didSelect(repository: RepositoryEntity) { | |
router.showRepositoryDetail(repository) | |
} | |
} | |
``` | |
### Interactor | |
-「ビジネスロジック」を担当 | |
- Presenter から依頼されたビジネスロジックを実施し、結果を返す | |
- 非同期で結果を返すのがおすすめ (UnitテストのためのMock作成が楽になる) | |
- クロージャ | |
- Delegate | |
- 循環参照にならないよう実装注意 | |
- import UIKit 禁止 | |
- UIがどうなっているかを気にしない | |
```swift: サンプル | |
import Foundation | |
protocol SearchRepositoryUsecase: AnyObject { | |
func fetchRepositories(keyword: String, | |
completion: @escaping (Result<[RepositoryEntity], Error>) -> Void) | |
} | |
final class SearchRepositoryInteractor { | |
// GitHubに問い合わせるためのAPIクライアント | |
// Interactorのテスト時にAPIクライアントをMockに差し替えて任意のレスポンスを返すようにするため | |
private let client: GitHubRequestable | |
init(client: GitHubRequestable = GitHubClient()) { | |
self.client = client | |
} | |
} | |
// Interactorのプロトコルに準拠する | |
extension SearchRepositoryInteractor: SearchRepositoryUsecase { | |
func fetchRepositories(keyword: String, | |
completion: @escaping (Result<[RepositoryEntity], Error>) -> Void) { | |
let request = GitHubAPI.SearchRepositories(keyword: keyword) | |
client.send(request: request) { result in | |
completion(result.map { $0.items }) | |
} | |
} | |
} | |
``` | |
### Entity | |
- IDに基づく同一性 | |
-「データ構造の定義」を担当 | |
- structでデータ構造を定義する | |
- import UIKit 禁止 | |
- UIがどうなっているかを気にしない | |
```swift: サンプル | |
「データ構造の定義」を担当 | |
structでデータ構造を定義する | |
import UIKit 禁止 | |
UIがどうなっているかを気にしない | |
基本的にプロパティとイニシャライザのみ定義し、ロジックを持たないようにする | |
``` | |
## プラクティス | |
- 小さく始めて段階的に拡張 | |
- 過度な抽象化を避ける | |
- コードよりも型を重視 | |
- 複雑さに応じてアプローチを調整 | |
## テスト戦略 | |
- 純粋関数の単体テストを優先 | |
- インメモリ実装によるリポジトリテスト | |
- テスト可能性を設計に組み込む | |
- アサートファースト:期待結果から逆算 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment