美文网首页
SwiftUI: 全局状态管理

SwiftUI: 全局状态管理

作者: 猪猪行天下 | 来源:发表于2021-04-01 11:13 被阅读0次

    随着SwiftUI的采用,创建全局范围内状态的新趋势正在形成。这是非常合理的,因为它是为数不多的正确处理深度链接、HUDs等的方法之一。
    在本文中,让我们看看这种方法,以及如何避免其使用弊端。

    App

    我们将构建一个只有两个tabs的小应用:home和settings 两个tab。

    我们将以TabView为例,但同样的方法也可以用于导航、表、所有其他SwiftUI视图演示等等。

    simple.gif
    struct ContentView: View {
      var body: some View {
       TabView {
          HomeView()
            .tabItem { Label("Home", systemImage: "house.fill") }
    
          SettingsView()
            .tabItem { Label("Settings", systemImage: "gear") }
        }
      }
    }
    
    struct HomeView: View {
      var body: some View {
        Text("Home")
      }
    }
    
    struct SettingsView: View {
      var body: some View {
        Text("Settings")
      }
    }
    

    这里我们已经声明了一个TabView和两个视图,HomeViewSettingsView,每个视图都有一个相关的Text文本。

    控制tab的选择

    我们的应用要求以编程方式选择活动标签:例如,我们希望有一个快捷方式让用户从Home页跳转到Settings标签,或者我们希望切换标签作为深度链接的响应,等等。

    我们需要自己管理TabView状态,这可以通过TabView初始化器来Binding参数实现。

    goto.gif
    enum Tab: Hashable {
      case home
      case settings
    }
    
    struct ContentView: View {
      @State private var selectedTab: Tab = .home
    
      var body: some View {
        TabView(selection: $selectedTab) {
          HomeView(selectedTab: $selectedTab)
            .tabItem { Label("Home", systemImage: "house.fill") }
            .tag(Tab.home)
    
          SettingsView()
            .tabItem { Label("Settings", systemImage: "gear") }
            .tag(Tab.settings)
        }
      }
    }
    
    struct HomeView: View {
      @Binding var selectedTab: Tab
    
      var body: some View {
        VStack {
          Button("go to settings") {
            selectedTab = .settings
          }
          Text("Home")
            .onAppear(perform: { print("home on appear")})
        }
      }
    }
    
    struct SettingsView: View {
      ... // same as before
    }
    

    这次我们更新了如下内容:

    • 声明了一个带有所有可能TabView状态的Tab枚举
    • ContentView添加了selectedTab属性,用于控制TabView的状态
    • Home页添加了一个按钮可以让我们跳转到Setting选项

    虽然这已经实现了需求,它让我们可以通过编程方式改变标签状态,但我们仍然需要直接访问ContentViewselectedTab属性,然后才能改变标签栏状态。

    更重要的是,随着应用的发展,将tab状态从一个视图传递到另一个视图是不现实的,因此我们需要将状态移出TabView,并通过应用范围内的状态将它放到环境中。

    App全局状态

    目前的限制是TabView的状态可访问性,我们可以通过创建一个全局状态并在环境中设置它来克服这个挑战:

    class AppWideState: ObservableObject {
      @Published var selectedTab: Tab = .home
    }
    

    AppWideState只保存选项卡状态,并在selectedTab即将更改时发送一个新的发布事件。

    我们希望这个状态在任何地方都可以访问,我们将把它附加到我们的主App,然后把它注入到环境中:

    @main
    struct FiveStarsApp: App {
      @StateObject var appWideState = AppWideState()
    
      var body: some Scene {
        WindowGroup {
          ContentView()
            .environmentObject(appWideState) // injected in the environment
        }
      }
    }
    

    一切就绪,让我们更新我们的视图来使用用这个新状态:

    struct ContentView: View {
      @EnvironmentObject var state: AppWideState // environment object
    
      var body: some View {
        TabView(selection: $state.selectedTab) { // state from the environment object
          HomeView()
            .tabItem { Label("Home", systemImage: "house.fill") }
            .tag(Tab.home)
    
          SettingsView()
            .tabItem { Label("Settings", systemImage: "gear") }
            .tag(Tab.settings)
        }
      }
    }
    
    struct HomeView: View {
      @EnvironmentObject var state: AppWideState // environment object
    
      var body: some View {
        VStack {
          Button("go to settings") {
            state.selectedTab = .settings // sets the state from the environment object
          }
          Text("Home")
        }
      }
    }
    
    struct SettingsView: View {
      ... // same as before
    }
    

    HomeView仍然可以使用@Binding

    这个新结构的工作方式和之前完全一样,但是我们现在可以从主应用和环境到达的任何地方更改所选的标签。

    几周后

    几周过去了,我们的AppWideState获得了一些新的@Published属性,更多的视图观察到这个对象,我们的应用程序开始变慢:每次我们改变全局状态,我们会注意到在更改选项卡或新导航被推送之前有轻微的延迟,等等。

    我们可以通过我们的例子来研究这个谜团。让我们在我们的HomeView中添加一个小的副作用,一个打印语句告诉我们HomeView主体什么时候执行:

    struct HomeView: View {
      @EnvironmentObject var state: AppWideState
    
      var body: some View {
        let _ = print("HomeView body") // side effect
        VStack {
          Button("go to settings") {
            state.selectedTab = .settings
          }
          Text("Home")
        }
      }
    }
    

    我们可以再次运行应用程序,我们会注意到,每次我们改变标签(点击标签栏或go to settings按钮)HomeViewbody重新计算:这是真的,尽管HomeView没有真正的读取这个状态而只是设置了它。

    print.gif

    如果我们删除了按钮操作,那么@EnvironmentObject状态根本就不需要做任何事情,这种情况仍然会发生

    struct HomeView: View {
      @EnvironmentObject var state: AppWideState
    
      var body: some View {
        let _ = print("HomeView body") // side effect
        VStack {
          Button("go to settings") {
            // does nothing
          }
          Text("Home")
        }
      }
    }
    
    print.gif

    现在让我们想象一下:

    • 我们有一个导航堆栈(或多个导航堆栈)
    • 堆栈中的一些界面使用我们的AppWideState环境对象
    • 用户在堆栈的多个层次深处

    我们对AppWideState所做的每一个改变都将为观察AppWideState的所有视图触发一个新的body渲染评估,而不仅仅是用户当前正在查看的最后一个视图。
    现在很容易解释为什么应用程序变得越来越慢:我们使用和扩展AppWideState越多,更多的视图会在每次更改时重新渲染它们的body

    这个陷阱其实是意料之中的,因为EnvironmentObject只是我们的视图订阅的另一个ObservableObject实例,SwiftUI正在做它一直承诺要做的事情:自动订阅和对状态变化做出反应。

    解决方案

    虽然大多数视图可能需要通过AppWideState来设置它,但实际上很少需要观察它的状态:以tabbar状态为例,只有TabView需要观察它,所有其他视图只需要改变它。

    要做到这一点,一种方法是创建一个包含全应用范围状态的容器,容器本身不发布任何东西:

    class AppStateContainer: ObservableObject {
      ...
    }
    

    不同于将每个状态声明为该容器的@Published属性,每个状态都将嵌套到自己的ObservableObject中,然后ObservableObject将成为容器的一部分:

    class TabViewState: ObservableObject {
      @Published var selectedTab: Tab = .home
    }
    
    class AppStateContainer: ObservableObject {
      var tabViewState = TabViewState()
    }
    
    

    容器符合ObservableObject,因为它是环境对象的需求,但是它不发布任何东西,同时我们把selectedTab状态移动到它自己的TabViewState

    这种方法还将每个应用程序状态隔离到它自己的“迷你容器”中(上面的TabViewState),因此我们可以将每个特定状态的所有逻辑(如果有的话)集中到它自己的类中,而不是将其共享在一个大类中。

    有了这些,我们现在可以把AppStateContainerTabViewState设置到环境中了:

    @main
    struct FiveStarsApp: App {
      @StateObject var appStateContainer = AppStateContainer()
    
      var body: some Scene {
        WindowGroup {
          ContentView()
            .environmentObject(appStateContainer)
            .environmentObject(appStateContainer.tabViewState)
        }
      }
    }
    

    需要观察状态变化的视图将直接观察TabViewState,而只需要改变状态的视图将直接作用于容器:

    struct ContentView: View {
      @EnvironmentObject var state: TabViewState
    
      var body: some View {
        TabView(selection: $state.selectedTab) {
          HomeView()
            .tabItem {
              Label("Home", systemImage: "house.fill")
            }
            .tag(Tab.home)
    
          SettingsView()
            .tabItem {
              Label("Settings", systemImage: "gear")
            }
            .tag(Tab.settings)
        }
      }
    }
    
    struct HomeView: View {
      @EnvironmentObject var container: AppStateContainer
    
      var body: some View {
        let _ = print("Home body")
        VStack {
          Button("go to settings") {
            container.tabViewState.selectedTab = .settings
          }
          Text("Home")
            .onAppear(perform: { print("home on appear")})
        }
      }
    }
    

    有了这个改变,我们的应用性能又回来了,没有多余的视图body计算。
    在这个例子中,我们让HomeView直接访问AppStateContainer并改变TabView状态本身,然而,我们也可以在容器上添加一些方便的API来让这变得更简单.

    相关文章

      网友评论

          本文标题:SwiftUI: 全局状态管理

          本文链接:https://www.haomeiwen.com/subject/paqtkltx.html