使用 EnvironmentObject 和 AppState 管理全局状态

使用 EnvironmentObject 和 AppState 管理全局状态

在应用程序开发中,组件之间的状态传递是一个常见的需求。管理状态的方式会影响代码的可维护性、可扩展性以及可读性。本文将介绍两种常见的状态传递方式:变量传递和全局状态管理。

使用变量传递状态

变量传递是最直观的状态传递方式。在这种方式中,我们在声明组件时创建变量,并在需要的地方通过显式方式传递这些变量

举个例子,假设我们有一个父组件 ParentView 和一个子组件 ChildView,我们可以通过变量传递在这两个组件之间共享数据。

struct ParentView: View {
    @State private var username: String = "John Doe"

    var body: some View {
        VStack {
            Text("Parent View")
            ChildView(username: $username)
            Text("Username: \(username)")
        }
    }
}
struct ChildView: View {
    @Binding var username: String

    var body: some View {
        VStack {
            Text("Child View")
            TextField("Enter username", text: $username)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
        }
    }
}

在 ParentView 中定义了一个 @State 修饰的变量 username。通过 $username 绑定传递,ChildView 中的 @Binding 修饰符能够引用并修改这个变量。这样,我们在子组件中修改 username 时,父组件中的值也会同步更新。

这种方法的优点在于简单易用,变量传递方式直观明了,易于理解和实现,特别适用于小型应用或简单的组件间状态传递。然而,在现代编程中,提倡高度模块化以及组件的复用,对于组件嵌套较多的应用,跨层级传递状态会变得复杂和繁琐——你需要手动编写代码传递变量。随着应用规模扩大,变量传递会导致代码难以维护。

使用全局状态管理优化状态传递

如何在组件层次结构中有效地管理和共享状态,而无需逐层传递数据,是各个编程语言及框架都面临的一个问题。我们来看看其他受到大家认可的优秀的框架是如何解决这个问题的。

React 中的 Context API 

在 React 中,Context API 提供了一种无需通过组件树逐层传递 props 就能在组件树间进行数据传递的方法,它通过以下几个步骤实现:

1.创建 Context:定义一个 Context 对象,用于保存全局状态。

// 创建一个 Context 对象
const AppContext = createContext();

2.提供 Context:使用 Context Provider 组件在组件树的顶层提供状态,这样应用中所有组件都可以使用 useContext 钩子访问和更新共享的状态。

const AppProvider = ({ children }) => {
  const [username, setUsername] = useState('John Doe');
  
  return (
    <AppContext.Provider value={{ username, setUsername }}>
      {children}
    </AppContext.Provider>
  );
};

3.消费 Context:在需要访问全局状态的子组件中使用 Context Consumer 或 useContext 钩子。

const ChildComponent = () => {
  const { username, setUsername } = useContext(AppContext);
  
  return (
    <div>
      <p>Username: {username}</p>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
    </div>
  );
};

通过这种方式,Context API 允许我们在不显式传递 props 的情况下在不同组件之间共享状态,大大简化了复杂应用中的状态管理。

Python 中的配置文件

在 Python 中,通常通过配置文件或单例模式来管理全局状态。

# config.py
class Config:
    def __init__(self):
        self.username = "John Doe"
config = Config()

# module_a.py
from config import config

def set_username(username):
    config.username = username

# module_b.py
from config import config

def get_username():
    return config.username

在这个示例中,我们通过一个名为 Config 的类来存储全局状态,并在不同模块中引用同一个 config 实例,从而实现状态共享。

Swift 中的 EnvironmentObject

介于 React Context 的成功,Swift 在很大程度上借鉴了其概念和方法,因此 Swift 的 EnvironmentObject(环境对象) 和 React 的 Context API 在思路上非常相似。

1.创建 ObservableObject:定义一个继承自 ObservableObject 的类,并使用 @Published 属性包装器来声明可观察的属性。这些属性将保存全局状态。

class State: ObservableObject {
    @Published var userName: String = "John Doe"
}

2.提供 EnvironmentObject:在应用程序的根视图中创建 ObservableObject 的实例,并通过 .environmentObject 修饰符将其注入到视图层次结构中。这使得应用中的所有子视图都可以访问和更新共享的状态。

@main
struct MyApp: App {
    var state = State()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(state)
        }
    }
}

3.消费 EnvironmentObject:在需要访问全局状态的子视图中,使用 @EnvironmentObject 属性包装器来访问共享的状态。这使得子视图可以读取和更新全局状态,而无需显式地传递数据。

struct ContentView: View {
    @EnvironmentObject var state: State

