使用带有 iOS 11 的大标题时调整条形按钮项目的位置

Adjust position of bar button item when using large titles with iOS 11

我正在使用带 iOS 11 的大标题导航栏,但是当我添加一个条形按钮项目时,它看起来很奇怪位于与原始标题导航栏相同的位置。我想在标题较大时将条形按钮项目向下移动,并在导航栏不再大时将其移回原来的位置。这样做的最佳方法是什么?

这张图片显示了条形按钮项目的奇怪位置

我可以使用 viewWillLayoutSubviews() 动态获取导航栏高度,但我无法使用 setTitlePositionAdjustment

更改栏按钮项目的位置
override func viewWillLayoutSubviews() {
    guard let navbarHeight = self.navigationController?.navigationBar.frame.height else{ return }
}

您要做的是设置 BarButtonItem 的标题位置调整。将以下行添加到 viewWillAppear 函数。使用 verticalhorizontal 值来获得您喜欢的 layout

navigationItem.rightBarButtonItem?.setTitlePositionAdjustment(.init(horizontal: 10, vertical: 20), for: UIBarMetrics.default)

https://developer.apple.com/documentation/uikit/uibarbuttonitem/1617149-settitlepositionadjustment

The good way is you can adjust the navigation title if its large so that your bar button will adjust automatically. Here is the code. Also iOS mail application does the same thing for your reference.

func adjustsTitle() {
 guard let font = UIFont(name: "Helvetica-Medium", size: 16) else { return }
 let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
 label.textColor = UIColor.black
 label.textAlignment = .center
 label.text = navigationItem.title
 label.adjustsFontSizeToFitWidth = true
 navigationItem.titleView = label
}

更新答案 如果您想在标题下方的按钮变大时调整它,那么在这种情况下,您需要在导航栏上加载自定义视图。

//Hide back button. Since you are going to have custom button
navigationItem.hidesBackButton = true
//Increase the height based on your view intrinsic content size
navigationController?.navigationBar.frame.size.height = 100
guard let yourCustomView = UINib(nibName: "yourCustomXib", bundle: nil).instantiate(withOwner: nil, options: nil).first as? YourCustomView else {
 fatalError("Missing yourCustomXib")
}
navigationController?.navigationBar.addSubview(yourCustomView)

为了解决我自己的问题,我只是添加了一个按钮作为导航栏的子视图,并为导航栏设置了右侧和底部约束。当导航栏改变大小时,按钮现在会上下移动。但是,这需要在您从该视图控制器显示 segue 的任何视图控制器中删除该按钮。因此,我向按钮添加了 1 的标签,并将它从另一个视图控制器的超级视图中删除。这是最简单的解决方法,也是我发现的最简单的方法。

要设置右按钮:

func setupNavBar() {

    self.title = "Home"
    self.navigationController?.navigationBar.prefersLargeTitles = true
    self.navigationController?.navigationBar.isTranslucent = false

    let searchController = UISearchController(searchResultsController: nil)
    self.navigationItem.searchController = searchController

    let rightButton = UIButton()
    rightButton.setTitle("Right Button", for: .normal)
    rightButton.setTitleColor(.purple, for: .normal)
    rightButton.addTarget(self, action: #selector(rightButtonTapped(_:)), for: .touchUpInside)
    navigationController?.navigationBar.addSubview(rightButton)
    rightButton.tag = 1
    rightButton.frame = CGRect(x: self.view.frame.width, y: 0, width: 120, height: 20)

    let targetView = self.navigationController?.navigationBar

    let trailingContraint = NSLayoutConstraint(item: rightButton, attribute:
        .trailingMargin, relatedBy: .equal, toItem: targetView,
                         attribute: .trailingMargin, multiplier: 1.0, constant: -16)
    let bottomConstraint = NSLayoutConstraint(item: rightButton, attribute: .bottom, relatedBy: .equal,
                                    toItem: targetView, attribute: .bottom, multiplier: 1.0, constant: -6)
    rightButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([trailingContraint, bottomConstraint])

}

要将其从任何 show segued 视图控制器中删除:

func removeRightButton(){
    guard let subviews = self.navigationController?.navigationBar.subviews else{return}
    for view in subviews{
        if view.tag != 0{
            view.removeFromSuperview()
        }
    }
} 

viewWillAppear 函数中调用了这两个函数

我进行了一些挖掘,最终得出了与“消息”应用程序中完全相同的行为(这意味着按钮位于导航栏下方而不是上方)。 唯一缺少的部分是当 UIBarButtonItem 出现时发生的漂亮 animation/blur 事情……

公平警告:我当前的解决方案是使用私有 class(名为 _UINavigationBarLargeTitleView),Apple 可能会因此拒绝您的应用程序……

// Make sure you have a `navigationBar`
guard let navigationBar = navigationController?.navigationBar else {
    return
}
// Make sure you get the correct class from the string, the class itself is not exposed…
guard let UINavigationBarLargeTitleView = NSClassFromString("_UINavigationBarLargeTitleView") else {
    return
}
// Then, you need to find the subview of type `_UINavigationBarLargeTitleView` :
navigationBar.subviews.forEach { subview in
    if subview.isKind(of: UINavigationBarLargeTitleView.self) {
        // If you have it, add whatever button you want (some example below)
        subview.addSubview(largeTitleViewRightBarButton)

        // Constrain it as you want
        NSLayoutConstraint.activate([
            largeTitleViewRightBarButton.bottomAnchor.constraint(equalTo: subview.bottomAnchor, constant: -10),
            largeTitleViewRightBarButton.trailingAnchor.constraint(
                equalTo: subview.trailingAnchor,
                constant: -view.directionalLayoutMargins.trailing
            )
        ])
    }
}

// Finally, the magic happens with one scrollView delegate method :
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.contentOffset.y >= -103 { // Moving up
        navigationItem.rightBarButtonItem = rightBarButtonItem
    } else { // Moving down
        navigationItem.rightBarButtonItem = nil
    }
}

这是我制作按钮的方法:

private(set) lazy var image: UIImage? = {
    let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .semibold, scale: .default)
    let image = UIImage(systemName: "magnifyingglass.circle.fill", withConfiguration: config)

    return image
}()

private(set) lazy var largeTitleViewRightBarButton: UIButton = {
    let button = UIButton(type: .custom)

    button.translatesAutoresizingMaskIntoConstraints = false
    button.imageView?.tintColor = R.color.appDodgerBlue()
    button.setImage(image, for: .normal)
    button.addTarget(presenter, action: #selector(presenter.onSearchRequested), for: .touchUpInside)

    return button
}()

private(set) lazy var rightBarButtonItem: UIBarButtonItem = {
    let barButtonItem = UIBarButtonItem(
        image: image,
        style: .plain,
        target: presenter,
        action: #selector(presenter.onSearchRequested)
    )
    return barButtonItem
}()

Demo

如果有人仍在寻找如何在 SwiftUI 中执行此操作。我做了一个 package named NavigationBarLargeTitleItems 来处理这个问题。它模仿您在 AppStore 和 Messages-app.

中看到的行为

在此示例中,您将看到个人资料图标,但您也可以使用创建 Recipe-button 等文本。如果你想让它在滚动时立即显示为标准导航栏项目,你需要 GeometryReader。如果你喜欢一个例子,请告诉我。

请注意,为了能够完成此行为,我们需要添加到私有 class 的“_UINavigationBarLargeTitleView”,因此您的应用在提交到 App Store 时可能会被拒绝.

我还为那些不喜欢链接或只想copy/paste的人提供了完整的相关源代码。

分机:

// Copyright © 2020 Mark van Wijnen
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import SwiftUI

public extension View {
    func navigationBarLargeTitleItems<L>(trailing: L) -> some View where L : View {
        overlay(NavigationBarLargeTitleItems(trailing: trailing).frame(width: 0, height: 0))
    }
}

fileprivate struct NavigationBarLargeTitleItems<L : View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = Wrapper
    
    private let trailingItems: L
    
    init(trailing: L) {
        self.trailingItems = trailing
    }
    
    func makeUIViewController(context: Context) -> Wrapper {
        Wrapper(representable: self)
    }
    
    func updateUIViewController(_ uiViewController: Wrapper, context: Context) {
    }
    
    class Wrapper: UIViewController {
        private let representable: NavigationBarLargeTitleItems?
        
        init(representable: NavigationBarLargeTitleItems) {
            self.representable = representable
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder: NSCoder) {
            self.representable = nil
            super.init(coder: coder)
        }
                
        override func viewWillAppear(_ animated: Bool) {
            guard let representable = self.representable else { return }
            guard let navigationBar = self.navigationController?.navigationBar else { return }
            guard let UINavigationBarLargeTitleView = NSClassFromString("_UINavigationBarLargeTitleView") else { return }
           
            navigationBar.subviews.forEach { subview in
                if subview.isKind(of: UINavigationBarLargeTitleView.self) {
                    let controller = UIHostingController(rootView: representable.trailingItems)
                    controller.view.translatesAutoresizingMaskIntoConstraints = false
                    subview.addSubview(controller.view)
                    
                    NSLayoutConstraint.activate([
                        controller.view.bottomAnchor.constraint(
                            equalTo: subview.bottomAnchor,
                            constant: -15
                        ),
                        controller.view.trailingAnchor.constraint(
                            equalTo: subview.trailingAnchor,
                            constant: -view.directionalLayoutMargins.trailing
                        )
                    ])
                }
            }
        }
    }
}

用法:

import SwiftUI
import NavigationBarLargeTitleItems

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                ForEach(1..<50) { index in
                    Text("Sample Row \(String(index))")
                }
            }
            .navigationTitle("Navigation")
            .navigationBarLargeTitleItems(trailing: ProfileIcon())
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct ProfileIcon: View {
    var body: some View{
        Button(action: {
            print("Profile button was tapped")
        }) {
            Image(systemName: "person.circle.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.red)
                .frame(width: 36, height: 36)
        }
        .offset(x: -20, y: 5)
    }
}

预览