iOS 导航栏方案框架 NXNavigationExtension(SwiftUI 教程)

2,565 阅读8分钟

🔥 NXNavigationExtension 是为 iOS 应用设计的一个轻量级的导航栏处理框架,同时支持 SwiftUI 和 UIKit。框架对现有代码入侵非常小,只需要简单的几个方法调用就可以满足大部分的应用场景。可能是最省心的 iOS 导航栏处理框架之一。NXNavigationExtension 框架本身和示例代码都已经适配暗黑模式可供大家参考。先附上地址: github.com/l1Dan/NXNav…,欢迎 Star 🌟 和 PR。

另外附上 UIKit 教程

🎉 预览

SwiftUI 路由完全自定义导航栏返回事件拦截修改导航栏外观

🌈 要求

最新版本最低支持 iOS 9.0

NXNavigationExtension VersionMinimum iOS TargetMinimum macOS TargetFrameworksNote
4.xiOS 9.0macOS 10.15SwiftUI、UIKit、macCatalystXcode13、SwiftUI 2.0
3.xiOS 9.0macOS 10.15UIKit、macCatalyst/
2.xiOS 11.0macOS 10.15UIKit、macCatalyst/

优点

  • API 设计通俗易懂,容易上手。
  • 没有继承关系,所有操作基于分类实现,低耦合。
  • 白名单模式,按需注册所使用的导航控制器,这样才不会影响所有的导航控制器外观。
  • 没有对原生导航栏视图层级进行修改,无需担心系统升级的兼容性问题。
  • 适配 iOS、iPadOS、macOS、横竖屏切换、暗黑模式。
  • 提供 SwiftUI、UIKit、macCatalyst 框架的支持。
  • 支持 CocoaPods、Carthage、Project、Swift Package Manager 方式集成。

👏 功能

下面这些特别实用的功能,总有一部分适合你的项目

基本功能

  • 设置导航栏透明
  • 实现系统导航栏模糊效果
  • 自定义返回按钮图片
  • 自定义返回按钮
  • 自定义导航栏模糊背景
  • 修改返回按钮箭头颜色
  • 修改系统返回按钮文字
  • 修改导航栏标题颜色
  • 修改导航栏背景颜色
  • 修改导航栏背景图片
  • 修改导航栏底部线条颜色
  • 修改导航栏底部线条图片

高级功能

  • 禁用滑动返回手势
  • 启用全屏滑动返回手势
  • 导航栏返回事件拦截
  • 重定向任一控制器跳转
  • SwiftUI 路由
  • 导航栏点击事件穿透到底部
  • 动态修改导航栏样式
  • 更新导航栏样式
  • 渐变导航栏样式
  • 长按返回按钮显示菜单功能
  • 更多功能请查看示例代码...

🍽 使用

🌟 开始使用

下载 NXNavigationExtension 示例代码。

使用 CocoaPods 集成

使用 CocoaPods 将 NXNavigationExtension 集成到 Xcode 项目中,需要在 Podfile 中指定:

## For SwiftUI
pod 'NXNavigationExtension/SwiftUI'

## For UIKit
pod 'NXNavigationExtension'

使用 Carthage 管理

使用 Carthage 管理 NXNavigationExtension framework,请将以下内容添加到您的 Cartfile 文件中:

# For SwiftUI
github "l1Dan/NXNavigationExtension" # Requires
github "l1Dan/NXNavigationExtensionSwiftUI"

# For UIKit
github "l1Dan/NXNavigationExtension"

使用 Swift Package Manager 集成

使用 Swift Package Manager 集成 NXNavigationExtension,请将以下内容添加到您的 Package.swift 文件的依赖中:

dependencies: [
    .package(url: "https://github.com/l1Dan/NXNavigationExtension.git", .upToNextMajor(from: "4.0.2"))
]

NXNavigationExtensionSwiftUINXNavigationExtension 框架提供强力支持,他们的功能基本保持一致。注册完成之后需要在 NavigationView 中指定使用 .navigationViewStyle(.stack) 风格,但是目前仅支持 iOS 14 及以上系统的 StackNavigationViewStyle 风格,其他系统和 NavigationViewStyle 后续会不断完善。

下面是框架对 StackNavigationViewStyle 风格和 iOS 系统版本的支持情况:

NavigationViewStyle / iOS versioniOS 13iOS 14iOS 15
.automatic
.stack
.columns
  1. 💉 导入模块。

    • 使用 CocoaPods 集成:import NXNavigationExtension
    • 使用 Carthage 管理:import NXNavigationExtensionSwiftUI
  2. 💉 使用之前需要先在 AppDelegate 中注册需要修改的导航控制器。

✅ 推荐

