Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wide accessory view to ShyHeaderView #2080

Merged
merged 6 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class NavigationControllerDemoController: DemoController {
addTitle(text: "Large Title with Primary style")
container.addArrangedSubview(createButton(title: "Show without accessory", action: #selector(showLargeTitle)))
container.addArrangedSubview(createButton(title: "Show with collapsible search bar", action: #selector(showLargeTitleWithShyAccessory)))
container.addArrangedSubview(createButton(title: "Show with collapsible search bar and pill segmented control", action: #selector(showLargeTitleWithShyAccessoryAndSecondaryAccessory)))
container.addArrangedSubview(createButton(title: "Show with fixed search bar", action: #selector(showLargeTitleWithFixedAccessory)))
container.addArrangedSubview(createButton(title: "Show without an avatar", action: #selector(showLargeTitleWithoutAvatar)))
container.addArrangedSubview(createButton(title: "Show with a custom leading button", action: #selector(showLargeTitleWithCustomLeadingButton)))
Expand Down Expand Up @@ -56,6 +57,9 @@ class NavigationControllerDemoController: DemoController {
addTitle(text: "Top Accessory View")
container.addArrangedSubview(createButton(title: "Show with top search bar for large screen width", action: #selector(showWithTopSearchBar)))

addTitle(text: "Top Accessory View with shy wide accessory view")
container.addArrangedSubview(createButton(title: "Show with top search bar for large screen width with a shy pill segment control", action: #selector(showWithTopSearchBarWithShySecondaryAccessoryView)))

addTitle(text: "Change Style Periodically")
container.addArrangedSubview(createButton(title: "Change the style every second", action: #selector(showSearchChangingStyleEverySecond)))
}
Expand Down Expand Up @@ -87,6 +91,10 @@ class NavigationControllerDemoController: DemoController {
presentController(withTitleStyle: .largeLeading, accessoryView: createAccessoryView(), contractNavigationBarOnScroll: true)
}

@objc func showLargeTitleWithShyAccessoryAndSecondaryAccessory() {
presentController(withTitleStyle: .largeLeading, accessoryView: createAccessoryView(), secondaryAccessoryView: createSecondaryAccessoryView(), contractNavigationBarOnScroll: true)
}

@objc func showLargeTitleWithFixedAccessory() {
presentController(withTitleStyle: .largeLeading, accessoryView: createAccessoryView(), contractNavigationBarOnScroll: false)
}
Expand Down Expand Up @@ -189,6 +197,10 @@ class NavigationControllerDemoController: DemoController {
presentController(withTitleStyle: .largeLeading, style: .system, accessoryView: createAccessoryView(with: .onSystemNavigationBar), showsTopAccessory: true, contractNavigationBarOnScroll: false)
}

@objc func showWithTopSearchBarWithShySecondaryAccessoryView() {
presentController(withTitleStyle: .largeLeading, style: .system, accessoryView: createAccessoryView(with: .onSystemNavigationBar), secondaryAccessoryView: createSecondaryAccessoryView(), showsTopAccessory: true, contractNavigationBarOnScroll: true)
}

@objc func showSearchChangingStyleEverySecond() {
presentController(withTitleStyle: .largeLeading, style: .system, accessoryView: createAccessoryView(with: .onSystemNavigationBar), showsTopAccessory: true, contractNavigationBarOnScroll: false, updateStylePeriodically: true)
}
Expand All @@ -207,6 +219,7 @@ class NavigationControllerDemoController: DemoController {
subtitle: String? = nil,
style: NavigationBar.Style = .primary,
accessoryView: UIView? = nil,
secondaryAccessoryView: UIView? = nil,
showsTopAccessory: Bool = false,
contractNavigationBarOnScroll: Bool = true,
showShadow: Bool = true,
Expand All @@ -219,6 +232,7 @@ class NavigationControllerDemoController: DemoController {
content.navigationItem.navigationBarStyle = style
content.navigationItem.navigationBarShadow = showShadow ? .automatic : .alwaysHidden
content.navigationItem.accessoryView = accessoryView
content.navigationItem.secondaryAccessoryView = secondaryAccessoryView
content.navigationItem.topAccessoryViewAttributes = NavigationBarTopSearchBarAttributes()
content.navigationItem.contentScrollView = contractNavigationBarOnScroll ? content.tableView : nil
content.showsTopAccessoryView = showsTopAccessory
Expand Down Expand Up @@ -295,6 +309,16 @@ class NavigationControllerDemoController: DemoController {
return searchBar
}

private func createSecondaryAccessoryView() -> UIView {
let segmentControl = createSegmentedControl(compatibleWith: .system)
let stackView = UIStackView()
stackView.addArrangedSubview(segmentControl)
stackView.layoutMargins = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.backgroundColor = view.fluentTheme.color(.background1)
return stackView
}

private func createSegmentedControl(compatibleWith style: NavigationBar.Style) -> UIView {
let segmentItems: [SegmentItem] = [
SegmentItem(title: "First"),
Expand Down Expand Up @@ -481,6 +505,7 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
}

var showsTopAccessoryView: Bool = false
var secondaryAccessoryView: UIView?

var personaData: PersonaData = {
let personaData = PersonaData(name: "Kat Larsson", image: UIImage(named: "avatar_kat_larsson"))
Expand Down Expand Up @@ -835,13 +860,20 @@ class RootViewController: UIViewController, UITableViewDataSource, UITableViewDe
extension RootViewController: SearchBarDelegate {
func searchBarDidBeginEditing(_ searchBar: SearchBar) {
searchBar.progressSpinner.state.isAnimating = false
if navigationItem.secondaryAccessoryView != nil && !showsTopAccessoryView {
secondaryAccessoryView = navigationItem.secondaryAccessoryView
navigationItem.secondaryAccessoryView = nil
}
}

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

func searchBarDidCancel(_ searchBar: SearchBar) {
searchBar.progressSpinner.state.isAnimating = false
if secondaryAccessoryView != nil && !showsTopAccessoryView {
navigationItem.secondaryAccessoryView = secondaryAccessoryView
}
}

func searchBarDidRequestSearch(_ searchBar: SearchBar) {
Expand Down
11 changes: 9 additions & 2 deletions ios/FluentUI/Navigation/Shy Header/ShyHeaderController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ShyHeaderController: UIViewController {
}

private var accessoryViewObservation: NSKeyValueObservation?
private var secondaryAccessoryViewObservation: NSKeyValueObservation?

private var navigationBarCenterObservation: NSKeyValueObservation?
private var navigationBarStyleObservation: NSKeyValueObservation?
Expand Down Expand Up @@ -101,6 +102,10 @@ class ShyHeaderController: UIViewController {
accessoryViewObservation = contentViewController.navigationItem.observe(\UINavigationItem.accessoryView) { [weak self] item, _ in
self?.shyHeaderView.accessoryView = item.accessoryView
}

secondaryAccessoryViewObservation = contentViewController.navigationItem.observe(\UINavigationItem.secondaryAccessoryView) { [weak self] item, _ in
self?.shyHeaderView.secondaryAccessoryView = item.secondaryAccessoryView
}
}

required init?(coder aDecoder: NSCoder) {
Expand Down Expand Up @@ -211,8 +216,10 @@ class ShyHeaderController: UIViewController {
}

private func setupShyHeaderView() {
huanluu marked this conversation as resolved.
Show resolved Hide resolved
shyHeaderView.accessoryView = contentViewController.navigationItem.accessoryView
shyHeaderView.navigationBarShadow = contentViewController.navigationItem.navigationBarShadow
let navigationItem = contentViewController.navigationItem
shyHeaderView.accessoryView = navigationItem.accessoryView
shyHeaderView.secondaryAccessoryView = navigationItem.secondaryAccessoryView
shyHeaderView.navigationBarShadow = navigationItem.navigationBarShadow
shyHeaderView.paddingView = paddingView
shyHeaderView.parentController = self
shyHeaderView.maxHeightChanged = { [weak self] in
Expand Down
74 changes: 72 additions & 2 deletions ios/FluentUI/Navigation/Shy Header/ShyHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
tokenSet.registerOnUpdate(for: self) { [weak self] in
self?.updateColors()
}
self.initSecondaryContentStackView()
}

override func willMove(toWindow newWindow: UIWindow?) {
Expand Down Expand Up @@ -125,6 +126,13 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
willSet {
accessoryView?.removeFromSuperview()
contentStackView.removeFromSuperview()
// When there is no accessoryView, the top anchor of the secondaryContentStackView should be equal to
// the top anchor of the parent view.
if let secondaryContentStackViewTopAnchorConstraint {
NSLayoutConstraint.activate([
secondaryContentStackViewTopAnchorConstraint
])
}
}
didSet {
if let newContentView = accessoryView {
Expand All @@ -135,13 +143,39 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
}
}

var maxHeight: CGFloat {
var secondaryAccessoryView: UIView? {
willSet {
secondaryAccessoryView?.removeFromSuperview()
}
didSet {
if let newContentView = secondaryAccessoryView {
secondaryContentStackView.addArrangedSubview(newContentView)
}
maxHeightChanged?()
}
}

var accessoryViewHeight: CGFloat {
if accessoryView == nil {
return maxHeightNoAccessory
} else {
return contentTopInset + Constants.accessoryHeight + contentBottomInset
}
}

var secondaryAccessoryViewHeight: CGFloat {
guard let secondaryAccessoryView else {
return 0.0
}

let secondaryAccessoryViewSize = secondaryAccessoryView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
return secondaryAccessoryViewSize.height
}

var maxHeight: CGFloat {
return accessoryViewHeight + secondaryAccessoryViewHeight
}

private var maxHeightNoAccessory: CGFloat {
if traitCollection.verticalSizeClass == .compact {
return traitCollection.horizontalSizeClass == .compact ? Constants.maxHeightNoAccessoryCompact : Constants.maxHeightNoAccessoryCompactForLargePhone
Expand Down Expand Up @@ -186,6 +220,9 @@ class ShyHeaderView: UIView, TokenizedControlInternal {
}

private let contentStackView = UIStackView()
private var contentStackViewHeightConstraint: NSLayoutConstraint?
private let secondaryContentStackView = UIStackView()
private var secondaryContentStackViewTopAnchorConstraint: NSLayoutConstraint?
private let shadow = Separator()

private var needsShadow: Bool {
Expand Down Expand Up @@ -222,12 +259,45 @@ class ShyHeaderView: UIView, TokenizedControlInternal {

private func initContentStackView() {
contentStackView.isLayoutMarginsRelativeArrangement = true
contentStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentStackView)
contentStackView.fitIntoSuperview(usingConstraints: true)

// When there is a accessoryView, the top anchor of the secondaryContentStackView should be equal to
// the bottom anchor of contentStackView.
if let secondaryContentStackViewTopAnchorConstraint {
NSLayoutConstraint.deactivate([
secondaryContentStackViewTopAnchorConstraint
])
}

let heightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: accessoryViewHeight)
contentStackViewHeightConstraint = heightConstraint
NSLayoutConstraint.activate([
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentStackView.topAnchor.constraint(equalTo: topAnchor),
contentStackView.bottomAnchor.constraint(equalTo: secondaryContentStackView.topAnchor),
heightConstraint
])
updateContentInsets()
contentStackView.addInteraction(UILargeContentViewerInteraction())
}

private func initSecondaryContentStackView() {
secondaryContentStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(secondaryContentStackView)
let topAnchorConstraint = secondaryContentStackView.topAnchor.constraint(equalTo: topAnchor)
secondaryContentStackViewTopAnchorConstraint = topAnchorConstraint
NSLayoutConstraint.activate([
secondaryContentStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
secondaryContentStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
topAnchorConstraint,
secondaryContentStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])

secondaryContentStackView.addInteraction(UILargeContentViewerInteraction())
}

private func initShadow() {
let shadowView = shadow
shadowView.translatesAutoresizingMaskIntoConstraints = false
Expand Down
13 changes: 13 additions & 0 deletions ios/FluentUI/Navigation/UINavigationItem+Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import UIKit
@objc public extension UINavigationItem {
private struct AssociatedKeys {
static var accessoryView: UInt8 = 0
static var secondaryAccessoryView: UInt8 = 0
static var titleAccessory: UInt8 = 0
static var titleImage: UInt8 = 0
static var topAccessoryView: UInt8 = 0
Expand All @@ -31,6 +32,18 @@ import UIKit
}
}

/// An wide accessory view that can be shown as a subview of ShyHeaderView but doesn't have leading, trailing
/// and bottom insets. So it can appear as being part of the content view but still contract and expand as part of
/// the shy header.
var secondaryAccessoryView: UIView? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.secondaryAccessoryView) as? UIView
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.secondaryAccessoryView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

/// 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.
var titleAccessory: NavigationBarTitleAccessory? {
get {
Expand Down
Loading