Skip to content

Commit d22f6c2

Browse files
huanluuhuanlums
andauthored
Add wide accessory view to ShyHeaderView (microsoft#2080)
* Add wide accessory view to ShyHeaderView * change some text * minor change * PR feedback * Rename wideAccessoryView to secondaryAccessoryView * Rename wideContentStackView to secondaryContentStackView --------- Co-authored-by: Huan Lu <[email protected]>
1 parent 2421cf4 commit d22f6c2

File tree

4 files changed

+126
-4
lines changed

4 files changed

+126
-4
lines changed

ios/FluentUI.Demo/FluentUI.Demo/Demos/NavigationControllerDemoController.swift

+32
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class NavigationControllerDemoController: DemoController {
1515
addTitle(text: "Large Title with Primary style")
1616
container.addArrangedSubview(createButton(title: "Show without accessory", action: #selector(showLargeTitle)))
1717
container.addArrangedSubview(createButton(title: "Show with collapsible search bar", action: #selector(showLargeTitleWithShyAccessory)))
18+
container.addArrangedSubview(createButton(title: "Show with collapsible search bar and pill segmented control", action: #selector(showLargeTitleWithShyAccessoryAndSecondaryAccessory)))
1819
container.addArrangedSubview(createButton(title: "Show with fixed search bar", action: #selector(showLargeTitleWithFixedAccessory)))
1920
container.addArrangedSubview(createButton(title: "Show without an avatar", action: #selector(showLargeTitleWithoutAvatar)))
2021
container.addArrangedSubview(createButton(title: "Show with a custom leading button", action: #selector(showLargeTitleWithCustomLeadingButton)))
@@ -56,6 +57,9 @@ class NavigationControllerDemoController: DemoController {
5657
addTitle(text: "Top Accessory View")
5758
container.addArrangedSubview(createButton(title: "Show with top search bar for large screen width", action: #selector(showWithTopSearchBar)))
5859

60+
addTitle(text: "Top Accessory View with shy wide accessory view")
61+
container.addArrangedSubview(createButton(title: "Show with top search bar for large screen width with a shy pill segment control", action: #selector(showWithTopSearchBarWithShySecondaryAccessoryView)))
62+
5963
addTitle(text: "Change Style Periodically")
6064
container.addArrangedSubview(createButton(title: "Change the style every second", action: #selector(showSearchChangingStyleEverySecond)))
6165
}
@@ -87,6 +91,10 @@ class NavigationControllerDemoController: DemoController {
8791
presentController(withTitleStyle: .largeLeading, accessoryView: createAccessoryView(), contractNavigationBarOnScroll: true)
8892
}
8993

94+
@objc func showLargeTitleWithShyAccessoryAndSecondaryAccessory() {
95+
presentController(withTitleStyle: .largeLeading, accessoryView: createAccessoryView(), secondaryAccessoryView: createSecondaryAccessoryView(), contractNavigationBarOnScroll: true)
96+
}
97+
9098
@objc func showLargeTitleWithFixedAccessory() {
9199
presentController(withTitleStyle: .largeLeading, accessoryView: createAccessoryView(), contractNavigationBarOnScroll: false)
92100
}
@@ -189,6 +197,10 @@ class NavigationControllerDemoController: DemoController {
189197
presentController(withTitleStyle: .largeLeading, style: .system, accessoryView: createAccessoryView(with: .onSystemNavigationBar), showsTopAccessory: true, contractNavigationBarOnScroll: false)
190198
}
191199

200+
@objc func showWithTopSearchBarWithShySecondaryAccessoryView() {
201+
presentController(withTitleStyle: .largeLeading, style: .system, accessoryView: createAccessoryView(with: .onSystemNavigationBar), secondaryAccessoryView: createSecondaryAccessoryView(), showsTopAccessory: true, contractNavigationBarOnScroll: true)
202+
}
203+
192204
@objc func showSearchChangingStyleEverySecond() {
193205
presentController(withTitleStyle: .largeLeading, style: .system, accessoryView: createAccessoryView(with: .onSystemNavigationBar), showsTopAccessory: true, contractNavigationBarOnScroll: false, updateStylePeriodically: true)
194206
}
@@ -207,6 +219,7 @@ class NavigationControllerDemoController: DemoController {
207219
subtitle: String? = nil,
208220
style: NavigationBar.Style = .primary,
209221
accessoryView: UIView? = nil,
222+
secondaryAccessoryView: UIView? = nil,
210223
showsTopAccessory: Bool = false,
211224
contractNavigationBarOnScroll: Bool = true,
212225
showShadow: Bool = true,
@@ -219,6 +232,7 @@ class NavigationControllerDemoController: DemoController {
219232
content.navigationItem.navigationBarStyle = style
220233
content.navigationItem.navigationBarShadow = showShadow ? .automatic : .alwaysHidden
221234
content.navigationItem.accessoryView = accessoryView
235+
content.navigationItem.secondaryAccessoryView = secondaryAccessoryView
222236
content.navigationItem.topAccessoryViewAttributes = NavigationBarTopSearchBarAttributes()
223237
content.navigationItem.contentScrollView = contractNavigationBarOnScroll ? content.tableView : nil
224238
content.showsTopAccessoryView = showsTopAccessory
@@ -295,6 +309,16 @@ class NavigationControllerDemoController: DemoController {
295309
return searchBar
296310
}
297311

312+
private func createSecondaryAccessoryView() -> UIView {
313+
let segmentControl = createSegmentedControl(compatibleWith: .system)
314+
let stackView = UIStackView()
315+
stackView.addArrangedSubview(segmentControl)
316+
stackView.layoutMargins = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
317+
stackView.isLayoutMarginsRelativeArrangement = true
318+
stackView.backgroundColor = view.fluentTheme.color(.background1)
319+
return stackView
320+
}
321+
298322
private func createSegmentedControl(compatibleWith style: NavigationBar.Style) -> UIView {
299323
let segmentItems: [SegmentItem] = [
300324
SegmentItem(title: "First"),
@@ -481,6 +505,7 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
481505
}
482506

483507
var showsTopAccessoryView: Bool = false
508+
var secondaryAccessoryView: UIView?
484509

485510
var personaData: PersonaData = {
486511
let personaData = PersonaData(name: "Kat Larsson", image: UIImage(named: "avatar_kat_larsson"))
@@ -835,13 +860,20 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
835860
extension RootViewController: SearchBarDelegate {
836861
func searchBarDidBeginEditing(_ searchBar: SearchBar) {
837862
searchBar.progressSpinner.state.isAnimating = false
863+
if navigationItem.secondaryAccessoryView != nil && !showsTopAccessoryView {
864+
secondaryAccessoryView = navigationItem.secondaryAccessoryView
865+
navigationItem.secondaryAccessoryView = nil
866+
}
838867
}
839868

840869
func searchBar(_ searchBar: SearchBar, didUpdateSearchText newSearchText: String?) {
841870
}
842871

843872
func searchBarDidCancel(_ searchBar: SearchBar) {
844873
searchBar.progressSpinner.state.isAnimating = false
874+
if secondaryAccessoryView != nil && !showsTopAccessoryView {
875+
navigationItem.secondaryAccessoryView = secondaryAccessoryView
876+
}
845877
}
846878

847879
func searchBarDidRequestSearch(_ searchBar: SearchBar) {

ios/FluentUI/Navigation/Shy Header/ShyHeaderController.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class ShyHeaderController: UIViewController {
4545
}
4646

4747
private var accessoryViewObservation: NSKeyValueObservation?
48+
private var secondaryAccessoryViewObservation: NSKeyValueObservation?
4849

4950
private var navigationBarCenterObservation: NSKeyValueObservation?
5051
private var navigationBarStyleObservation: NSKeyValueObservation?
@@ -101,6 +102,10 @@ class ShyHeaderController: UIViewController {
101102
accessoryViewObservation = contentViewController.navigationItem.observe(\UINavigationItem.accessoryView) { [weak self] item, _ in
102103
self?.shyHeaderView.accessoryView = item.accessoryView
103104
}
105+
106+
secondaryAccessoryViewObservation = contentViewController.navigationItem.observe(\UINavigationItem.secondaryAccessoryView) { [weak self] item, _ in
107+
self?.shyHeaderView.secondaryAccessoryView = item.secondaryAccessoryView
108+
}
104109
}
105110

106111
required init?(coder aDecoder: NSCoder) {
@@ -211,8 +216,10 @@ class ShyHeaderController: UIViewController {
211216
}
212217

213218
private func setupShyHeaderView() {
214-
shyHeaderView.accessoryView = contentViewController.navigationItem.accessoryView
215-
shyHeaderView.navigationBarShadow = contentViewController.navigationItem.navigationBarShadow
219+
let navigationItem = contentViewController.navigationItem
220+
shyHeaderView.accessoryView = navigationItem.accessoryView
221+
shyHeaderView.secondaryAccessoryView = navigationItem.secondaryAccessoryView
222+
shyHeaderView.navigationBarShadow = navigationItem.navigationBarShadow
216223
shyHeaderView.paddingView = paddingView
217224
shyHeaderView.parentController = self
218225
shyHeaderView.maxHeightChanged = { [weak self] in

ios/FluentUI/Navigation/Shy Header/ShyHeaderView.swift

+72-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
7272
tokenSet.registerOnUpdate(for: self) { [weak self] in
7373
self?.updateColors()
7474
}
75+
self.initSecondaryContentStackView()
7576
}
7677

7778
override func willMove(toWindow newWindow: UIWindow?) {
@@ -125,6 +126,13 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
125126
willSet {
126127
accessoryView?.removeFromSuperview()
127128
contentStackView.removeFromSuperview()
129+
// When there is no accessoryView, the top anchor of the secondaryContentStackView should be equal to
130+
// the top anchor of the parent view.
131+
if let secondaryContentStackViewTopAnchorConstraint {
132+
NSLayoutConstraint.activate([
133+
secondaryContentStackViewTopAnchorConstraint
134+
])
135+
}
128136
}
129137
didSet {
130138
if let newContentView = accessoryView {
@@ -135,13 +143,39 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
135143
}
136144
}
137145

138-
var maxHeight: CGFloat {
146+
var secondaryAccessoryView: UIView? {
147+
willSet {
148+
secondaryAccessoryView?.removeFromSuperview()
149+
}
150+
didSet {
151+
if let newContentView = secondaryAccessoryView {
152+
secondaryContentStackView.addArrangedSubview(newContentView)
153+
}
154+
maxHeightChanged?()
155+
}
156+
}
157+
158+
var accessoryViewHeight: CGFloat {
139159
if accessoryView == nil {
140160
return maxHeightNoAccessory
141161
} else {
142162
return contentTopInset + Constants.accessoryHeight + contentBottomInset
143163
}
144164
}
165+
166+
var secondaryAccessoryViewHeight: CGFloat {
167+
guard let secondaryAccessoryView else {
168+
return 0.0
169+
}
170+
171+
let secondaryAccessoryViewSize = secondaryAccessoryView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
172+
return secondaryAccessoryViewSize.height
173+
}
174+
175+
var maxHeight: CGFloat {
176+
return accessoryViewHeight + secondaryAccessoryViewHeight
177+
}
178+
145179
private var maxHeightNoAccessory: CGFloat {
146180
if traitCollection.verticalSizeClass == .compact {
147181
return traitCollection.horizontalSizeClass == .compact ? Constants.maxHeightNoAccessoryCompact : Constants.maxHeightNoAccessoryCompactForLargePhone
@@ -186,6 +220,9 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
186220
}
187221

188222
private let contentStackView = UIStackView()
223+
private var contentStackViewHeightConstraint: NSLayoutConstraint?
224+
private let secondaryContentStackView = UIStackView()
225+
private var secondaryContentStackViewTopAnchorConstraint: NSLayoutConstraint?
189226
private let shadow = Separator()
190227

191228
private var needsShadow: Bool {
@@ -222,12 +259,45 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
222259

223260
private func initContentStackView() {
224261
contentStackView.isLayoutMarginsRelativeArrangement = true
262+
contentStackView.translatesAutoresizingMaskIntoConstraints = false
225263
addSubview(contentStackView)
226-
contentStackView.fitIntoSuperview(usingConstraints: true)
264+
265+
// When there is a accessoryView, the top anchor of the secondaryContentStackView should be equal to
266+
// the bottom anchor of contentStackView.
267+
if let secondaryContentStackViewTopAnchorConstraint {
268+
NSLayoutConstraint.deactivate([
269+
secondaryContentStackViewTopAnchorConstraint
270+
])
271+
}
272+
273+
let heightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: accessoryViewHeight)
274+
contentStackViewHeightConstraint = heightConstraint
275+
NSLayoutConstraint.activate([
276+
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
277+
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
278+
contentStackView.topAnchor.constraint(equalTo: topAnchor),
279+
contentStackView.bottomAnchor.constraint(equalTo: secondaryContentStackView.topAnchor),
280+
heightConstraint
281+
])
227282
updateContentInsets()
228283
contentStackView.addInteraction(UILargeContentViewerInteraction())
229284
}
230285

286+
private func initSecondaryContentStackView() {
287+
secondaryContentStackView.translatesAutoresizingMaskIntoConstraints = false
288+
addSubview(secondaryContentStackView)
289+
let topAnchorConstraint = secondaryContentStackView.topAnchor.constraint(equalTo: topAnchor)
290+
secondaryContentStackViewTopAnchorConstraint = topAnchorConstraint
291+
NSLayoutConstraint.activate([
292+
secondaryContentStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
293+
secondaryContentStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
294+
topAnchorConstraint,
295+
secondaryContentStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
296+
])
297+
298+
secondaryContentStackView.addInteraction(UILargeContentViewerInteraction())
299+
}
300+
231301
private func initShadow() {
232302
let shadowView = shadow
233303
shadowView.translatesAutoresizingMaskIntoConstraints = false

ios/FluentUI/Navigation/UINavigationItem+Navigation.swift

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import UIKit
88
@objc public extension UINavigationItem {
99
private struct AssociatedKeys {
1010
static var accessoryView: UInt8 = 0
11+
static var secondaryAccessoryView: UInt8 = 0
1112
static var titleAccessory: UInt8 = 0
1213
static var titleImage: UInt8 = 0
1314
static var topAccessoryView: UInt8 = 0
@@ -31,6 +32,18 @@ import UIKit
3132
}
3233
}
3334

35+
/// An wide accessory view that can be shown as a subview of ShyHeaderView but doesn't have leading, trailing
36+
/// and bottom insets. So it can appear as being part of the content view but still contract and expand as part of
37+
/// the shy header.
38+
var secondaryAccessoryView: UIView? {
39+
get {
40+
return objc_getAssociatedObject(self, &AssociatedKeys.secondaryAccessoryView) as? UIView
41+
}
42+
set {
43+
objc_setAssociatedObject(self, &AssociatedKeys.secondaryAccessoryView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
44+
}
45+
}
46+
3447
/// Defines an accessory shown after the title or subtitle in a navigation bar. When defined, this gives the indication that the title can be tapped to show additional information.
3548
var titleAccessory: NavigationBarTitleAccessory? {
3649
get {

0 commit comments

Comments
 (0)