在 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
函数。使用vertical
和horizontal
值来获得你喜欢的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 中使用大标题时调整栏按钮项的位置的主要内容,如果未能解决你的问题,请参考以下文章