Image for Navigation Bar with Large Title iOS 11 Image for Navigation Bar with Large Title iOS 11 swift swift

Image for Navigation Bar with Large Title iOS 11


After several hours of coding, I finally managed to make it work. I also decided to write a detailed tutorial: link. Follow it in case you prefer very detailed instructions.

Demo:enter image description here

Complete project on GitHub: link.

Here are 5 steps to accomplish it:

Step 1: Create an image

private let imageView = UIImageView(image: UIImage(named: "image_name"))

Step 2: Add Constants

/// WARNING: Change these constants according to your project's designprivate 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}

Step 3: setup UI:

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)        ])}

Step 4: create image resizing method

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

Step 5:

override func scrollViewDidScroll(_ scrollView: UIScrollView) {    guard let height = navigationController?.navigationBar.frame.height else { return }    moveAndResizeImage(for: height)}

Hope it's clear and helps you!Please let me know in comments if you have any additional questions.


If anyone is still looking how to do this in SwiftUI. I made a package named NavigationBarLargeTitleItems to deal with this. It mimics the behavior you see in the AppStore and Messages-app.

Please note to be able to accomplish this behavior we need to add to the '_UINavigationBarLargeTitleView' which is a private class and therefor might get your app rejected when submitting to the App Store.

I'm also including the full relevant source code here for those who dislike links or just want to copy/paste.

Extension:

// 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 SwiftUIpublic 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                        )                    ])                }            }        }    }}

Usage:

import SwiftUIimport NavigationBarLargeTitleItemsstruct 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)    }}

Preview

enter image description here


You could create the UIBarButtonItem using a custom view. This custom view will be a UIView with the actual UIButton (as a subview) placed x pixels from the top (x=the number of pixels you want to move it down).