解决 WatchOS 10.5 SwiftUI 应用中 TabView 工具栏按钮丢失的问题

86 阅读2分钟

在这里插入图片描述

问题现象

在 WatchOS 10.5 中,以 SwiftUI 原生 TabView 容器为根视图的应用若在某个 Tab 子视图中发生导航行为,则在导航回退后 TabView 顶部工具栏里的按钮可能会消失不见:

在这里插入图片描述

如上图所示:TabView 左上角的 + 按钮在其 Tab 子视图进入导航再回退之后会消失的无影无踪。

而相同的代码在 WatchOS 11 上并没有类似的问题:

在这里插入图片描述

如果希望在 WatchOS 10.5 中能够摆脱此类问题的小伙伴们,可以继续观赏后续的解决之道。


代码剖析

WatchOS 应用的总体布局结构很简单,App 的根视图即为一个嵌在导航视图中的 TabView:

NavigationStack {
    TabView(selection: $sltingTab) {
        MainView()
            .tag(0)
            .tabItem {
                Label("Tempt", systemImage: "person.fill.xmark.rtl")
            }
        
        HistoryRecordsView()
            .tag(1)
            .tabItem {
                Label("History", systemImage: "clock")
            }
        
        SettingsView()
            .tag(2)
            .tabItem {
                Label("Settings", systemImage: "gearshape")
            }
    }
    .tabViewStyle(.verticalPage)
}

在上面代码中我们还为 TabView 应用了在 WatchOS 10+ 新加入的 verticalPage 样式,它用来取代即将在未来被抛弃的 carousel 样式:

在这里插入图片描述

TabView 内部的第一个子视图为 MainView。注意只有它的左上角工具栏中包含一个功能按钮,当其被点击后将会导航至 NewTemptingRecordView 视图里:

struct MainView: View {
    
    @Environment(\.modelContext) var modelContext
    @State private var isNavToNewTempting = false
    
    @Query var timingRecords: [TemptingRecord]
    
    var body: some View {
        Group {
            if timingRecords.isEmpty {
                Label("空空如也", systemImage: "bubbles.and.sparkles")
                    .foregroundStyle(.gray)
            } else {
                List {
                    ForEach(timingRecords) { record in
                        TimingRecordCell(record: record)
                    }
                }
            }
        }
        .navigationDestination(isPresented: $isNavToNewTempting) {
            NewTemptingRecordView()
        }
        .navigationTitle("主页")
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                Button(action: {
                    isNavToNewTempting = true
                }, label: {
                    Label("新增诱惑", systemImage: "plus.circle")
                })
                .fontWeight(.bold)
                .tint(.red)
            }
        }
    }
}

此外,每个 TemptingRecord 都对应一个 TimingRecordCell 视图,我们在它的内部实现了另一个导航操作:

struct TimingRecordCell: View {
    @Environment(\.modelContext) var modelContext
    let record: TemptingRecord
    
    @State private var isNavToRecordClosingView = false
    @State private var clock: Clock?
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: record.type.logoName)
                                
                TemptingDegreeView(degree: record.degree)
                
                Spacer()
                
                if let desc = record.desc {
                    Text(desc)
                        .font(.headline)
                        .foregroundStyle(.gray)
                }
            }
            .font(.title2.weight(.black))
            
            if let clock {
                EmbedCountClockCell(clock: clock)
                    .onChange(of: clock.state) {old,new in
                        if new == .finished {
                            // 若 Record.clock 已结束,则导航至封口视图
                            isNavToRecordClosingView = true
                        }
                        
                        try! modelContext.save(clock)
                        WidgetCenter.shared.reloadTimelines(ofKind: Common.widgetKind)
                    }
            }
        }
        .navigationDestination(isPresented: $isNavToRecordClosingView) {
            TimingRecordClosingView(record: record)
        }
    }
}

以上就是 TimingRecordCell 视图的源代码,从中可以看到:如果 Record 中的计时器已结束,我们则导航至 TimingRecordClosingView 视图去完成 Record 的封口操作。

就是这个导航在返回后造成了 MainView 工具栏左上角按钮消失的问题!

解决方法

综上代码所示,我们并没有在 Toolbar 按钮的显示上做过多的文章,只是简单的将它放在 TabView 顶部,仅此而已。

并且该问题在 WatchOS 11 上也是“销声匿迹”,所以有可能这是 WatchOS 10.5(WatchOS 10 的其它子版本没有测试过)中的一个 “Quirk (怪癖)”。

一种解决方法是将 TimingRecordCell 中的 navigationDestination 修改器放到 MainView 中调用。不过这样一来,将会造成代码逻辑的大幅度修改。

有没有其它更简单的办法呢?

其实,我们只要在 MainView 中放置一个“空”的 navigationDestination 修改器调用即可,即使它没有任何用处:

struct MainView: View {
    
    @Environment(\.modelContext) var modelContext
    @State private var isNavToNewTempting = false
    @Query var timingRecords: [TemptingRecord]
    
    var body: some View {
        Group {
            if timingRecords.isEmpty {
                Label("空空如也", systemImage: "bubbles.and.sparkles")
                    .foregroundStyle(.gray)
            } else {
                
                List {
                    ForEach(timingRecords) { record in
                        TimingRecordCell(record: record)
                    }
                }
            }
        }
        .navigationDestination(isPresented: $isNavToNewTempting) {
            NewTemptingRecordView()
        }
        .navigationDestination(for: TemptingRecord.self) { record in
            // 站桩 Stub 闭包,并不需要任何内容
        }
    }
}

以上是修改后的 MainView 视图,省略了无关代码。从中可以看到:我们为其添加了一个绝对不会参与导航的 navigationDestination 修改器,并且之前的导航代码逻辑原封不动,不需要做任何改变。

如此一来,现在我们的代码在 WatchOS 10.5 中也能“从容面对”了:

在这里插入图片描述

如果大家也遇到了类似的问题,希望可以帮到你们哦。

总结

在本篇博文中,我们讨论了 SwiftUI 应用 TabView 顶部工具栏按钮可能会在导航操作中消失的问题,这个问题出现在 WatchOS 10.5 中,但在 WatchOS 11 中并不存在。

感谢观赏,再会!8-)