-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathCollapsibleSearchField.swift
More file actions
118 lines (102 loc) · 3.97 KB
/
CollapsibleSearchField.swift
File metadata and controls
118 lines (102 loc) · 3.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import SwiftUI
import AppKit
public struct CollapsibleSearchField: View {
@Binding public var searchText: String
@Binding public var isExpanded: Bool
public let placeholderString: String
public init(
searchText: Binding<String>,
isExpanded: Binding<Bool>,
placeholderString: String = "Search..."
) {
self._searchText = searchText
self._isExpanded = isExpanded
self.placeholderString = placeholderString
}
public var body: some View {
Group {
if isExpanded {
SearchFieldRepresentable(
searchText: $searchText,
isExpanded: $isExpanded,
placeholderString: placeholderString
)
.frame(width: 200, height: 24)
.transition(.opacity)
} else {
Button(action: {
isExpanded = true
}) {
Image(systemName: "magnifyingglass")
.font(.system(size: 13))
}
.buttonStyle(.plain)
.frame(height: 24)
.transition(.opacity)
}
}
}
}
private struct SearchFieldRepresentable: NSViewRepresentable {
@Binding var searchText: String
@Binding var isExpanded: Bool
let placeholderString: String
func makeNSView(context: Context) -> NSSearchField {
let searchField = NSSearchField()
searchField.placeholderString = placeholderString
searchField.delegate = context.coordinator
searchField.target = context.coordinator
searchField.action = #selector(Coordinator.searchFieldDidChange(_:))
// Make the magnifying glass clickable to collapse
if let cell = searchField.cell as? NSSearchFieldCell {
cell.searchButtonCell?.target = context.coordinator
cell.searchButtonCell?.action = #selector(Coordinator.magnifyingGlassClicked(_:))
}
return searchField
}
func updateNSView(_ nsView: NSSearchField, context: Context) {
if nsView.stringValue != searchText {
nsView.stringValue = searchText
}
context.coordinator.isExpanded = $isExpanded
// Auto-focus when expanded, only if not already first responder
if isExpanded && nsView.window?.firstResponder != nsView.currentEditor() {
DispatchQueue.main.async {
nsView.window?.makeFirstResponder(nsView)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(searchText: $searchText, isExpanded: $isExpanded)
}
class Coordinator: NSObject, NSSearchFieldDelegate, NSTextFieldDelegate {
@Binding var searchText: String
var isExpanded: Binding<Bool>
init(searchText: Binding<String>, isExpanded: Binding<Bool>) {
_searchText = searchText
self.isExpanded = isExpanded
}
@objc func searchFieldDidChange(_ sender: NSSearchField) {
searchText = sender.stringValue
}
@objc func magnifyingGlassClicked(_ sender: Any) {
// Collapse when magnifying glass is clicked
DispatchQueue.main.async { [weak self] in
withAnimation(.easeInOut(duration: 0.2)) {
self?.isExpanded.wrappedValue = false
}
}
}
func controlTextDidEndEditing(_ obj: Notification) {
// Collapse search field when it loses focus and text is empty
if searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
DispatchQueue.main.async { [weak self] in
withAnimation(.easeInOut(duration: 0.2)) {
self?.isExpanded.wrappedValue = false
self?.searchText = ""
}
}
}
}
}
}