fatbobman / blogComments

1 stars 0 forks source link

SwiftUI 4.0 的全新导航系统 | 肘子的Swift记事本 #151

Open fatbobman opened 2 years ago

fatbobman commented 2 years ago

https://www.fatbobman.com/posts/new_navigator_of_SwiftUI_4/

长久以来,开发者对 SwiftUI 的导航系统颇有微词。受 NavigationView 的能力限制,开发者需要动用各种技巧乃至黑科技才能实现一些本应具备基本功能(例如:返回根视图、向堆栈添加任意视图、返回任意层级视图 、Deep Link 跳转等 )。SwiftUI 4.0( iOS 16+ 、macOS 13+ )对导航系统作出了重大改变,提供了以视图堆栈为管理对象的新 API ,让开发者可以轻松实现编程式导航。本文将对新的导航系统作以介绍。

DouKing commented 2 years ago

请教:如何在分栏情况下在 sideBar 里面嵌套 TabView 呢

fatbobman commented 2 years ago

刚才看错问题了。

struct ContentView: View {
    @State var selection = 2
    var body: some View {
        NavigationSplitView(sidebar: {
            TabView(selection: $selection) {
                Text("1111")
                    .tag(1)
                    .tabItem {
                        Text("111")
                    }
                Text("2222")
                    .tag(2)
                    .tabItem {
                        Text("222")
                    }
            }
        }, detail: {
            Text("Hello world")
        })
    }
}
DouKing commented 2 years ago

设置栏宽度好像不起作用

struct ContentView: View {

    @State private var path: [Int] = []
    @State private var value: Int?

    var body: some View {
        NavigationSplitView {
            TabView {
                NavigationStack(path: $path) {
                    List(0..<10, id: \.self) { item in
                        NavigationLink("\(item)", value: item)
                    }
                    .navigationDestination(for: Int.self) { item in
                        Button("\(item)") {
                            value = item
                        }
                    }
                    .navigationTitle("test")
                }.tabItem {
                    Image(systemName: "house")
                    Text("Home")
                }

                Text("User").tabItem {
                    Image(systemName: "person")
                    Text("User")
                }

                Text("User").tabItem {
                    Image(systemName: "person")
                    Text("User")
                }
            }
            .navigationSplitViewColumnWidth(400) //不起作用
        } detail: {
            ZStack {
                if let value {
                    Text("\(value)")
                } else {
                    Text("Hello")
                }
            }
        }
    }
}
fatbobman commented 2 years ago

经我的测试,目前 sidebar 好像最大支持到 320.因此 tabview 无法显示完整。

2022年7月12日 11:46,DouKing @.***> 写道:

设置栏宽度好像不起作用

struct ContentView: View {

@State private var path: [Int] = []
@State private var value: Int?

var body: some View {
    NavigationSplitView {
        TabView {
            NavigationStack(path: $path) {
                List(0..<10, id: \.self) { item in
                    NavigationLink("\(item)", value: item)
                }
                .navigationDestination(for: Int.self) { item in
                    Button("\(item)") {
                        value = item
                    }
                }
                .navigationTitle("test")
            }.tabItem {
                Image(systemName: "house")
                Text("Home")
            }

            Text("User").tabItem {
                Image(systemName: "person")
                Text("User")
            }

            Text("User").tabItem {
                Image(systemName: "person")
                Text("User")
            }
        }
        .navigationSplitViewColumnWidth(400) //不起作用
    } detail: {
        ZStack {
            if let value {
                Text("\(value)")
            } else {
                Text("Hello")
            }
        }
    }
}

} — Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/151#issuecomment-1181281234, or unsubscribe https://github.com/notifications/unsubscribe-auth/ANIYIGMC5HKQHFRY3R3GAQ3VTTTAVANCNFSM5YVZHRWQ. You are receiving this because you authored the thread.

akring commented 1 year ago

我在 tvOS 中使用 NavigationStack 时,发现List点按触发导航的时候 .navigationDestination 会随机的调用 2 - 3 次,导致目的地 View 被初始化多次,一直无法找到问题何在。

    var body: some View {
        NavigationStack {
            List {
                ForEach(viewModel.userViewArray) { view in
                    NavigationLink(view.name ?? "", value: view)
                }
            }
            .onAppear {
                viewModel.requestUserViews()
            }
            .navigationDestination(for: ItemModel.self) { item in
                CategoryPage(viewModel: CategoryViewModel(itemID: item.id ?? ""))
            }
        }
    }
fatbobman commented 1 year ago

我在 tvOS 中使用 NavigationStack 时,发现List点按触发导航的时候 .navigationDestination 会随机的调用 2 - 3 次,导致目的地 View 被初始化多次,一直无法找到问题何在。

在 SwiftUI 中,无需太在意视图描述的实例( 注意,不是视图)被多次初始化。创建实例的过程是非常轻量级的( 因此不要在构造方法中使用任何会造成负担的操作 )。SwiftUI 可能会出于任意的目的多次创建实例,但通常只会用其中一个实例(通常是最后一个)来生成视图( 也就是通过该实例的 body 中来创建视图)。

