Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ final class DomainAssembler: Assembler {
container.register(UpdatePushSettingsUseCase.self) {
UpdatePushSettingsUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(FetchTodosByKindUseCase.self) {
FetchTodosByKindUseCaseImpl(container.resolve(TodoRepository.self))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// FetchTodosByKindUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/1/26.
//

protocol FetchTodosByKindUseCase {
var repository: TodoRepository { get }
func execute(_ kind: TodoKind) async throws -> [Todo]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// FetchTodosByKindUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/1/26.
//

final class FetchTodosByKindUseCaseImpl: FetchTodosByKindUseCase {
let repository: TodoRepository

init(_ repository: TodoRepository) {
self.repository = repository
}

func execute(_ kind: TodoKind) async throws -> [Todo] {
return try await repository.fetchTodos(kind)
}
}
43 changes: 34 additions & 9 deletions DevLog/Presentation/ViewModel/TodoViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ final class TodoViewModel: Store {
struct State {
var todos: [Todo] = []
var searchText: String = ""
var kind: TodoKind
let kind: TodoKind
var showEditor: Bool = false
var showToast: Bool = false
var toastMessage: String = ""
var showAlert: Bool = false
var alertMessage: String = ""
var scope: TodoScope = .title
var filterOption: FilterOption = .create
var isLoading = false
Expand All @@ -37,12 +37,14 @@ final class TodoViewModel: Store {
// Binding
case openEditor
case closeEditor
case closeToast
case closeAlert
case setScope(TodoScope)
case setSearchText(String)

// Call from run
case didFetchTodos([Todo])
case didLoading(Bool)
case didShowAlert(String)
case didTogglePinned(Todo)
}

Expand All @@ -53,13 +55,16 @@ final class TodoViewModel: Store {
case swipeTodo(Todo)
}

private let fetchTodosByKindUseCase: FetchTodosByKindUseCase
private let upsertTodoUseCase: UpsertTodoUseCase
@Published private(set) var state: State

init(
fetchTodosByKindUseCase: FetchTodosByKindUseCase,
upsertTodoUseCase: UpsertTodoUseCase,
kind: TodoKind
) {
self.fetchTodosByKindUseCase = fetchTodosByKindUseCase
self.upsertTodoUseCase = upsertTodoUseCase
self.state = State(kind: kind)
}
Expand All @@ -80,14 +85,19 @@ final class TodoViewModel: Store {
state.showEditor = true
case .closeEditor:
state.showEditor = false
case .closeToast:
state.showToast = false
case .closeAlert:
state.showAlert = false
case .setScope(let scope):
state.scope = scope
case .setSearchText(let text):
state.searchText = text
case .didFetchTodos(let todos):
state.todos = todos
case .didLoading(let value):
state.isLoading = value
case .didShowAlert(let message):
state.alertMessage = message
state.showAlert = true
case .didTogglePinned(let todo):
if let index = state.todos.firstIndex(where: { $0.id == todo.id }) {
state.todos[index] = todo
Expand All @@ -99,11 +109,26 @@ final class TodoViewModel: Store {
func run(_ effect: SideEffect) {
switch effect {
case .fetchTodos:
break
Task {
do {
defer { send(.didLoading(false)) }
send(.didLoading(true))
let todos = try await fetchTodosByKindUseCase.execute(state.kind)
send(.didFetchTodos(todos))
} catch {
send(.didShowAlert(error.localizedDescription))
}
}
case .upsertTodo(let todo):
Task {
try await upsertTodoUseCase.execute(todo)
send(.refresh)
do {
defer { send(.didLoading(false)) }
send(.didLoading(true))
try await upsertTodoUseCase.execute(todo)
send(.refresh)
} catch {
send(.didShowAlert(error.localizedDescription))
}
}
case .togglePinned(let todo):
break
Expand Down
3 changes: 3 additions & 0 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@
},
"베타 테스트 참여" : {

},
"불러오기 실패" : {

},
"사용자 설정" : {

Expand Down
3 changes: 2 additions & 1 deletion DevLog/UI/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ struct HomeView: View {
.navigationDestination(for: Path.self) { path in
switch path {
case .kind(let todoKind):
TodoView(viewModel: TodoViewModel(
TodoView(viewModel:TodoViewModel(
fetchTodosByKindUseCase: container.resolve(FetchTodosByKindUseCase.self),
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
kind: todoKind
))
Expand Down
127 changes: 68 additions & 59 deletions DevLog/UI/Home/TodoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,70 +12,86 @@ struct TodoView: View {
@EnvironmentObject var router: NavigationRouter

var body: some View {
VStack {
if viewModel.state.todos.isEmpty {
VStack {
Spacer()
Text("작성된 내용이 없습니다.")
.foregroundStyle(Color.gray)
Spacer()
}
.frame(maxWidth: .infinity, alignment: .center)
ZStack {
if viewModel.state.isLoading {
LoadingView()
} else {
List(viewModel.state.todos) { todo in
Button {
router.push(Path.detail(todo))
} label: {
VStack(alignment: .leading, spacing: 5) {
HStack {
if todo.isPinned {
Image(systemName: "star.fill")
if viewModel.state.todos.isEmpty {
VStack {
Spacer()
Text("작성된 내용이 없습니다.")
.foregroundStyle(Color.gray)
Spacer()
}
.frame(maxWidth: .infinity, alignment: .center)
} else {
List(viewModel.state.todos) { todo in
Button {
router.push(Path.detail(todo))
} label: {
VStack(alignment: .leading, spacing: 5) {
HStack {
if todo.isPinned {
Image(systemName: "star.fill")
.font(.headline)
.foregroundStyle(Color.orange)
}
Text(todo.title)
.font(.headline)
.foregroundStyle(Color.orange)
.lineLimit(1)
}
Text(todo.title)
.font(.headline)
Text(todo.content)
.font(.subheadline)
.foregroundStyle(Color.gray)
.lineLimit(1)
}
Text(todo.content)
.font(.subheadline)
.foregroundStyle(Color.gray)
.lineLimit(1)
.padding(.vertical, 5)
}
.padding(.vertical, 5)
}
.swipeActions(edge: .leading) {
Button(action: {
viewModel.send(.tapTogglePinned(todo))
}) {
Image(systemName: "star\(todo.isPinned ? ".slash" : ".fill")")
}
.tint(Color.orange)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive, action: {
viewModel.send(.swipeTodo(todo))
}) {
Image(systemName: "trash")
.swipeActions(edge: .leading) {
Button(action: {
viewModel.send(.tapTogglePinned(todo))
}) {
Image(systemName: "star\(todo.isPinned ? ".slash" : ".fill")")
}
.tint(Color.orange)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive, action: {
viewModel.send(.swipeTodo(todo))
}) {
Image(systemName: "trash")
}

}
}
}
.listStyle(.plain)
.refreshable {
viewModel.send(.refresh)
}
.navigationDestination(for: Path.self) { path in
switch path {
case .detail(let todo):
TodoDetailView(
todo: todo,
onSubmit: { viewModel.send(.upsertTodo($0)) }
)
.listStyle(.plain)
.refreshable {
viewModel.send(.refresh)
}
.navigationDestination(for: Path.self) { path in
switch path {
case .detail(let todo):
TodoDetailView(
todo: todo,
onSubmit: { viewModel.send(.upsertTodo($0)) }
)
}
}
}
}
}
.alert("불러오기 실패", isPresented: Binding(
get: { viewModel.state.showAlert },
set: { _, _ in }
)) {
Button(role: .cancel, action: {
viewModel.send(.closeAlert)
}) {
Text("확인")
}
} message: {
Text(viewModel.state.alertMessage)
}
.navigationTitle(viewModel.state.kind.localizedName)
.navigationBarTitleDisplayMode(.large)
.fullScreenCover(isPresented: Binding(
Expand Down Expand Up @@ -181,14 +197,7 @@ struct TodoView: View {
Text(scope.localizedName).tag(scope)
}
}
.task {
viewModel.send(.onAppear)
}
.overlay {
if viewModel.state.isLoading {
LoadingView()
}
}
.task { viewModel.send(.onAppear) }
}

private enum Path: Hashable {
Expand Down