Skip to content

Commit 25edf54

Browse files
CopilotVetle444
andcommitted
Move iOS SearchBehavior to bottom search pill with full-screen search mode overlay
Agent-Logs-Url: https://github.com/DIPSAS/DIPS.Mobile.UI/sessions/36d6a731-666c-4139-a31d-3d3894376301 Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com>
1 parent 645bb2f commit 25edf54

4 files changed

Lines changed: 203 additions & 38 deletions

File tree

src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.Properties.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ public StatusBarStyle StatusBarStyle
114114

115115
/// <summary>
116116
/// A behavior that adds native platform search to this page.
117-
/// On iOS, this integrates a <c>UISearchController</c> into the navigation bar.
118-
/// On Android, this adds a Material 3 <c>SearchBar</c> + <c>SearchView</c>.
117+
/// On both iOS and Android, a pill-shaped search trigger is placed at the bottom of the page.
118+
/// When tapped, the page transforms into a full-screen search mode.
119119
/// </summary>
120-
/// <remarks>The page must be inside a NavigationPage or Shell for iOS navigation bar integration.</remarks>
120+
/// <remarks>Set <see cref="SearchBehavior.SearchCommand"/> to receive search text changes.</remarks>
121121
public SearchBehavior? SearchBehavior
122122
{
123123
get => (SearchBehavior?)GetValue(SearchBehaviorProperty);

src/library/DIPS.Mobile.UI/Components/Pages/Search/SearchBehavior.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ namespace DIPS.Mobile.UI.Components.Pages.Search;
44

55
/// <summary>
66
/// A behavior that adds native platform search to a <see cref="ContentPage"/>.
7-
/// On iOS, this integrates a <c>UISearchController</c> into the navigation bar.
8-
/// On Android, this adds a Material 3 <c>SearchBar</c> + <c>SearchView</c>.
7+
/// On both iOS and Android, a pill-shaped search trigger is placed at the bottom of the page.
8+
/// When tapped, the page transforms into a full-screen search mode with the search field at the top.
9+
/// On Android, this uses Material 3 <c>SearchBar</c> + <c>SearchView</c>.
910
/// </summary>
1011
/// <remarks>
1112
/// Set this on <see cref="ContentPage.SearchBehavior"/> to enable search.
12-
/// The page must be inside a NavigationPage or Shell for iOS navigation bar integration.
1313
/// </remarks>
1414
public partial class SearchBehavior : BindableObject
1515
{
Lines changed: 194 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,235 @@
1+
using CoreGraphics;
12
using Foundation;
23
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;
37

48
namespace DIPS.Mobile.UI.Components.Pages.Search;
59

610
/// <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.
915
/// </summary>
10-
internal class PageSearchController : NSObject, IUISearchResultsUpdating
16+
internal class PageSearchController : NSObject, IUISearchBarDelegate
1117
{
1218
private readonly WeakReference<SearchBehavior> m_weakBehavior;
1319
private string m_previousText = string.Empty;
1420

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;
1625

1726
public PageSearchController(SearchBehavior behavior)
1827
{
1928
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);
2087

21-
SearchController = new UISearchController(searchResultsController: null)
88+
// Placeholder label
89+
var placeholderLabel = new UILabel
2290
{
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
2695
};
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+
]);
27108

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;
31114
}
32115

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)
34191
{
35192
if (!m_weakBehavior.TryGetTarget(out var behavior))
36193
return;
37194

38-
var newText = searchController.SearchBar.Text ?? string.Empty;
39195
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+
}
41211

42-
behavior.OnNativeSearchTextChanged(newText, oldText);
212+
HideSearchMode();
43213
}
44214

45215
public void Focus()
46216
{
47-
SearchController.Active = true;
48-
SearchController.SearchBar.BecomeFirstResponder();
217+
ShowSearchMode();
49218
}
50219

51220
public void Unfocus()
52221
{
53-
SearchController.SearchBar.ResignFirstResponder();
222+
HideSearchMode();
54223
}
55224

56225
public void Cleanup()
57226
{
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;
60234
}
61235
}

src/library/DIPS.Mobile.UI/Components/Pages/Search/iOS/SearchBehavior.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public partial class SearchBehavior
99

1010
partial void SetupNativeSearch(ContentPage page)
1111
{
12-
// Dispatch to ensure the handler and navigation hierarchy are fully established
12+
// Dispatch to ensure the handler and view hierarchy are fully established
1313
page.Dispatcher.Dispatch(async () =>
1414
{
1515
await Task.Delay(1);
@@ -18,13 +18,11 @@ partial void SetupNativeSearch(ContentPage page)
1818
return;
1919

2020
var viewController = platformHandler.ViewController;
21-
if (viewController?.NavigationController == null)
21+
if (viewController?.View == null)
2222
return;
2323

2424
m_searchController = new PageSearchController(this);
25-
viewController.NavigationItem.SearchController = m_searchController.SearchController;
26-
viewController.NavigationItem.HidesSearchBarWhenScrolling = false;
27-
viewController.DefinesPresentationContext = true;
25+
m_searchController.SetupInViewController(viewController);
2826

2927
FocusSearchAction = () => m_searchController?.Focus();
3028
UnfocusSearchAction = () => m_searchController?.Unfocus();
@@ -36,13 +34,6 @@ partial void SetupNativeSearch(ContentPage page)
3634

3735
partial void TeardownNativeSearch(ContentPage page)
3836
{
39-
if (page.Handler is IPlatformViewHandler platformHandler)
40-
{
41-
var viewController = platformHandler.ViewController;
42-
if (viewController != null)
43-
viewController.NavigationItem.SearchController = null;
44-
}
45-
4637
m_searchController?.Cleanup();
4738
m_searchController = null;
4839
FocusSearchAction = null;

0 commit comments

Comments
 (0)