本人已经使用 SwiftUI
开发了 简习录, 并修复了若干 BUG
. 在这些 BUG
中让人头痛的当属 NavigtaionView
了, 这篇文章将会向您报告其中的问题;
# 使用 UINavigationBarAppearance
在普通情况下我们会在 application(_, didFinishLaunchingWithOptions)
使用如下代码来对 UINavigationBar
进行配置:
UINavigationBar.appearance().isTranslucent = false
UINavigationBar.appearance().barTintColor = UIColor(named: "themeColor")
UINavigationBar.appearance().shadowImage = UIImage()
...
对于 SwiftUI
在 iOS 13.4
这些代码也会起到作用, 但是:
- 在
iOS 13.3
将会直接引起崩溃,并且通过crash文件无法分析到原因; - 如果在代码中一旦对
NavigationView
设置.navigationViewStyle(StackNavigationViewStyle())
,那么不论什么系统版本都会在这个NavigationView
将要展示的时候直接崩溃;
这里推荐使用 iOS 13
的新 API
进行设置,带有注释的代码示例如下:
let coloredAppearance = UINavigationBarAppearance()
// 设置为不透明
coloredAppearance.configureWithOpaqueBackground()
// 设置背景色(对于纯色的背景可以使用 backgroundColor 而不需要设置 backgroundImage )
coloredAppearance.backgroundColor = UIColor(named: "themeColor2")
// 完全可以不用设置 shadowColor, nil 或者 clear 为相同表现
coloredAppearance.shadowColor = .clear
let titleAttr: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white
]
// 设置 inline 状态下的 title 属性,large 状态对应的是 largeTitleTextAttributes
coloredAppearance.titleTextAttributes = titleAttr
// 这里还可以设置 titlePositionAdjustment 来控制标题的偏移量 UIOffset(horizontal: -10, vertical: 0)
// 设置返回按钮的样式
let backButtonAppearance = UIBarButtonItemAppearance()
// 字体样式
backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.white]
coloredAppearance.backButtonAppearance = backButtonAppearance
let backImage = UIImage(named: "back")?.withRenderingMode(.alwaysOriginal)
// 设置返回按钮 箭头 image, 注意两个都要设置任何一个为 nil,则使用系统的
coloredAppearance.setBackIndicatorImage(backImage, transitionMaskImage: backImage)
// 设置标准(large)状态下的外观属性,compactAppearance 如未设置将使用 standardAppearance 的样式
UINavigationBar.appearance().standardAppearance = coloredAppearance
// 描述当相关联的UIScrollView到达与导航栏邻接的边缘(导航栏的顶部边缘)时要使用的导航栏的外观属性。
// 如果未设置,则将使用修改后的standardAppearance。
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
# NavigationLink
的问题
新建一个 SwiftUI
工程,将 ContentView.swift
代码替换如下:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 30) {
NavigationLink(destination: secondView()) {
Text("直接使用NavigationLink")
}
}
.navigationBarTitle("Navigation BUG", displayMode: .inline)
.navigationBarItems(
trailing: NavigationLink(destination: secondView()) {
Text("Link")
})
}
}
func secondView() -> some View {
return Text("second").navigationBarTitle("Sencod", displayMode: .inline)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
当你在 iOS 13.4.x
和 iOS 13.1.x
的设备上运行后不论是点击 Link
还是 直接使用NavigationLink
, 然后点击返回都不会出现问题(?_? 13.1.? 上可能会出现 NavigationBar
无限叠加的问题,这个没有复现😑);
当你在 iOS 13.2
设备上运行时点击 直接使用NavigationLink
没任何问题, 但是点击 Link
然后返回,会在显示完转场动画后直接崩溃,控制台打印信息为:
*** Assertion failure in -[UINavigationController popToViewController:transition:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3900.12.16/UINavigationController.m:8129
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'
First throw call stack:
(
0 CoreFoundation 0x00007fff23c4f02e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff50b97b20 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23c4eda8 +[NSException raise:format:arguments:] + 88
3 Foundation 0x00007fff256c9b61 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
4 UIKitCore 0x00007fff4713d9b1 __57-[UINavigationController popToViewController:transition:]_block_invoke + 620
5 UIKitCore 0x00007fff4713d65e -[UINavigationController popToViewController:transition:] + 753
...
解决办法是使用 NavigationLink
的另外一种写法:
NavigationLink(destination: secondView(), isActive: $showDetail, label: { EmptyView() })
添加此段代码后的 ContentView
struct 的整体代码如下
struct ContentView: View {
@State private var showDetail = false
var body: some View {
NavigationView {
VStack(spacing: 30) {
NavigationLink(destination: secondView()) {
Text("直接使用NavigationLink")
}
NavigationLink(destination: secondView(), isActive: $showDetail, label: { EmptyView() })
Button(action: {
self.showDetail = true
}) {
Text("使用isActive和EmptyView")
}
}
.navigationBarTitle("Navigation BUG", displayMode: .inline)
.navigationBarItems(
leading: Button("Empty") {
self.showDetail = true
},
trailing: NavigationLink(destination: secondView()) {
Text("Link")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
func secondView() -> some View {
return Text("second").navigationBarTitle("Sencod", displayMode: .inline)
}
}
运行后点击 Empty
和 使用isActive和EmptyView
跳转并返回无任何问题, OK 这里宣告结束? NO! 我们高兴的太早了, 在 iOS 13.3
的设备中运行你会发现如果点击 Link
跳转并返回后再次点击 Link
将不会再跳转, 同样的点击 Empty
, 直接使用NavigationLink
, 使用isActive和EmptyView
, 也是如此!!!
程序是没崩溃可是我已经要崩溃了! 这个问题暂时无解了! 如果您有好的解决办法请务必留言通知,感谢.
开始写到完成程序只花了 4 天不到的时间,可是单是适配个 iOS 13.1
, iOS 13.2.x
, iOS 13.3
就已经在考验我的发量了;
所以为了不拔高自己的发际线, 在 简习录 第二次的版本更新中, 我不得不指定 iOS 13.4
可用, 但是这并不是完结。
# 在 transition 动画在 NavigationView
中的问题
整体代码如下, 问题件代码注释:
import SwiftUI
fileprivate extension AnyTransition {
static var moveToOpacity: AnyTransition {
let insertion = AnyTransition.move(edge: .bottom)
let removal = AnyTransition.opacity
return .asymmetric(insertion: insertion, removal: removal)
}
}
fileprivate class CModel: Identifiable {
let id = UUID()
}
struct BUGTransition: View {
var body: some View {
ShowOrHiddenAnimation2()
}
}
// NavigationView 内部包裹的 ZStcak 使用 ZStcak ==> transition无问题,navigationBar 无法遮挡
fileprivate struct ShowOrHiddenAnimation4: View {
@State private var model: CModel? = nil
var body: some View {
NavigationView {
ZStack {
Button("Tap Me") {
withAnimation(.easeOut(duration: 3)) {
self.model = CModel()
}
}
if self.model != nil {
FullScreenView1(model: $model)
}
}
.navigationBarTitle("xxx", displayMode: .inline)
}
}
}
// ZStcak 内部包裹 NavigationView 后使用 Group ==> transition的insert无问题removal无效,navigationBar 完美遮挡
fileprivate struct ShowOrHiddenAnimation3: View {
@State private var model: CModel? = nil
var body: some View {
ZStack {
NavigationView {
Button("Tap Me") {
withAnimation(.easeOut(duration: 3)) {
self.model = CModel()
}
}
.navigationBarTitle("xxx", displayMode: .inline)
}
if self.model != nil {
/*
Color.pink.edgesIgnoringSafeArea(.all)
.transition(.moveToOpacity)
.animation(.easeOut(duration: 3))
.onTapGesture {
withAnimation(.easeOut(duration: 3)) {
self.model = nil
}
}
*/
FullScreenView2(model: $model)
}
}
}
}
// ZStcak 内部包裹 NavigationView 后使用 ZStack ==> transition的insert无问题removal无效,navigationBar 完美遮挡
fileprivate struct ShowOrHiddenAnimation2: View {
@State private var model: CModel? = nil
var body: some View {
ZStack {
NavigationView {
Button("Tap Me") {
withAnimation(.easeOut(duration: 3)) {
self.model = CModel()
}
}
.navigationBarTitle("xxx", displayMode: .inline)
}
if self.model != nil {
FullScreenView1(model: $model)
}
}
}
}
// ZStack 内部直接使用 ZStack ==> transition无问题,未使用 navigationBar
fileprivate struct ShowOrHiddenAnimation1: View {
@State private var model: CModel? = nil
var body: some View {
ZStack {
Button("Tap Me") {
withAnimation(.easeOut(duration: 3)) {
self.model = CModel()
}
}
if self.model != nil {
FullScreenView1(model: $model)
}
}
}
}
// 内部指定使用 ZStack
fileprivate struct FullScreenView1: View {
@Binding var model: CModel?
var body: some View {
ZStack {
Color.pink.edgesIgnoringSafeArea(.all)
}
.transition(.moveToOpacity)
.animation(.easeOut(duration: 3))
.onTapGesture {
withAnimation(.easeOut(duration: 3)) {
self.model = nil
}
}
}
}
// 使用 Group 包装,布局由外部决定
fileprivate struct FullScreenView2: View {
@Binding var model: CModel?
var body: some View {
Group {
Color.pink.edgesIgnoringSafeArea(.all)
NavigationView {
Text("FullScreenView2")
}
}
.transition(.moveToOpacity)
.animation(.easeOut(duration: 3))
.onTapGesture {
withAnimation(.easeOut(duration: 3)) {
self.model = nil
}
}
}
}
struct BUGTransition_Previews: PreviewProvider {
static var previews: some View {
BUGTransition()
}
}
同样的如果你下载了 官方教程demo ,找到
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
运行后也会发现 removal
效果在 NavigationView
中完全失效了🤭,这是官方彩蛋?
# 完结?
这里仅仅是列举了 NavigationView
中的一部分问题,
- 其他不是很严重的不再赘述;
- 和
NavigationView
不相关的也不在这篇中描述了(例如, 对使用UIViewRepresentable
的View
添加cornerRadius
修饰符会导致其“失去响应”🤥);
目前 简习录 已经支持 iOS 13
的全部系统版本.
最终为了完全适配 iOS 13.x
, 我不得不弃用 SwiftUI
中的 NavigationView
, 转而采用外部的 UINavigationController
设置 UIHostingController
为 rootVC
, UIHostingController
使用对应的 SwiftUI-View
来初始化的方式, 修复这些问题。