美文网首页
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