    var body: some View {
        VStack {
            Text("Username: \(appState.userName)")
            TextField("Enter username", text: $appState.userName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
        }
    }
}

通过这种方法,SwiftUI 的 EnvironmentObject 提供了一种简洁而高效的方式来管理全局状态,使得在复杂的视图层次结构中共享和更新状态变得更加容易。

创建 AppState 统一管理全局状态

在设计 SwiftUI 应用时,推荐创建一个专门的状态管理类,如 AppState,来统一管理全局状态,这样做具有多种优点,例如集中管理、易于维护。

定义 AppState

首先,定义一个 AppState 类,该类需要从 ObservableObject 继承。这样可以确保任何状态变化都能被 SwiftUI 的视图系统捕获并相应地更新 UI。使用 @Published 属性包装器来标记那些当改变时需要更新 UI 的属性。

import SwiftUI

class AppState: ObservableObject {
    @Published var isAuthenticated: Bool = false

    func logIn() {
        // 这里可以添加实际的登录逻辑
        self.isAuthenticated = true
    }

    func logOut() {
        // 这里可以添加实际的登出逻辑
        self.isAuthenticated = false
    }
}

在 AppState 类除了定义状态变量,还推荐定义状态更新的方法。这些方法不仅可以改变状态,还可以封装与状态变更相关的逻辑。

  • 封装:通过在 AppState 中添加方法,可以将状态变更的逻辑封装在一个地方,而不是分散在多个视图中。这样,每当状态需要更新时,视图只需调用一个方法,而不必了解背后的实现细节。
  • 重用:当多个视图需要执行相同的状态变更操作时,通过在 AppState 中定义方法可以避免代码重复。这不仅减少了代码量,还使得未来的更改更加集中,易于管理。
  • 可维护性和可读性:将状态管理逻辑和UI逻辑分开,可以提高代码的可维护性和可读性。这让其他开发者更容易理解和维护代码,特别是在大型项目中。

在 SwiftUI 视图中使用 EnvironmentObject

接下来,确保你的 AppState 实例可以在需要的视图中被访问。

在你的应用的入口点(通常是 App 的主体部分),创建 AppState 的实例,并使用 .environmentObject 方法将它注入到环境中。

@main
struct MyApp: App {
    @StateObject var appState = AppState()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
        }
    }
}

注意是 var appState = AppState(),而不是 var appState: AppState,前者创建了一个 AppState 类的实例,而后者没有。

如果应用视图需要根据 appState 中的状态变化更新,那么应该使用 @StateObject (在大部分情况下都推荐使用)。如果只是一些不需要观察其变化的静态数据或配置,则可以删除 @StateObject 装饰器。

在视图中访问 AppState

在任何 SwiftUI 视图中,如果你希望访问这个全局状态,可以通过 @EnvironmentObject 属性包装器来获取 AppState 的实例。当状态更新时,SwiftUI 组件可以自动根据状态来更新 UI。

struct ContentView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack {
            if appState.isAuthenticated {
                Text("用户已认证")
                Button("登出") {
                    appState.logOut()
                }
            } else {
                Text("用户未认证")
                Button("登录") {
                    appState.logIn()
                }
            }
        }
    }
}

这样,我们就实现了一个简单的登录逻辑,将这些逻辑放在 AppState 中,可以让任何视图通过简单地调用 appState.logIn() 来实现用户登录,而无需关心登录过程的具体细节。这种方法也便于在后续需要修改登录逻辑时,只需在 AppState 中修改,无需触及任何使用了登录功能的视图。

在 Preview 中使用 EnvironmentObject

在 Preview 中模拟环境对象的行为,必须首先创建一个 State 实例,然后手动将环境对象注入到 Preview 视图中。

  1. 创建环境对象的实例:首先,您需要创建一个或多个环境对象的实例。
  2. 注入环境对象到预览中:在预览提供者中,使用 .environmentObject() 方法将状态对象注入到预览的视图中。

像下面这样使用:

// SwiftUI 预览
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        // 创建 AppState 的实例
        let appState = AppState()

        // 将 appState 作为环境对象传递给 ContentView
        ContentView()
            .environmentObject(appState)
    }
}

特别注意:即使在 App 组件中已经声明并注入了 AppState 到环境中,你在预览(Preview)中依然需要重新创建并注入 AppState 的实例。这是因为预览代码与应用的运行环境是隔离的,预览不会自动从 App 组件中继承环境对象。

因此,像下面这样是错误的,因为没有创建 AppState 实例:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        @EnvironmentObject var appState: AppState

        // 将 appState 作为环境对象传递给 ContentView
        ContentView()
            .environmentObject(appState)
    }
}