parent
eb1f69ec97
commit
62fd9c5610
@ -0,0 +1,168 @@ |
|||||||
|
// |
||||||
|
// UserSearchView.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 03/12/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Combine |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
class UserSearchViewModel: ObservableObject { |
||||||
|
@Published var searchText = "" |
||||||
|
@Published var users: [ShortUser] = [] |
||||||
|
@Published var isLoading = false |
||||||
|
@Published var error: String? |
||||||
|
@Published var selectedUser: ShortUser? = nil |
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>() |
||||||
|
private var originalUsers: [ShortUser] = [] |
||||||
|
private var lastSearchTerm = "" |
||||||
|
|
||||||
|
init() { |
||||||
|
// Debounce search to avoid too many requests |
||||||
|
$searchText |
||||||
|
.removeDuplicates() |
||||||
|
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) |
||||||
|
.sink { [weak self] searchTerm in |
||||||
|
self?.handleSearch(searchTerm) |
||||||
|
} |
||||||
|
.store(in: &cancellables) |
||||||
|
} |
||||||
|
|
||||||
|
private func handleSearch(_ searchTerm: String) { |
||||||
|
guard !searchTerm.isEmpty else { |
||||||
|
users = [] |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// If going backwards in search, filter existing results |
||||||
|
if searchTerm.count < lastSearchTerm.count && !originalUsers.isEmpty { |
||||||
|
filterExistingResults(searchTerm) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Otherwise, make a new request |
||||||
|
performServerSearch(searchTerm) |
||||||
|
} |
||||||
|
|
||||||
|
private func filterExistingResults(_ searchTerm: String) { |
||||||
|
users = originalUsers.filter { user in |
||||||
|
user.firstName.localizedCaseInsensitiveContains(searchTerm) |
||||||
|
|| user.lastName.localizedCaseInsensitiveContains(searchTerm) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func performServerSearch(_ searchTerm: String) { |
||||||
|
isLoading = true |
||||||
|
error = nil |
||||||
|
|
||||||
|
Task { |
||||||
|
do { |
||||||
|
let services = try StoreCenter.main.service() |
||||||
|
let searchResults = try await services.searchUsers(string: searchTerm) |
||||||
|
|
||||||
|
await MainActor.run { |
||||||
|
self.originalUsers = searchResults |
||||||
|
self.users = searchResults |
||||||
|
self.lastSearchTerm = searchTerm |
||||||
|
self.isLoading = false |
||||||
|
} |
||||||
|
} catch { |
||||||
|
await MainActor.run { |
||||||
|
self.error = error.localizedDescription |
||||||
|
self.isLoading = false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct UserSearchView: View { |
||||||
|
@StateObject private var viewModel = UserSearchViewModel() |
||||||
|
|
||||||
|
var handler: (ShortUser) -> Void |
||||||
|
|
||||||
|
var body: some View { |
||||||
|
NavigationView { |
||||||
|
VStack { |
||||||
|
searchField |
||||||
|
|
||||||
|
if viewModel.isLoading { |
||||||
|
loadingView |
||||||
|
} else if let error = viewModel.error { |
||||||
|
errorView(error) |
||||||
|
} else { |
||||||
|
List { |
||||||
|
ForEach(viewModel.users, id: \.id) { user in |
||||||
|
let isSelected = (user.id == viewModel.selectedUser?.id) |
||||||
|
UserRow(user: user, isSelected: isSelected) |
||||||
|
.contentShape(Rectangle()) |
||||||
|
.onTapGesture { |
||||||
|
viewModel.selectedUser = user |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.listStyle(PlainListStyle()) |
||||||
|
} |
||||||
|
} |
||||||
|
.navigationTitle("Search Users") |
||||||
|
.toolbar { |
||||||
|
ToolbarItem(placement: .navigationBarTrailing) { |
||||||
|
Button("Select") { |
||||||
|
if let selectedUser = viewModel.selectedUser { |
||||||
|
handler(selectedUser) |
||||||
|
} |
||||||
|
} |
||||||
|
.disabled(viewModel.selectedUser == nil) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private var searchField: some View { |
||||||
|
TextField("Search users...", text: $viewModel.searchText) |
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle()) |
||||||
|
.padding() |
||||||
|
} |
||||||
|
|
||||||
|
private var loadingView: some View { |
||||||
|
ProgressView() |
||||||
|
.progressViewStyle(CircularProgressViewStyle()) |
||||||
|
.scaleEffect(1.5) |
||||||
|
.frame(maxHeight: .infinity) |
||||||
|
} |
||||||
|
|
||||||
|
private func errorView(_ error: String) -> some View { |
||||||
|
Text(error) |
||||||
|
.foregroundColor(.red) |
||||||
|
.frame(maxHeight: .infinity) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
struct UserRow: View { |
||||||
|
let user: ShortUser |
||||||
|
let isSelected: Bool |
||||||
|
|
||||||
|
var body: some View { |
||||||
|
HStack { |
||||||
|
Text("\(user.firstName) \(user.lastName)") |
||||||
|
Spacer() |
||||||
|
if self.isSelected { |
||||||
|
Image(systemName: "checkmark").tint(.logoOrange) |
||||||
|
} |
||||||
|
} |
||||||
|
.padding(.vertical, 4) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Preview provider |
||||||
|
struct UserSearchView_Previews: PreviewProvider { |
||||||
|
static var previews: some View { |
||||||
|
UserSearchView { user in |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue