在 iOS 11 中使用大标题时调整栏按钮项的位置

Posted

技术标签:

【中文标题】在 iOS 11 中使用大标题时调整栏按钮项的位置【英文标题】:Adjust position of bar button item when using large titles with iOS 11 【发布时间】:2017-07-26 05:13:39 【问题描述】:

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

这是显示栏按钮项的奇怪位置的图像

我可以使用 viewWillLayoutSubviews() 动态获取导航栏高度,但我无法使用 setTitlePositionAdjustment 更改栏按钮项的位置

override func viewWillLayoutSubviews() 
    guard let navbarHeight = self.navigationController?.navigationBar.frame.height else return 

【问题讨论】:

虽然很糟糕,但这是整个 iOS 中使用的“标准”导航视图 UI。你真的想偏离苹果自己的应用程序设定的标准吗? (例如,转到 Mail 的邮箱页面——它使用相同的视图。) @BallpointBen 新应用商店的 UI 在导航栏上有一个向下移动的按钮,所以我不明白为什么会出现问题。 True... 在您看到 Apple 引入的硬件与此 UI 更改一起出现之前,您可能希望推迟此操作。或者至少将更改限制在 7 和更早版本。 【参考方案1】:

为了解决我自己的问题,我只是添加了一个按钮作为导航栏的子视图,并为导航栏设置了右侧和底部约束。现在,当导航栏改变大小时,按钮将上下移动。但是,这需要在您从该视图控制器中显示的任何视图控制器中删除该按钮。因此,我向按钮添加了一个标签 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 elsereturn
    for view in subviews
        if view.tag != 0
            view.removeFromSuperview()
        
    
 

两个函数都在 viewWillAppear 函数中调用

【讨论】:

视图出现后导航栏跳来跳去有什么问题吗?当视图出现时,我的导航栏向上移动了几个像素。否则效果很好! 任何人在使用 iOS 14.6 时遇到此问题?它在调试模式下工作正常,但在生产中卡在大标题中【参考方案2】:

我进行了一些挖掘,最终得出了与 Messages 应用程序中完全相同的行为(这意味着按钮位于导航栏下方而不是上方)。 唯一缺少的部分是UIBarButtonItem 出现时发生的漂亮动画/模糊...

公平警告:我目前的解决方案是使用私有类(名为 _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

【讨论】:

【参考方案3】:

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

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

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

【讨论】:

这对我不起作用。栏按钮项目的位置没有改变。我还想知道如何检测导航栏何时显示大标题以及何时显示原始标题,以便我可以动态更改位置。感谢您的帮助! 您可以观察navigationController.navigationBar.frame,以便在高度变化时得到通知。【参考方案4】:

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

在此示例中,您将看到一个配置文件图标,但您也可以使用创建食谱按钮等文本。如果您想让它在滚动后立即显示为标准导航栏项目,则需要 GeometryReader。如果您喜欢示例,请告诉我。

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

我还在这里为那些不喜欢链接或只想复制/粘贴的人提供完整的相关源代码。

扩展:

// 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)
    

预览

【讨论】:

【参考方案5】:

如果导航标题很大,您可以调整导航标题,以便您的栏 按钮会自动调整。这是代码。还有 iOS 邮件 应用程序做同样的事情供您参考。

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)

【讨论】:

什么是 DLLabel?我尝试改用 UILabel,但它并没有改变任何关于标题的内容。 我的意思是 UILabel 这仍然没有改变标题视图,仍然出现正常的大标题。 你使用了adjustsFontSizeToFitWidth吗??因为基于固定帧它会自动调整 我确实用过。虽然,我相信导航项甚至没有将 titleView 设置为新标签,因为我试图更改颜色和框架,但什么也没发生

以上是关于在 iOS 11 中使用大标题时调整栏按钮项的位置的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 中调整导航栏项的位置

如何防止在android片段中按下后退按钮时调用onCreateView

从具有按钮的 ListView 获取列表项的位置

如何在可能的导航控制器中更改条形按钮项的垂直位置和大小?

ios/xcode:更改条形按钮项的标题

当用户点击屏幕上的任意位置时调用函数