例如,下面的代码,在 iPhone 下也会出现重复创建实例的情况,但 onAppear 只会打印一次( 视图在该次才正式被创建 )

struct StackTV:View {
    var body: some View{
        NavigationStack{
            List(0..<10){ i in
                NavigationLink("\(i)", value: i)
            }
            .navigationDestination(for: Int.self, destination: { i in
                StackSubView(i: i)
            })
        }
    }
}

struct StackSubView:View {
    let i : Int
    init(i:Int){
        self.i = i
        print("init \(i)")
    }
    var body: some View{
        Text("\(i)")
            .onAppear{
                print("onAppear")
            }
    }
}
akring commented 1 year ago

@fatbobman

我在 tvOS 中使用 NavigationStack 时,发现List点按触发导航的时候 .navigationDestination 会随机的调用 2 - 3 次,导致目的地 View 被初始化多次,一直无法找到问题何在。

在 SwiftUI 中,无需太在意视图描述的实例( 注意,不是视图)被多次初始化。创建实例的过程是非常轻量级的( 因此不要在构造方法中使用任何会造成负担的操作 )。SwiftUI 可能会出于任意的目的多次创建实例,但通常只会用其中一个实例(通常是最后一个)来生成视图( 也就是通过该实例的 body 中来创建视图)。

例如,下面的代码,在 iPhone 下也会出现重复创建实例的情况,但 onAppear 只会打印一次( 视图在该次才正式被创建 )

struct StackTV:View {
    var body: some View{
        NavigationStack{
            List(0..<10){ i in
                NavigationLink("\(i)", value: i)
            }
            .navigationDestination(for: Int.self, destination: { i in
                StackSubView(i: i)
            })
        }
    }
}

struct StackSubView:View {
    let i : Int
    init(i:Int){
        self.i = i
        print("init \(i)")
    }
    var body: some View{
        Text("\(i)")
            .onAppear{
                print("onAppear")
            }
    }
}

确实,我也做了类似的测试,init xx 会打印多次,但 onAppear 就一次

akring commented 1 year ago

重新测试后更奇怪的事情发生了:

  1. 我在 onAppear 调用 ViewModel 的方法请求数据,但数据还未返回之前,断点监测到页面发生了重绘
  2. 此时观察 ViewModel, 已经变成另外一个全新的对象了,导致 ViewModel 的 @Published var userViewArray 被重新赋值后(因为 vm 都不是同一个了)页面无法正常重绘制。

猜测还是因为在页面 .onAppear 之后,navigationDestination 的多次调用依然依然导致了页面被反复创建

struct CategoryPage: View {

    @ObservedObject var viewModel = CategoryViewModel()

    var body: some View {
        List {

            ...

        }
        .onAppear {
            viewModel.requestItemData(itemID: itemID)
        }
    }
}
fatbobman commented 1 year ago

@ObservedObject var viewModel = CategoryViewModel() 用法有问题。 原因请参阅 StateObject 与 ObservedObject 一文

akring commented 1 year ago

@ObservedObject var viewModel = CategoryViewModel() 用法有问题。 原因请参阅 StateObject 与 ObservedObject 一文

写了一段时间 Objective-C,把这茬给忘了 😂

LivenChief commented 1 month ago

请问一下,mac 应用 这种结构下,能否动态的三列到sidebar和detail两列?我试了多种方式都不理想 NavigationSplitView(columnVisibility: $mode) { SideBarView() } content: { ContentColumnView() } detail: { DetailView() }

fatbobman commented 1 month ago

@LivenChief 可以根据需要(两列或三列)来调整 content 列的宽度,以实现你的需求。

struct ContentView: View {
  @State var mode: NavigationSplitViewVisibility = .all
  @State var columnMode = Mode.three
  let contentDefaultWidth: CGFloat = 300
  var body: some View {
    NavigationSplitView(columnVisibility: $mode) {
      Text("side")
    }
    content: {
      Text("content")
        .navigationSplitViewColumnWidth(columnMode == .three ? 300 : 0)
    }
    detail: {
      Text("Detail")
        .toolbar {
          ToolbarItem {
            Button {
              switch columnMode {
              case .two:
                columnMode = .three
              case .three:
                columnMode = .two
              }
            } label: {
              switch columnMode {
              case .two:
                Text("2")
              case .three:
                Text("3")
              }
            }
          }
        }
    }
  }

  enum Mode {
    case two, three
  }
}

https://github.com/user-attachments/assets/1505a988-3156-4ffb-a3e6-9f1553f266d8

LivenChief commented 1 month ago

我也目前是這樣實現的,不過沒有大佬的方案好,感謝 NavigationSplitView { SidebarView(viewModel: viewModel)

    } detail: {
        HSplitView {
            if viewModel.topicListVisibility {
                TopicListView(viewModel: viewModel)
                    .frame(width: 210)
            }
            ArticleDetailView(
                content: viewModel.selectedArticleContent,
                tocVisibility: viewModel.tocVisibility
            )
            .background(.white)
        }

    }