Skip to content

Commit 8c552da

Browse files
CopilotVetle444
andcommitted
Use native UISearchController for iOS SearchBehavior instead of custom pill + overlay
Agent-Logs-Url: https://github.com/DIPSAS/DIPS.Mobile.UI/sessions/a88f919a-236b-42b6-a293-1ec1dcddef9b Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com>
1 parent 25edf54 commit 8c552da

4 files changed

Lines changed: 43 additions & 203 deletions

File tree

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

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

115115
/// <summary>
116116
/// A behavior that adds native platform search to this page.
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.
117+
/// On iOS, this uses <c>UISearchController</c> integrated into the navigation bar.
118+
/// On Android, this uses Material 3 <c>SearchBar</c> + <c>SearchView</c>.
119119
/// </summary>
120-
/// <remarks>Set <see cref="SearchBehavior.SearchCommand"/> to receive search text changes.</remarks>
120+
/// <remarks>
121+
/// Set <see cref="SearchBehavior.SearchCommand"/> to receive search text changes.
122+
/// On iOS, the page must be inside a NavigationPage or Shell for navigation bar integration.
123+
/// </remarks>
121124
public SearchBehavior? SearchBehavior
122125
{
123126
get => (SearchBehavior?)GetValue(SearchBehaviorProperty);

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ 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 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>.
7+
/// On iOS, this uses <c>UISearchController</c> integrated into the navigation bar.
8+
/// On Android, this uses Material 3 <c>SearchBar</c> + <c>SearchView</c> with a pill-shaped
9+
/// trigger at the bottom and full-screen search mode transformation.
1010
/// </summary>
1111
/// <remarks>
1212
/// Set this on <see cref="ContentPage.SearchBehavior"/> to enable search.
13+
/// On iOS, the page must be inside a NavigationPage or Shell for navigation bar integration.
1314
/// </remarks>
1415
public partial class SearchBehavior : BindableObject
1516
{
Lines changed: 21 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,235 +1,62 @@
1-
using CoreGraphics;
21
using Foundation;
32
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;
73

84
namespace DIPS.Mobile.UI.Components.Pages.Search;
95

106
/// <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.
1510
/// </summary>
16-
internal class PageSearchController : NSObject, IUISearchBarDelegate
11+
internal class PageSearchController : NSObject, IUISearchResultsUpdating
1712
{
1813
private readonly WeakReference<SearchBehavior> m_weakBehavior;
1914
private string m_previousText = string.Empty;
2015

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; }
2517

2618
public PageSearchController(SearchBehavior behavior)
2719
{
2820
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);
8721

88-
// Placeholder label
89-
var placeholderLabel = new UILabel
22+
SearchController = new UISearchController(searchResultsController: null)
9023
{
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
9527
};
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-
]);
10828

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;
11432
}
11533

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)
19135
{
19236
if (!m_weakBehavior.TryGetTarget(out var behavior))
19337
return;
19438

39+
var newText = searchController.SearchBar.Text ?? string.Empty;
19540
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;
21142

212-
HideSearchMode();
43+
behavior.OnNativeSearchTextChanged(newText, oldText);
21344
}
21445

21546
public void Focus()
21647
{
217-
ShowSearchMode();
48+
SearchController.Active = true;
49+
SearchController.SearchBar.BecomeFirstResponder();
21850
}
21951

22052
public void Unfocus()
22153
{
222-
HideSearchMode();
54+
SearchController.SearchBar.ResignFirstResponder();
22355
}
22456

22557
public void Cleanup()
22658
{
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();
23461
}
23562
}

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

Lines changed: 12 additions & 3 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 view hierarchy are fully established
12+
// Dispatch to ensure the handler and navigation hierarchy are fully established
1313
page.Dispatcher.Dispatch(async () =>
1414
{
1515
await Task.Delay(1);
@@ -18,11 +18,13 @@ partial void SetupNativeSearch(ContentPage page)
1818
return;
1919

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

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

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

3537
partial void TeardownNativeSearch(ContentPage page)
3638
{
39+
if (page.Handler is IPlatformViewHandler platformHandler)
40+
{
41+
var viewController = platformHandler.ViewController;
42+
if (viewController != null)
43+
viewController.NavigationItem.SearchController = null;
44+
}
45+
3746
m_searchController?.Cleanup();
3847
m_searchController = null;
3948
FocusSearchAction = null;

0 commit comments

Comments
 (0)