UIKit 版本中其实只需要 NXNavigationConfiguration().registerNavigationControllerClasses([YourNavigationController.self]) 这一行代码就完成导航控制器的注册了。但是在 SwiftUI 版本中还需要一个额外的步骤:指定 NXNavigationVirtualWrapperView 的查找规则,他是 SwiftUI 与 UIKit 之间的桥梁。查找规则开发者可以自定义,也可以使用框架提供的默认查找规则。

// AppDelegate.swift
var classes: [AnyClass] = []
    if #available(iOS 15.0, *) {
        classes = [
            NSClassFromString("SwiftUI.SplitViewNavigationController"),
            NSClassFromString("SwiftUI.UIKitNavigationController"),
        ].compactMap { $0 }
    } else {
        classes = [
            NSClassFromString("SwiftUI.SplitViewNavigationController"), // iOS14
        ].compactMap { $0 }
    }

    let defaultConfiguration = NXNavigationConfiguration.default
    defaultConfiguration.registerNavigationControllerClasses(classes) { navigationController, configuration in
        // Configure
        navigationController.nx_applyFilterNavigationVirtualWrapperViewRuleCallback(NXNavigationVirtualView.configureWithDefaultRule(for:))
        return configuration
}
// Example: ContentView.swift
import SwiftUI
import NXNavigationExtension

struct DestinationView: View {
    @State private var context: NXNavigationRouter.Context = NXNavigationRouter.Context(routeName: "/destinationView")

    var body: some View {
        Button {
            // NXNavigationRouter.of(context).pop()
            NXNavigationRouter.of(context /* /destinationView */).popUntil("/contentView")
        } label: {
            Text("Pop")
                .padding()
        }
        .useNXNavigationView(context: $context, onPrepareConfiguration: { configuration in
            // `DestinationView` NavigationView backgroundColor
            configuration.navigationBarAppearance.backgroundColor = .red
        })
    }

}

struct ContentView: View {
    @State private var context: NXNavigationRouter.Context = NXNavigationRouter.Context(routeName: "/contentView")

    var body: some View {
        NavigationView {
            NavigationLink { // 1. 使用 NavigationView 包装
                DestinationView()
            } label: {
                Text("Push")
                    .padding()
                    .useNXNavigationView(context: $context /* /contentView */, onPrepareConfiguration: { configuration in
                        // 3. 修改导航栏背景颜色 ... `Text` NavigationView backgroundColor
                        configuration.navigationBarAppearance.backgroundColor = .brown
                    })
            }
        }
        .navigationViewStyle(.stack) // 2. 使用 StackNavigationViewStyle 风格
    }
}

🍻 基本功能

修改返回按钮箭头颜色

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.tintColor = .red
    })

修改系统返回按钮文字

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.useSystemBackButton = true
        configuration.navigationBarAppearance.systemBackButtonTitle = title
    })

修改导航栏标题颜色

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black]
    })

修改导航栏背景颜色

导航栏背景颜色默认使用系统蓝色 UIColor.systemBlue,这样处理能够快速辨别框架是否生效,也可以使用以下方式进行重写:

📝 示例代码

// 全局统一修改
let configuration = NXNavigationConfiguration.default
configuration.navigationBarAppearance.backgroundColor = .red

// 基于视图控制器修改
Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backgroundColor = .red
    })

修改导航栏背景图片

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backgroundImage = UIImage(named: "NavigationBarBackground88")
    })

设置导航栏透明

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backgroundColor = .clear
        configuration.navigationBarAppearance.shadowColor = .clear
    })

实现系统导航栏模糊效果

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backgroundColor = .clear
        configuration.viewControllerPreferences.useBlurNavigationBar = true
    })

修改导航栏底部线条颜色

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backgroundColor = .systemBackground
        configuration.navigationBarAppearance.shadowColor = .systemRed
    })

修改导航栏底部线条图片

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backgroundColor = .systemBackground
        configuration.navigationBarAppearance.shadowImage = UIImage(named: "NavigationBarShadowImage")
    })

自定义返回按钮图片

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backImage = UIImage(systemName: "arrow.left")
    })

自定义返回按钮

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.backButtonCustomView = backButton
    })

🍺 高级功能

禁用滑动返回手势

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.viewControllerPreferences.disableInteractivePopGesture = true
    })

启用全屏滑动返回手势

📝 示例代码

  • 全局有效
let configuration = NXNavigationConfiguration.default
configuration.viewControllerPreferences.enableFullScreenInteractivePopGesture = true
  • 局部有效(在所处页面设置)
Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.viewControllerPreferences.enableFullScreenInteractivePopGesture = true
    })

导航栏返回事件拦截

