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
41 changes: 40 additions & 1 deletion DevLog/Presentation/ViewModel/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class SearchViewModel: Store {
var searchQuery: String = ""
var selectedWebPage: WebPageItem?
var webPages: OrderedSet<WebPageItem> = []
var recentQueries: OrderedSet<String> = []
var filteredWebPages: [WebPageItem] {
webPages.filter {
$0.title.localizedCaseInsensitiveContains(searchQuery) ||
Expand All @@ -30,6 +31,9 @@ final class SearchViewModel: Store {
case onAppear
case fetchWebPage([WebPageItem]? = nil)
case selectWebPage(WebPageItem)
case addRecentQuery(String)
case removeRecentQuery(String)
case clearRecentQueries
case setAlert(Bool)
case setLoading(Bool)
case setSearching(Bool)
Expand All @@ -42,9 +46,21 @@ final class SearchViewModel: Store {

@Published private(set) var state: State = .init()
private let fetchWebPagesUseCase: FetchWebPagesUseCase
private let userDefaults: UserDefaults

init(fetchWebPagesUseCase: FetchWebPagesUseCase) {
private enum DefaultsKey {
static let recentQueries = "Search.recentQueries"
}

private let maxRecentQueries = 20

init(
fetchWebPagesUseCase: FetchWebPagesUseCase,
userDefaults: UserDefaults = .standard
) {
self.fetchWebPagesUseCase = fetchWebPagesUseCase
self.userDefaults = userDefaults
self.state.recentQueries = Self.loadRecentQueries(userDefaults: userDefaults)
}

func reduce(with action: Action) -> [SideEffect] {
Expand All @@ -63,6 +79,21 @@ final class SearchViewModel: Store {
state.webPages = OrderedSet(items)
case .selectWebPage(let item):
state.selectedWebPage = item
case .addRecentQuery(let query):
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { break }
state.recentQueries.remove(trimmed)
state.recentQueries.insert(trimmed, at: 0)
if maxRecentQueries < state.recentQueries.count {
state.recentQueries = OrderedSet(state.recentQueries.prefix(maxRecentQueries))
}
saveRecentQueries(Array(state.recentQueries))
case .removeRecentQuery(let query):
state.recentQueries.remove(query)
saveRecentQueries(Array(state.recentQueries))
case .clearRecentQueries:
state.recentQueries = []
saveRecentQueries([])
case .setAlert(let isPresented):
setAlert(&state, isPresented: isPresented)
case .setLoading(let isLoading):
Expand Down Expand Up @@ -103,4 +134,12 @@ private extension SearchViewModel {
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
state.showAlert = isPresented
}

static func loadRecentQueries(userDefaults: UserDefaults) -> OrderedSet<String> {
OrderedSet(userDefaults.stringArray(forKey: DefaultsKey.recentQueries) ?? [])
}

func saveRecentQueries(_ queries: [String]) {
userDefaults.set(queries, forKey: DefaultsKey.recentQueries)
}
}
6 changes: 6 additions & 0 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@
},
"작성된 내용이 없습니다." : {

},
"전체 삭제" : {

},
"정렬 옵션" : {

Expand All @@ -370,6 +373,9 @@
},
"지난주" : {

},
"최근 검색" : {

},
"최근에 중요 표시를 한 Todo가 여기 표시됩니다." : {

Expand Down
72 changes: 61 additions & 11 deletions DevLog/UI/Search/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ struct SearchView: View {
if viewModel.state.isLoading {
LoadingView()
} else if viewModel.state.searchQuery.isEmpty {
searchInstruction
if viewModel.state.recentQueries.isEmpty {
searchInstruction
} else {
recentQueries
}
} else if viewModel.state.filteredWebPages.isEmpty {
emptySearchResult
} else {
Expand All @@ -76,20 +80,25 @@ struct SearchView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}

if #available(iOS 17.0, *) {
scrollContent.searchable(
text: searchQueryBinding,
isPresented: searchingBinding,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "검색"
)
} else {
scrollContent
.searchable(
Group {
if #available(iOS 17.0, *) {
scrollContent.searchable(
text: searchQueryBinding,
isPresented: searchingBinding,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "검색"
)
} else {
scrollContent
.searchable(
text: searchQueryBinding,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "검색"
)
}
}
.onSubmit(of: .search) {
viewModel.send(.addRecentQuery(viewModel.state.searchQuery))
}
}

Expand Down Expand Up @@ -153,6 +162,47 @@ struct SearchView: View {
.padding(.vertical, 8)
}

private var recentQueries: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("최근 검색")
.font(.headline)
.foregroundStyle(Color(.label))
Spacer()
Button("전체 삭제") {
viewModel.send(.clearRecentQueries)
}
.font(.subheadline)
.foregroundStyle(Color.gray)
}

ForEach(viewModel.state.recentQueries, id: \.self) { query in
HStack {
Image(systemName: "clock.arrow.circlepath")
.foregroundStyle(Color.gray)
Text(query)
.foregroundStyle(Color.primary)
Spacer()
Button {
viewModel.send(.removeRecentQuery(query))
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(Color.gray)
}
.buttonStyle(.plain)
}
.padding(.vertical, 4)
.contentShape(Rectangle())
.onTapGesture {
viewModel.send(.setSearchQuery(query))
viewModel.send(.setSearching(true))
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}

private enum Path: Hashable {
case webView(URL)
}
Expand Down