-
Notifications
You must be signed in to change notification settings - Fork 0
[#570] TodoDetailView에 TCA를 적용한다 #588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
0b0ab8b
feat: TodoDetailFeature 리듀서 추가
opficdev fb64f75
refactor: TodoDetailViewModel을 TodoDetailFeature로 대체
opficdev 16424fd
refactor: TodoDetail Store 조립 방식 정리
opficdev 00b46c1
refactor: TodoDetail 시트 Store 스코프 정리
opficdev e3c80b5
refactor: TodoDetail 시트 상태 enum 정리
opficdev fcd4729
refactor: Bindable 적용
opficdev 41fae5f
chore: 폴더링 수정
opficdev 4cad673
refactor: TodoDetail 로딩 상태를 LoadingFeature로 분리
opficdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
223 changes: 223 additions & 0 deletions
223
Application/DevLogPresentation/Sources/Home/Detail/TodoDetailFeature.swift
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| // | ||
| // TodoDetailFeature.swift | ||
| // DevLogPresentation | ||
| // | ||
| // Created by opfic on 6/11/26. | ||
| // | ||
|
|
||
| import ComposableArchitecture | ||
| import DevLogDomain | ||
| import Foundation | ||
|
|
||
| @Reducer | ||
| struct TodoDetailFeature { | ||
| @ObservableState | ||
| struct State: Equatable { | ||
| @Presents var alert: AlertState<Never>? | ||
| @Presents var sheet: SheetState? | ||
| @Presents var fullScreenCover: FullScreenCoverState? | ||
| var todoId: String | ||
| var showEditButton: Bool | ||
| var todo: Todo? | ||
| var referenceItems: [Int: TodoReferenceItem] = [:] | ||
| var isLoading = false | ||
| } | ||
|
|
||
| @ObservableState | ||
| @CasePathable | ||
| enum SheetState: Equatable { | ||
| case info | ||
| case todo(TodoDetailFeature.State) | ||
|
|
||
| var todoDetail: TodoDetailFeature.State? { | ||
| get { | ||
| guard case .todo(let state) = self else { return nil } | ||
| return state | ||
| } | ||
| set { | ||
| guard let newValue else { return } | ||
| self = .todo(newValue) | ||
| } | ||
| } | ||
|
|
||
| static func todo(_ todoId: TodoIdItem) -> Self { | ||
| .todo( | ||
| TodoDetailFeature.State( | ||
| todoId: todoId.id, | ||
| showEditButton: false | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @ObservableState | ||
| struct FullScreenCoverState: Equatable { | ||
| var destination: Destination | ||
|
|
||
| enum Destination: Equatable { | ||
| case editor | ||
| } | ||
|
|
||
| static let editor = Self(destination: .editor) | ||
| } | ||
|
|
||
| enum Action { | ||
| case alert(PresentationAction<Never>) | ||
| case sheet(PresentationAction<Sheet>) | ||
| case fullScreenCover(PresentationAction<Never>) | ||
| case onAppear | ||
| case fetchFailed | ||
| case setSheet(SheetState?) | ||
| case setFullScreenCover(FullScreenCoverState?) | ||
| case setTodo(Todo) | ||
| case setReferenceItems([Int: TodoReferenceItem]) | ||
| case setLoading(Bool) | ||
|
|
||
| @CasePathable | ||
| enum Sheet { | ||
| case tapCloseButton | ||
| case todo(TodoDetailFeature.Action) | ||
| } | ||
| } | ||
|
|
||
| @Dependency(\.fetchTodoByIdUseCase) var fetchTodoUseCase | ||
| @Dependency(\.fetchReferenceItemsUseCase) var fetchReferenceItemsUseCase | ||
| private let loadingState = LoadingState() | ||
|
opficdev marked this conversation as resolved.
Outdated
|
||
|
|
||
| var body: some ReducerOf<Self> { | ||
| Reduce { state, action in | ||
| switch action { | ||
| case .sheet(.dismiss): | ||
| state.sheet = nil | ||
| case .alert: | ||
| break | ||
| case .sheet(.presented(.tapCloseButton)): | ||
| state.sheet = nil | ||
| case .sheet: | ||
| break | ||
| case .fullScreenCover(.dismiss): | ||
| state.fullScreenCover = nil | ||
| case .fullScreenCover: | ||
| break | ||
| case .onAppear: | ||
| return fetchTodoEffect(todoId: state.todoId) | ||
|
opficdev marked this conversation as resolved.
|
||
| case .fetchFailed: | ||
| state.alert = alertState() | ||
| case .setSheet(let sheet): | ||
| state.sheet = sheet | ||
| case .setFullScreenCover(let cover): | ||
| state.fullScreenCover = cover | ||
| case .setTodo(let todo): | ||
| state.todo = todo | ||
| state.referenceItems = [:] | ||
| return resolveMarkdownEffect(content: todo.content) | ||
| case .setReferenceItems(let items): | ||
| state.referenceItems = items | ||
| case .setLoading(let value): | ||
| state.isLoading = value | ||
| } | ||
|
|
||
| return .none | ||
| } | ||
| .ifLet(\.$alert, action: \.alert) | ||
| .ifLet(\.$sheet, action: \.sheet) { | ||
| TodoDetailSheetFeature() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private struct TodoDetailSheetFeature: Reducer { | ||
| typealias State = TodoDetailFeature.SheetState | ||
| typealias Action = TodoDetailFeature.Action.Sheet | ||
|
|
||
| var body: some ReducerOf<Self> { | ||
| EmptyReducer() | ||
| .ifCaseLet(\.todo, action: \.todo) { | ||
| TodoDetailFeature() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension DependencyValues { | ||
| var fetchTodoByIdUseCase: FetchTodoByIdUseCase { | ||
| get { self[FetchTodoByIdUseCaseKey.self] } | ||
| set { self[FetchTodoByIdUseCaseKey.self] = newValue } | ||
| } | ||
|
|
||
| var fetchReferenceItemsUseCase: FetchReferenceItemsUseCase { | ||
| get { self[FetchReferenceItemsUseCaseKey.self] } | ||
| set { self[FetchReferenceItemsUseCaseKey.self] = newValue } | ||
| } | ||
| } | ||
|
|
||
| private enum FetchTodoByIdUseCaseKey: DependencyKey { | ||
| static var liveValue: FetchTodoByIdUseCase { | ||
| preconditionFailure("FetchTodoByIdUseCase must be provided.") | ||
| } | ||
|
|
||
| static var testValue: FetchTodoByIdUseCase { | ||
| liveValue | ||
| } | ||
| } | ||
|
|
||
| private enum FetchReferenceItemsUseCaseKey: DependencyKey { | ||
| static var liveValue: FetchReferenceItemsUseCase { | ||
| preconditionFailure("FetchReferenceItemsUseCase must be provided.") | ||
| } | ||
|
|
||
| static var testValue: FetchReferenceItemsUseCase { | ||
| liveValue | ||
| } | ||
| } | ||
|
|
||
| private extension TodoDetailFeature { | ||
| func fetchTodoEffect(todoId: String) -> Effect<Action> { | ||
| .run { [fetchTodoUseCase, loadingState] send in | ||
| await loadingState.begin(mode: .delayed) { isLoading in | ||
| send(.setLoading(isLoading)) | ||
| } | ||
| do { | ||
| let todo = try await fetchTodoUseCase.execute(todoId) | ||
| await loadingState.end(mode: .delayed) { isLoading in | ||
| send(.setLoading(isLoading)) | ||
| } | ||
| await send(.setTodo(todo)) | ||
| } catch { | ||
| await loadingState.end(mode: .delayed) { isLoading in | ||
| send(.setLoading(isLoading)) | ||
| } | ||
| await send(.fetchFailed) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func resolveMarkdownEffect(content: String) -> Effect<Action> { | ||
| .run { [fetchReferenceItemsUseCase] send in | ||
| let numbers = content.todoReferenceNumbers | ||
| var referenceItems = [Int: TodoReferenceItem]() | ||
|
|
||
| if !numbers.isEmpty { | ||
| do { | ||
| referenceItems = try await fetchReferenceItemsUseCase.execute(numbers) | ||
| .mapValues(TodoReferenceItem.init(from:)) | ||
| } catch { | ||
| referenceItems = [:] | ||
| } | ||
| } | ||
|
|
||
| await send(.setReferenceItems(referenceItems)) | ||
| } | ||
| } | ||
|
|
||
| func alertState() -> AlertState<Never> { | ||
| AlertState { | ||
| TextState(String(localized: "common_error_title")) | ||
| } actions: { | ||
| ButtonState(role: .cancel) { | ||
| TextState(String(localized: "common_close")) | ||
| } | ||
| } message: { | ||
| TextState(String(localized: "common_error_message")) | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.