📝 示例代码

  1. .callNXPopMethod: 调用 nx_pop 系列方法返回事件拦截。
  2. .backButtonAction: 点击返回按钮返回事件拦截。
  3. .backButtonMenuAction: 长按返回按钮选择菜单返回事件拦截。
  4. .popGestureRecognizer: 使用手势交互返回事件拦截。
Text("Destination")
    .useNXNavigationView(onWillPopViewController: { interactiveType in
        if selectedItemType == .backButtonAction && interactiveType == .backButtonAction ||
            selectedItemType == .backButtonMenuAction && interactiveType == .backButtonMenuAction ||
            selectedItemType == .popGestureRecognizer && interactiveType == .popGestureRecognizer ||
            selectedItemType == .callNXPopMethod && interactiveType == .callNXPopMethod ||
            selectedItemType == .all {
            isPresented = true
            return false
        }
        return true
    })

SwiftUI 路由

📝 示例代码

struct DestinationView: View {
    @State private var context: NXNavigationRouter.Context

    init() {
        context = NXNavigationRouter.Context(routeName: "/currentRouteName")
    }

    var body: some View {
        VStack {
            Button {
                NXNavigationRouter.of(context).pop()
            } label: {
                Text("Pop")
            }
            .useNXNavigationView(context: $context)
        }
    }
}
  1. 需要注意的是 NXNavigationRouter.of(context)NXNavigationRouter.of(context).nx 用于调用系统 pop 和框架 nx_pop 系列方法
  2. 使用 NXNavigationRouter.of(context).nx 方法弹出页面时会触发 onWillPopViewController 的回调。

导航栏点击事件穿透到底部

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.viewControllerPreferences.translucentNavigationBar = true
    })

更新导航栏样式

📝 示例代码

Button {
    NXNavigationRouter.of(context).setNeedsNavigationBarAppearanceUpdate()
} label: {
    Text("Update")
}

长按返回按钮显示菜单功能

📝 示例代码

Text("Destination")
    .useNXNavigationView(onPrepareConfiguration: { configuration in
        configuration.navigationBarAppearance.useSystemBackButton = true
    })

BackButtonMenu

FAQ 常见问题

Q:iOS14 及之后的版本为什么注册了 UIImagePickerControllerPHPickerViewController 类之后还是无法修改导航栏的外观?

A:因为 UIImagePickerControllerPHPickerViewController 里面的 UINavigationBar 是隐藏的,NXNavigationBar 会跟随系统导航栏隐藏与显示,所以无法修改(iOS14 之前系统的 UIImagePickerController 是可以修改的)。另外 PHPickerViewController 其实是一个 UIViewController 的子类,你既可以用 push 的方式显示控制器也可以用 present 的方式显示控制器,他们有个共同特点:使用的都是一个 “假” 的导航栏。


Q:为什么 iOS13 之前使用 self.navigationItem.searchController 设置的 UISearchBar 无法跟随导航栏的变化而变化,iOS13 之后的却可以呢?

A:因为在 iOS13 之前导航栏中不包含 UISearchBar,iOS13 之后导航栏才包含 UISearchBar 的。具体使用请参考示例代码


Q:如何解决 UIScrollViewUIPageViewController 全屏手势冲突?

A:使用 UIScrollViewUIPageViewController 全屏手势冲突解决方案。


Q:为什么 NXNavigationExtension 框架不包含控制器的转场动画功能?

A:原则就是尽可能的保持框架的简单轻量,将更多的精力花在框架本身的稳定性上,尽可能地使用系统原有功能。转场动画功能并不适用于所有业务场景,另外也不属于这个框架的功能。如果有转场动画的需求需要开发者自己实现,也可以参考VCTransitionsLibrary,或者参考示例代码


Q:为什么导航栏的系统返回按钮箭头和自定义返回按钮箭头的位置不一致?

A:因为导航栏的系统返回按钮是用 self.navigationItem.backBarButtonItem 属性来设置的。而自定义返回按钮是用 self.navigationItem.leftBarButtonItem 属性来设置的,他们的位置本来就不一样。当然你可以使用系统返回按钮,通过 (nx_)useSystemBackButton 属性设置是否使用系统返回按钮,再配合 (nx_)systemBackButtonTitle 属性设置系统返回按钮的标题。还可以通过 (nx_)backImageInsets 或者 (nx_)landscapeBackImageInsets 属性来控制自定义返回按钮图片的偏移量。

  • 返回按钮箭头在切图里尽量靠左而不要居中,右边可以保留透明背景。
  • 使用 nx_backButtonCustomView 属性自定义返回按钮时就需要开发者自己来修正箭头的偏移量了。

📄 协议

NXNavigationExtension 框架是在 MIT 许可下发布的。详情请参见 LICENSE