|
| 1 | +using CoreGraphics; |
1 | 2 | using Foundation; |
2 | 3 | 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; |
3 | 7 |
|
4 | 8 | namespace DIPS.Mobile.UI.Components.Pages.Search; |
5 | 9 |
|
6 | 10 | /// <summary> |
7 | | -/// Manages a native UISearchController for ContentPage search functionality. |
8 | | -/// Implements <see cref="IUISearchResultsUpdating"/> to receive search text updates. |
| 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. |
9 | 15 | /// </summary> |
10 | | -internal class PageSearchController : NSObject, IUISearchResultsUpdating |
| 16 | +internal class PageSearchController : NSObject, IUISearchBarDelegate |
11 | 17 | { |
12 | 18 | private readonly WeakReference<SearchBehavior> m_weakBehavior; |
13 | 19 | private string m_previousText = string.Empty; |
14 | 20 |
|
15 | | - public UISearchController SearchController { get; } |
| 21 | + private UIView? m_searchPill; |
| 22 | + private UIView? m_searchOverlay; |
| 23 | + private UISearchBar? m_searchBar; |
| 24 | + private WeakReference<UIViewController>? m_weakViewController; |
16 | 25 |
|
17 | 26 | public PageSearchController(SearchBehavior behavior) |
18 | 27 | { |
19 | 28 | 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); |
20 | 87 |
|
21 | | - SearchController = new UISearchController(searchResultsController: null) |
| 88 | + // Placeholder label |
| 89 | + var placeholderLabel = new UILabel |
22 | 90 | { |
23 | | - SearchResultsUpdater = this, |
24 | | - ObscuresBackgroundDuringPresentation = false, |
25 | | - HidesNavigationBarDuringPresentation = false |
| 91 | + Text = "Search", |
| 92 | + TextColor = Colors.GetColor(ColorName.color_text_subtle).ToPlatform(), |
| 93 | + Font = UIFont.SystemFontOfSize(16), |
| 94 | + TranslatesAutoresizingMaskIntoConstraints = false |
26 | 95 | }; |
| 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 | + ]); |
27 | 108 |
|
28 | | - var searchBar = SearchController.SearchBar; |
29 | | - searchBar.AutocapitalizationType = UITextAutocapitalizationType.None; |
30 | | - searchBar.SearchBarStyle = UISearchBarStyle.Minimal; |
| 109 | + // Tap gesture to open search mode |
| 110 | + var tapGesture = new UITapGestureRecognizer(() => ShowSearchMode()); |
| 111 | + pill.AddGestureRecognizer(tapGesture); |
| 112 | + |
| 113 | + return pill; |
31 | 114 | } |
32 | 115 |
|
33 | | - public void UpdateSearchResultsForSearchController(UISearchController searchController) |
| 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 | 191 | { |
35 | 192 | if (!m_weakBehavior.TryGetTarget(out var behavior)) |
36 | 193 | return; |
37 | 194 |
|
38 | | - var newText = searchController.SearchBar.Text ?? string.Empty; |
39 | 195 | var oldText = m_previousText; |
40 | | - m_previousText = newText; |
| 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 | 211 |
|
42 | | - behavior.OnNativeSearchTextChanged(newText, oldText); |
| 212 | + HideSearchMode(); |
43 | 213 | } |
44 | 214 |
|
45 | 215 | public void Focus() |
46 | 216 | { |
47 | | - SearchController.Active = true; |
48 | | - SearchController.SearchBar.BecomeFirstResponder(); |
| 217 | + ShowSearchMode(); |
49 | 218 | } |
50 | 219 |
|
51 | 220 | public void Unfocus() |
52 | 221 | { |
53 | | - SearchController.SearchBar.ResignFirstResponder(); |
| 222 | + HideSearchMode(); |
54 | 223 | } |
55 | 224 |
|
56 | 225 | public void Cleanup() |
57 | 226 | { |
58 | | - SearchController.SearchResultsUpdater = null; |
59 | | - SearchController.Dispose(); |
| 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; |
60 | 234 | } |
61 | 235 | } |
0 commit comments