解决搜索页面的焦点获取问题
This commit is contained in:
parent
6323b1d2c9
commit
b11362b912
137
dimensionhub/Views/Search/SearchBar.swift
Normal file
137
dimensionhub/Views/Search/SearchBar.swift
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
//
|
||||||
|
// SearchBar.swift
|
||||||
|
// dimensionhub
|
||||||
|
//
|
||||||
|
// Created by 安礼成 on 2025/6/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SearchBar: View {
|
||||||
|
// 键盘焦点的控制
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
@Binding var searchText: String
|
||||||
|
var doSearch: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ControlledTextField(text: $searchText, placeholder: "搜索..", isFirstResponder: $isFirstResponder, onCommit: doSearch)
|
||||||
|
.textFieldStyle(PlainTextFieldStyle())
|
||||||
|
.padding(8)
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.submitLabel(.search)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.keyboardType(.default)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
if !searchText.isEmpty {
|
||||||
|
doSearch()
|
||||||
|
isFirstResponder = false
|
||||||
|
print("isFirstResponder is set false")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("搜索")
|
||||||
|
.font(.system(size: 18))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
.disabled(searchText.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ControlledTextField: UIViewRepresentable {
|
||||||
|
@Binding var text: String
|
||||||
|
var placeholder: String
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
var onCommit: () -> Void
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITextField {
|
||||||
|
let textField = UITextField()
|
||||||
|
textField.placeholder = placeholder
|
||||||
|
textField.delegate = context.coordinator
|
||||||
|
textField.returnKeyType = .search
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.autocapitalizationType = .none
|
||||||
|
textField.clearButtonMode = .whileEditing
|
||||||
|
textField.borderStyle = .none
|
||||||
|
textField.keyboardType = .default
|
||||||
|
textField.font = UIFont.systemFont(ofSize: 17)
|
||||||
|
|
||||||
|
return textField
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||||
|
// 1. 安全更新文本内容
|
||||||
|
if uiView.text != text {
|
||||||
|
context.coordinator.isUpdatingFromBinding = true
|
||||||
|
uiView.text = text
|
||||||
|
context.coordinator.isUpdatingFromBinding = false
|
||||||
|
}
|
||||||
|
print("call me update UIView: \(isFirstResponder)")
|
||||||
|
|
||||||
|
// 2. 焦点控制优化
|
||||||
|
if isFirstResponder {
|
||||||
|
if !uiView.isFirstResponder {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
uiView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if uiView.isFirstResponder {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
uiView.resignFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(text: $text, isFirstResponder: $isFirstResponder, onCommit: onCommit)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UITextFieldDelegate {
|
||||||
|
@Binding var text: String
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
|
||||||
|
var onCommit: () -> Void
|
||||||
|
var isUpdatingFromBinding = false
|
||||||
|
|
||||||
|
init(text: Binding<String>, isFirstResponder: Binding<Bool>, onCommit: @escaping () -> Void) {
|
||||||
|
self._text = text
|
||||||
|
self._isFirstResponder = isFirstResponder
|
||||||
|
self.onCommit = onCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取到了焦点
|
||||||
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
|
self.isFirstResponder = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失去焦点
|
||||||
|
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
|
||||||
|
self.isFirstResponder = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
onCommit()
|
||||||
|
self.isFirstResponder = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||||
|
// 防止循环更新
|
||||||
|
if !isUpdatingFromBinding {
|
||||||
|
text = textField.text ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
70
dimensionhub/Views/Search/SearchHistoryView.swift
Normal file
70
dimensionhub/Views/Search/SearchHistoryView.swift
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// SearchHistoryView1.swift
|
||||||
|
// dimensionhub
|
||||||
|
//
|
||||||
|
// Created by 安礼成 on 2025/6/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct SearchHistoryView: View {
|
||||||
|
@Query(sort: \SearchHistory.timestamp, order: .reverse) private var historyItems: [SearchHistory]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if historyItems.count > 5 {
|
||||||
|
ForEach(Array(0..<5), id: \.self) { idx in
|
||||||
|
SearchHistoryItemView(history: historyItems[idx]) {
|
||||||
|
print("click me")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
//self.showHistoryNum = 5
|
||||||
|
} label: {
|
||||||
|
Text("查看更多历史")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
|
ForEach(historyItems) { history in
|
||||||
|
SearchHistoryItemView(history: history) {
|
||||||
|
print("click me")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 搜索历史
|
||||||
|
struct SearchHistoryItemView: View {
|
||||||
|
let history: SearchHistory
|
||||||
|
var onClick: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 20) {
|
||||||
|
Image("lost_network")
|
||||||
|
.resizable()
|
||||||
|
.clipShape(Circle())
|
||||||
|
.clipped()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
|
||||||
|
Text(history.keyword)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(width: 280, alignment: .leading)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
onClick()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct SearchView: View {
|
struct SearchView: View {
|
||||||
@Environment(\.modelContext) var modelContext
|
@Environment(\.modelContext) var modelContext
|
||||||
@ -16,7 +15,6 @@ struct SearchView: View {
|
|||||||
@State var searchText: String = ""
|
@State var searchText: String = ""
|
||||||
@State var searchModel = SearchModel()
|
@State var searchModel = SearchModel()
|
||||||
|
|
||||||
@FocusState private var isFocused: Bool
|
|
||||||
@State private var isSearching = false
|
@State private var isSearching = false
|
||||||
|
|
||||||
// 是否显示搜索结果
|
// 是否显示搜索结果
|
||||||
@ -25,61 +23,13 @@ struct SearchView: View {
|
|||||||
// 控制是否需要显示键盘
|
// 控制是否需要显示键盘
|
||||||
@State private var showKeyboard: Bool = true
|
@State private var showKeyboard: Bool = true
|
||||||
|
|
||||||
|
@State private var toolbarID = UUID()
|
||||||
|
|
||||||
|
// 键盘焦点的控制
|
||||||
|
@State private var isFocused: Bool = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
HStack(spacing: 12) {
|
|
||||||
Button(action: { dismiss() }) {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
.font(.system(size: 20, weight: .medium))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
TextField("搜索...", text: $searchText)
|
|
||||||
.focused($isFocused)
|
|
||||||
.textFieldStyle(PlainTextFieldStyle())
|
|
||||||
.padding(8)
|
|
||||||
.background(Color(.systemGray6))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.submitLabel(.search)
|
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
.disableAutocorrection(true)
|
|
||||||
.keyboardType(.default)
|
|
||||||
.onSubmit {
|
|
||||||
doSearch()
|
|
||||||
}
|
|
||||||
.overlay(alignment: .trailing) {
|
|
||||||
Button(action: {
|
|
||||||
searchText = ""
|
|
||||||
}) {
|
|
||||||
Image(systemName: "multiply.circle.fill")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.padding(.trailing, 8)
|
|
||||||
}
|
|
||||||
.opacity(isSearching ? 1 : 0)
|
|
||||||
.disabled(!isSearching)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
if !searchText.isEmpty {
|
|
||||||
doSearch()
|
|
||||||
isFocused = false // 关闭键盘
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("搜索")
|
|
||||||
.font(.system(size: 18))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
.padding(.trailing, 10)
|
|
||||||
.disabled(searchText.isEmpty)
|
|
||||||
}
|
|
||||||
.onChange(of: searchText) {
|
|
||||||
isSearching = !searchText.trimmingCharacters(in: .whitespaces).isEmpty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 50)
|
|
||||||
.padding(.top, 12)
|
|
||||||
|
|
||||||
if showSearchResult {
|
if showSearchResult {
|
||||||
if searchModel.dramaGroups.isEmpty {
|
if searchModel.dramaGroups.isEmpty {
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -111,17 +61,25 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.padding(.top, 10)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
.navigationBarBackButtonHidden(true)
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
SearchBar(isFirstResponder: $isFocused, searchText: $searchText) {
|
||||||
|
doSearch()
|
||||||
|
}
|
||||||
|
.id(toolbarID)
|
||||||
|
.onAppear {
|
||||||
|
print("searchBar appear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom) // 避免键盘遮挡
|
.ignoresSafeArea(.keyboard, edges: .bottom) // 避免键盘遮挡
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// 避免过早触发系统输入错误
|
print("main appear")
|
||||||
if showKeyboard {
|
toolbarID = UUID()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
isFocused = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,70 +140,7 @@ struct SearchDramaGroupView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchView {
|
//
|
||||||
|
//#Preview {
|
||||||
struct SearchHistoryView1: View {
|
// SearchView()
|
||||||
@Query(sort: \SearchHistory.timestamp, order: .reverse) private var historyItems: [SearchHistory]
|
//}
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
if historyItems.count > 5 {
|
|
||||||
ForEach(Array(0..<5), id: \.self) { idx in
|
|
||||||
SearchHistoryItemView(history: historyItems[idx]) {
|
|
||||||
print("click me")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
//self.showHistoryNum = 5
|
|
||||||
} label: {
|
|
||||||
Text("查看更多历史")
|
|
||||||
.font(.system(size: 16))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
} else {
|
|
||||||
ForEach(historyItems) { history in
|
|
||||||
SearchHistoryItemView(history: history) {
|
|
||||||
print("click me")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索历史
|
|
||||||
struct SearchHistoryItemView: View {
|
|
||||||
let history: SearchHistory
|
|
||||||
var onClick: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(alignment: .center, spacing: 20) {
|
|
||||||
Image("lost_network")
|
|
||||||
.resizable()
|
|
||||||
.clipShape(Circle())
|
|
||||||
.clipped()
|
|
||||||
.frame(width: 25, height: 25)
|
|
||||||
|
|
||||||
Text(history.keyword)
|
|
||||||
.font(.system(size: 16))
|
|
||||||
.lineLimit(1)
|
|
||||||
.frame(width: 280, alignment: .leading)
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
onClick()
|
|
||||||
}) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
SearchView()
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user