|
1 | | -using CoreGraphics; |
2 | 1 | using Foundation; |
3 | 2 | using UIKit; |
4 | | -using Colors = DIPS.Mobile.UI.Resources.Colors.Colors; |
5 | | -using DIPS.Mobile.UI.Resources.Colors; |
6 | | -using DIPS.Mobile.UI.Resources.Sizes; |
7 | 3 |
|
8 | 4 | namespace DIPS.Mobile.UI.Components.Pages.Search; |
9 | 5 |
|
10 | 6 | /// <summary> |
11 | | -/// Provides a search experience for ContentPage with a pill-shaped search trigger at the bottom |
12 | | -/// and a full-screen search mode overlay when activated. |
13 | | -/// When the search pill is tapped, the page "transforms" into search mode with the search field |
14 | | -/// at the top and the keyboard visible. |
| 7 | +/// Manages a native <see cref="UISearchController"/> for ContentPage search functionality. |
| 8 | +/// The search controller is added to the navigation item, providing the standard iOS search experience. |
| 9 | +/// Implements <see cref="IUISearchResultsUpdating"/> to receive search text updates. |
15 | 10 | /// </summary> |
16 | | -internal class PageSearchController : NSObject, IUISearchBarDelegate |
| 11 | +internal class PageSearchController : NSObject, IUISearchResultsUpdating |
17 | 12 | { |
18 | 13 | private readonly WeakReference<SearchBehavior> m_weakBehavior; |
19 | 14 | private string m_previousText = string.Empty; |
20 | 15 |
|
21 | | - private UIView? m_searchPill; |
22 | | - private UIView? m_searchOverlay; |
23 | | - private UISearchBar? m_searchBar; |
24 | | - private WeakReference<UIViewController>? m_weakViewController; |
| 16 | + public UISearchController SearchController { get; } |
25 | 17 |
|
26 | 18 | public PageSearchController(SearchBehavior behavior) |
27 | 19 | { |
28 | 20 | m_weakBehavior = new WeakReference<SearchBehavior>(behavior); |
29 | | - } |
30 | | - |
31 | | - /// <summary> |
32 | | - /// Sets up the bottom search pill and the full-screen search overlay on the given view controller. |
33 | | - /// </summary> |
34 | | - public void SetupInViewController(UIViewController viewController) |
35 | | - { |
36 | | - m_weakViewController = new WeakReference<UIViewController>(viewController); |
37 | | - var view = viewController.View; |
38 | | - if (view == null) return; |
39 | | - |
40 | | - // Create the pill-shaped search trigger at the bottom |
41 | | - m_searchPill = CreateSearchPill(view); |
42 | | - view.AddSubview(m_searchPill); |
43 | | - |
44 | | - // Position pill at bottom with constraints |
45 | | - m_searchPill.TranslatesAutoresizingMaskIntoConstraints = false; |
46 | | - var pillMargin = (nfloat)Sizes.GetSize(SizeName.content_margin_medium); |
47 | | - NSLayoutConstraint.ActivateConstraints([ |
48 | | - m_searchPill.LeadingAnchor.ConstraintEqualTo(view.SafeAreaLayoutGuide.LeadingAnchor, pillMargin), |
49 | | - m_searchPill.TrailingAnchor.ConstraintEqualTo(view.SafeAreaLayoutGuide.TrailingAnchor, -pillMargin), |
50 | | - m_searchPill.BottomAnchor.ConstraintEqualTo(view.SafeAreaLayoutGuide.BottomAnchor, -pillMargin), |
51 | | - m_searchPill.HeightAnchor.ConstraintEqualTo(48) |
52 | | - ]); |
53 | | - |
54 | | - // Create the full-screen search overlay (initially hidden) |
55 | | - m_searchOverlay = CreateSearchOverlay(view); |
56 | | - m_searchOverlay.Alpha = 0; |
57 | | - m_searchOverlay.Hidden = true; |
58 | | - view.AddSubview(m_searchOverlay); |
59 | | - |
60 | | - m_searchOverlay.TranslatesAutoresizingMaskIntoConstraints = false; |
61 | | - NSLayoutConstraint.ActivateConstraints([ |
62 | | - m_searchOverlay.TopAnchor.ConstraintEqualTo(view.TopAnchor), |
63 | | - m_searchOverlay.LeadingAnchor.ConstraintEqualTo(view.LeadingAnchor), |
64 | | - m_searchOverlay.TrailingAnchor.ConstraintEqualTo(view.TrailingAnchor), |
65 | | - m_searchOverlay.BottomAnchor.ConstraintEqualTo(view.BottomAnchor) |
66 | | - ]); |
67 | | - } |
68 | | - |
69 | | - private UIView CreateSearchPill(UIView parentView) |
70 | | - { |
71 | | - var pill = new UIView |
72 | | - { |
73 | | - BackgroundColor = Colors.GetColor(ColorName.color_surface_default).ToPlatform(), |
74 | | - }; |
75 | | - pill.Layer.CornerRadius = 24; |
76 | | - pill.Layer.BorderWidth = 1; |
77 | | - pill.Layer.BorderColor = Colors.GetColor(ColorName.color_border_default).ToCGColor(); |
78 | | - |
79 | | - // Search icon |
80 | | - var searchIcon = new UIImageView(UIImage.GetSystemImage("magnifyingglass")) |
81 | | - { |
82 | | - TintColor = Colors.GetColor(ColorName.color_text_default).ToPlatform(), |
83 | | - ContentMode = UIViewContentMode.ScaleAspectFit, |
84 | | - TranslatesAutoresizingMaskIntoConstraints = false |
85 | | - }; |
86 | | - pill.AddSubview(searchIcon); |
87 | 21 |
|
88 | | - // Placeholder label |
89 | | - var placeholderLabel = new UILabel |
| 22 | + SearchController = new UISearchController(searchResultsController: null) |
90 | 23 | { |
91 | | - Text = "Search", |
92 | | - TextColor = Colors.GetColor(ColorName.color_text_subtle).ToPlatform(), |
93 | | - Font = UIFont.SystemFontOfSize(16), |
94 | | - TranslatesAutoresizingMaskIntoConstraints = false |
| 24 | + SearchResultsUpdater = this, |
| 25 | + ObscuresBackgroundDuringPresentation = false, |
| 26 | + HidesNavigationBarDuringPresentation = false |
95 | 27 | }; |
96 | | - pill.AddSubview(placeholderLabel); |
97 | | - |
98 | | - NSLayoutConstraint.ActivateConstraints([ |
99 | | - searchIcon.LeadingAnchor.ConstraintEqualTo(pill.LeadingAnchor, 16), |
100 | | - searchIcon.CenterYAnchor.ConstraintEqualTo(pill.CenterYAnchor), |
101 | | - searchIcon.WidthAnchor.ConstraintEqualTo(20), |
102 | | - searchIcon.HeightAnchor.ConstraintEqualTo(20), |
103 | | - |
104 | | - placeholderLabel.LeadingAnchor.ConstraintEqualTo(searchIcon.TrailingAnchor, 12), |
105 | | - placeholderLabel.CenterYAnchor.ConstraintEqualTo(pill.CenterYAnchor), |
106 | | - placeholderLabel.TrailingAnchor.ConstraintEqualTo(pill.TrailingAnchor, -16) |
107 | | - ]); |
108 | 28 |
|
109 | | - // Tap gesture to open search mode |
110 | | - var tapGesture = new UITapGestureRecognizer(() => ShowSearchMode()); |
111 | | - pill.AddGestureRecognizer(tapGesture); |
112 | | - |
113 | | - return pill; |
| 29 | + var searchBar = SearchController.SearchBar; |
| 30 | + searchBar.AutocapitalizationType = UITextAutocapitalizationType.None; |
| 31 | + searchBar.SearchBarStyle = UISearchBarStyle.Minimal; |
114 | 32 | } |
115 | 33 |
|
116 | | - private UIView CreateSearchOverlay(UIView parentView) |
117 | | - { |
118 | | - var overlay = new UIView |
119 | | - { |
120 | | - BackgroundColor = Colors.GetColor(ColorName.color_surface_default).ToPlatform() |
121 | | - }; |
122 | | - |
123 | | - // Top bar with search bar and cancel button |
124 | | - var topBar = new UIView |
125 | | - { |
126 | | - TranslatesAutoresizingMaskIntoConstraints = false, |
127 | | - BackgroundColor = Colors.GetColor(ColorName.color_surface_default).ToPlatform() |
128 | | - }; |
129 | | - overlay.AddSubview(topBar); |
130 | | - |
131 | | - m_searchBar = new UISearchBar |
132 | | - { |
133 | | - SearchBarStyle = UISearchBarStyle.Minimal, |
134 | | - AutocapitalizationType = UITextAutocapitalizationType.None, |
135 | | - TranslatesAutoresizingMaskIntoConstraints = false, |
136 | | - Delegate = this, |
137 | | - ShowsCancelButton = true |
138 | | - }; |
139 | | - topBar.AddSubview(m_searchBar); |
140 | | - |
141 | | - NSLayoutConstraint.ActivateConstraints([ |
142 | | - topBar.TopAnchor.ConstraintEqualTo(overlay.SafeAreaLayoutGuide.TopAnchor), |
143 | | - topBar.LeadingAnchor.ConstraintEqualTo(overlay.LeadingAnchor), |
144 | | - topBar.TrailingAnchor.ConstraintEqualTo(overlay.TrailingAnchor), |
145 | | - topBar.HeightAnchor.ConstraintEqualTo(56), |
146 | | - |
147 | | - m_searchBar.TopAnchor.ConstraintEqualTo(topBar.TopAnchor), |
148 | | - m_searchBar.LeadingAnchor.ConstraintEqualTo(topBar.LeadingAnchor, 8), |
149 | | - m_searchBar.TrailingAnchor.ConstraintEqualTo(topBar.TrailingAnchor, -8), |
150 | | - m_searchBar.BottomAnchor.ConstraintEqualTo(topBar.BottomAnchor) |
151 | | - ]); |
152 | | - |
153 | | - return overlay; |
154 | | - } |
155 | | - |
156 | | - private void ShowSearchMode() |
157 | | - { |
158 | | - if (m_searchOverlay == null || m_searchPill == null) return; |
159 | | - |
160 | | - m_searchOverlay.Hidden = false; |
161 | | - |
162 | | - UIView.Animate(0.3, () => |
163 | | - { |
164 | | - m_searchOverlay!.Alpha = 1; |
165 | | - m_searchPill!.Alpha = 0; |
166 | | - }, () => |
167 | | - { |
168 | | - m_searchBar?.BecomeFirstResponder(); |
169 | | - }); |
170 | | - } |
171 | | - |
172 | | - private void HideSearchMode() |
173 | | - { |
174 | | - if (m_searchOverlay == null || m_searchPill == null) return; |
175 | | - |
176 | | - m_searchBar?.ResignFirstResponder(); |
177 | | - |
178 | | - UIView.Animate(0.3, () => |
179 | | - { |
180 | | - m_searchOverlay!.Alpha = 0; |
181 | | - m_searchPill!.Alpha = 1; |
182 | | - }, () => |
183 | | - { |
184 | | - m_searchOverlay!.Hidden = true; |
185 | | - }); |
186 | | - } |
187 | | - |
188 | | - // IUISearchBarDelegate |
189 | | - [Export("searchBar:textDidChange:")] |
190 | | - public void TextChanged(UISearchBar searchBar, string searchText) |
| 34 | + public void UpdateSearchResultsForSearchController(UISearchController searchController) |
191 | 35 | { |
192 | 36 | if (!m_weakBehavior.TryGetTarget(out var behavior)) |
193 | 37 | return; |
194 | 38 |
|
| 39 | + var newText = searchController.SearchBar.Text ?? string.Empty; |
195 | 40 | var oldText = m_previousText; |
196 | | - m_previousText = searchText; |
197 | | - behavior.OnNativeSearchTextChanged(searchText, oldText); |
198 | | - } |
199 | | - |
200 | | - [Export("searchBarCancelButtonClicked:")] |
201 | | - public void CancelButtonClicked(UISearchBar searchBar) |
202 | | - { |
203 | | - searchBar.Text = string.Empty; |
204 | | - |
205 | | - if (m_weakBehavior.TryGetTarget(out var behavior)) |
206 | | - { |
207 | | - var oldText = m_previousText; |
208 | | - m_previousText = string.Empty; |
209 | | - behavior.OnNativeSearchTextChanged(string.Empty, oldText); |
210 | | - } |
| 41 | + m_previousText = newText; |
211 | 42 |
|
212 | | - HideSearchMode(); |
| 43 | + behavior.OnNativeSearchTextChanged(newText, oldText); |
213 | 44 | } |
214 | 45 |
|
215 | 46 | public void Focus() |
216 | 47 | { |
217 | | - ShowSearchMode(); |
| 48 | + SearchController.Active = true; |
| 49 | + SearchController.SearchBar.BecomeFirstResponder(); |
218 | 50 | } |
219 | 51 |
|
220 | 52 | public void Unfocus() |
221 | 53 | { |
222 | | - HideSearchMode(); |
| 54 | + SearchController.SearchBar.ResignFirstResponder(); |
223 | 55 | } |
224 | 56 |
|
225 | 57 | public void Cleanup() |
226 | 58 | { |
227 | | - m_searchBar?.ResignFirstResponder(); |
228 | | - m_searchBar = null; |
229 | | - m_searchOverlay?.RemoveFromSuperview(); |
230 | | - m_searchOverlay = null; |
231 | | - m_searchPill?.RemoveFromSuperview(); |
232 | | - m_searchPill = null; |
233 | | - m_weakViewController = null; |
| 59 | + SearchController.SearchResultsUpdater = null; |
| 60 | + SearchController.Dispose(); |
234 | 61 | } |
235 | 62 | } |
0 commit comments