Изображение для панели навигации с большим заголовком iOS 11
Приложение AppStore имеет значок с изображением в правой части NabBar с большим заголовком:
Был бы очень признателен, если кто-нибудь знает, как это реализовать или идеи о том, как это сделать.
Кстати: установка изображения для UIButton внутри UIBarButtonItem не будет работать. Пробовал уже. Кнопка прилипает к верхней части экрана:
5 ответов
После нескольких часов кодирования мне наконец удалось заставить его работать. Я также решил написать подробное руководство: ссылка. Следуйте этому, если вы предпочитаете очень подробные инструкции.
Завершить проект на GitHub: ссылка.
Вот 5 шагов для достижения этой цели:
Шаг 1: Создать изображение
private let imageView = UIImageView(image: UIImage(named: "image_name"))
Шаг 2: Добавить константы
/// WARNING: Change these constants according to your project's design
private struct Const {
/// Image height/width for Large NavBar state
static let ImageSizeForLargeState: CGFloat = 40
/// Margin from right anchor of safe area to right anchor of Image
static let ImageRightMargin: CGFloat = 16
/// Margin from bottom anchor of NavBar to bottom anchor of Image for Large NavBar state
static let ImageBottomMarginForLargeState: CGFloat = 12
/// Margin from bottom anchor of NavBar to bottom anchor of Image for Small NavBar state
static let ImageBottomMarginForSmallState: CGFloat = 6
/// Image height/width for Small NavBar state
static let ImageSizeForSmallState: CGFloat = 32
/// Height of NavBar for Small state. Usually it's just 44
static let NavBarHeightSmallState: CGFloat = 44
/// Height of NavBar for Large state. Usually it's just 96.5 but if you have a custom font for the title, please make sure to edit this value since it changes the height for Large state of NavBar
static let NavBarHeightLargeState: CGFloat = 96.5
}
Шаг 3: настройка интерфейса:
private func setupUI() {
navigationController?.navigationBar.prefersLargeTitles = true
title = "Large Title"
// Initial setup for image for Large NavBar state since the the screen always has Large NavBar once it gets opened
guard let navigationBar = self.navigationController?.navigationBar else { return }
navigationBar.addSubview(imageView)
imageView.layer.cornerRadius = Const.ImageSizeForLargeState / 2
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.rightAnchor.constraint(equalTo: navigationBar.rightAnchor,
constant: -Const.ImageRightMargin),
imageView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor,
constant: -Const.ImageBottomMarginForLargeState),
imageView.heightAnchor.constraint(equalToConstant: Const.ImageSizeForLargeState),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor)
])
}
Шаг 4: создайте метод изменения размера изображения
private func moveAndResizeImage(for height: CGFloat) {
let coeff: CGFloat = {
let delta = height - Const.NavBarHeightSmallState
let heightDifferenceBetweenStates = (Const.NavBarHeightLargeState - Const.NavBarHeightSmallState)
return delta / heightDifferenceBetweenStates
}()
let factor = Const.ImageSizeForSmallState / Const.ImageSizeForLargeState
let scale: CGFloat = {
let sizeAddendumFactor = coeff * (1.0 - factor)
return min(1.0, sizeAddendumFactor + factor)
}()
// Value of difference between icons for large and small states
let sizeDiff = Const.ImageSizeForLargeState * (1.0 - factor) // 8.0
let yTranslation: CGFloat = {
/// This value = 14. It equals to difference of 12 and 6 (bottom margin for large and small states). Also it adds 8.0 (size difference when the image gets smaller size)
let maxYTranslation = Const.ImageBottomMarginForLargeState - Const.ImageBottomMarginForSmallState + sizeDiff
return max(0, min(maxYTranslation, (maxYTranslation - coeff * (Const.ImageBottomMarginForSmallState + sizeDiff))))
}()
let xTranslation = max(0, sizeDiff - coeff * sizeDiff)
imageView.transform = CGAffineTransform.identity
.scaledBy(x: scale, y: scale)
.translatedBy(x: xTranslation, y: yTranslation)
}
Шаг 5:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let height = navigationController?.navigationBar.frame.height else { return }
moveAndResizeImage(for: height)
}
Надеюсь, это понятно и поможет вам! Пожалуйста, дайте мне знать в комментариях, если у вас есть дополнительные вопросы.
Если кто-то еще ищет, как это сделать в SwiftUI. Я создал https://github.com/markvanwijnen/NavigationBarLargeTitleItems, чтобы справиться с этим. Он имитирует поведение, которое вы видите в AppStore и Messages-app.
Обратите внимание, что для реализации этого поведения нам необходимо добавить к "_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)
}
}
Предварительный просмотр
Хороший ответ о добавлении его в качестве подвида. Я бы добавил тот факт, что вы можете использовать чистую автоматическую верстку только без необходимостиCGAffineTransform
и все эти расчеты. Если вы также добавите вертикальные ограничения, он будет автоматически масштабироваться. Если вам все еще нужно использовать расчеты, вы можете использоватьnavigationController?.navigationBar.publisher(for: \.frame)
издатель вместо того, чтобы делать это внутри прокрутки. Таким образом, вы сможете сделать это более глобально, а не зависеть от вида прокрутки.
Вот как я это сделал, например (мне нужно было сделать это в начале и скрыть большой заголовок, но вы можете изменить эти ограничения, чтобы добавить его везде, где хотите):
- Добавлять
imageView
как свойство, так как мне также нужно скрыть его в некоторых случаях. (например, при открытии другого экрана)
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.kf.setImage(with: URL(string: "https://img.buzzfeed.com/buzzfeed-static/static/2021-07/21/15/campaign_images/b4661163b3f8/24-times-michael-scott-from-the-office-made-us-bu-2-7356-1626879661-2_dblbig.jpg?resize=1200:*")!)
imageView.cornerRadiusStyle = .heightFraction(1/2) // This is an extension in the codebase I'm working on but you can set the corner radius normally as you would. Inside layoutSubviews most probably.
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = true
return imageView
}()
- Настройте собственное изображение (убедитесь, что вы вызываете это ПОСЛЕ
navigationController
установлено и неnil
)
func setupCustomImage() {
// Adding imageView inside stackView just for convenience of hiding it later.
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(imageView)
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(lessThanOrEqualToConstant: 52), // In my case I needed max image size to be 52. You can change that.
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor) // I needed aspect ratio to be 1:1. You can change that also by adding multiplier.
])
guard let navigationBar = navigationController?.navigationBar else { return }
navigationBar.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor, constant: 16), // For leading padding
stackView.centerYAnchor.constraint(equalTo: navigationBar.centerYAnchor),
// You can play around with those constants as well to provide minimum size of the image needed.
navigationBar.bottomAnchor.constraint(greaterThanOrEqualTo: stackView.bottomAnchor, constant: 7),
stackView.topAnchor.constraint(greaterThanOrEqualTo: navigationBar.topAnchor, constant: 7)
])
}
Он автоматически сделает все масштабирование и прочее.
Благодаря @TungFam, я думаю, у меня есть лучшее решение. проверить это
две точки:
изменить рамку кнопки в зависимости от высоты панели навигации
// adjust topview height override func scrollViewDidScroll(_ scrollView: UIScrollView) { guard let navBar = self.navigationController?.navigationBar else { return } // hardcoded .. to improve if navBar.bounds.height > 44 + 40 + 10 { NSLayoutConstraint.deactivate(heightConstraint) heightConstraint = [topview.heightAnchor.constraint(equalToConstant: 40)] NSLayoutConstraint.activate(heightConstraint) } else { NSLayoutConstraint.deactivate(heightConstraint) var height = navBar.bounds.height - 44 - 10 if height < 0 { height = 0 } heightConstraint = [topview.heightAnchor.constraint(equalToConstant: height)] NSLayoutConstraint.activate(heightConstraint) } }
изменить кнопку альфа в зависимости от поп / толчок прогресс
@objc func onGesture(sender: UIGestureRecognizer) { switch sender.state { case .began, .changed: if let ct = navigationController?.transitionCoordinator { topview.alpha = ct.percentComplete } case .cancelled, .ended: return case .possible, .failed: break } }
Вы можете создать UIBarButtonItem с помощью пользовательского представления. Это пользовательское представление будет UIView с фактическим UIButton (в качестве подпредставления), размещенным на x пикселей сверху (x= количество пикселей, которое вы хотите переместить вниз).