Изображение для панели навигации с большим заголовком 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)издатель вместо того, чтобы делать это внутри прокрутки. Таким образом, вы сможете сделать это более глобально, а не зависеть от вида прокрутки.

Вот как я это сделал, например (мне нужно было сделать это в начале и скрыть большой заголовок, но вы можете изменить эти ограничения, чтобы добавить его везде, где хотите):

  1. Добавлять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
}()
  1. Настройте собственное изображение (убедитесь, что вы вызываете это ПОСЛЕ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, я думаю, у меня есть лучшее решение. проверить это

демонстрация

две точки:

  1. изменить рамку кнопки в зависимости от высоты панели навигации

      // 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)
    
        }
    
    }
    
  2. изменить кнопку альфа в зависимости от поп / толчок прогресс

    @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= количество пикселей, которое вы хотите переместить вниз).

Другие вопросы по тегам