实现自动登录
接下来我们需要做 `自动登陆功能,自动登陆就是登陆之后,下次启动开启状态下,直接进入首页。关闭情况下,则进入登陆页面。
@main
struct Win_App: App {
@StateObject private var appConfig:AppConfig = AppConfig.share
var body: some Scene {
WindowGroup {
if isLogin {
if appConfig.isAutoLogin {
TabPage()
} else {
LoginPage()
}
} else {
LoginPage()
}
}
}
...
}
我们需要两处需要初始化LoginPage的地方,这个玩意需要参数,或者其他设置,就比较麻烦了,虽然我们可以提炼代码。
@main
struct Win_App: App {
@StateObject private var appConfig:AppConfig = AppConfig.share
var body: some Scene {
WindowGroup {
if isLogin {
if appConfig.isAutoLogin {
TabPage()
} else {
loginPage()
}
} else {
loginPage()
}
}
}
...
private func loginPage() -> some View {
LoginPage()
}
}
但是我们的判断逻辑依然十分的复杂,我们可以变更一下流程图。
我们将判断的逻辑封装成一个方法,这样虽然看起来没啥变化,但是对于页面处理逻辑清晰。
@main
struct Win_App: App {
@StateObject private var appConfig:AppConfig = AppConfig.share
var body: some Scene {
WindowGroup {
if isNeedLogin {
LoginPage()
} else {
TabPage()
}
}
}
...
private var isNeedLogin:Bool {
return !isLogin || !appConfig.isAutoLogin
}
}
但是,经过测试,我们登陆成功也是无法进入首页的,因为 isAutoLogin 默认关闭的。经过思考,我们上面的逻辑是有问题的,需要修改一些逻辑。
1 当App全新未安装的时候(红线代表逻辑走向)
2 当执行登陆完毕之后
3 第二次启动 App已经登录过 但是没有开启自动登录
4 App启动 App已经登录过,开启了自动登录
我们按照流程图写一下代码
@main
struct Win_App: App {
@StateObject private var appConfig:AppConfig = AppConfig.share
...
private var isNeedLogin:Bool {
/// 如果 gatewayUserName 不存在 则需要进行登录
guard isExitGatewayUserName else { return true}
/// 如果 gatewayUserName 存在 并且 isGatewayUserNameFromCache = false 代表是刚刚登录的 则不需要登录
guard appConfig.isGatewayUserNameFromCache else { return false }
/// 如果 gatewayUserName 存在 并且 isGatewayUserNameFromCache = true 代表登录是之前运行操作的 如果没开启自动登录就需要前往重新登录
return !appConfig.isAutoLogin
}
/// 是否存在 gatewayUserName
private var isExitGatewayUserName:Bool {
guard let gatewayUserName = appConfig.gatewayUserName else { return false }
return !gatewayUserName.isEmpty
}
}
class AppConfig: ObservableObject {
...
/// gatewayUserName 是否来源于缓存 默认来源于缓存
var isGatewayUserNameFromCache:Bool = true
...
}
class LoginPageViewModel: BaseViewModel {
...
func login() async {
...
if let gatewayUserName = model.data?.gatewayUserName {
/// 放在 [AppConfig.share.gatewayUserName] 赋值的前面 这样 gatewayUserName 通知时候才能获取 isGatewayUserNameFromCache 最新值
AppConfig.share.isGatewayUserNameFromCache = false
AppConfig.share.gatewayUserName = gatewayUserName
}
...
}
}
接下来我们需要获取版本号和 build号显示出来,这个简单一些。
struct MyPage: View {
...
private func appVersionCell() -> some View {
MyDetailStyle1CellContentView(title: "版本",
detail: viewModel.versionValue)
}
...
}
class MyPageViewModel: BaseViewModel {
...
var versionValue:String {
guard let infoDictionary = Bundle.main.infoDictionary else { return "" }
guard let version = infoDictionary["CFBundleShortVersionString"] else { return "" }
guard let buildNumber = infoDictionary["CFBundleVersion"] else { return "" }
return "\(version)(\(buildNumber))"
}
}
我的页面接下来就只剩下退出登录功能了,我们按照我们上方登录流程图来看,只需要将 gatewayUserName 设置为 nil即可实现退出登录,回到登录界面。
struct MyPage: View {
...
@StateObject private var appConfig = AppConfig.share
...
private func logoutButton() -> some View {
Button {
appConfig.gatewayUserName = nil
} label: {
...
}
}
...
}
研究界面的初始化和重建
但是我们重新进来还是在我的界面,既然重新登录,我认为就应该回到首页。我们在研究生命周期时候发现下面的打印。
struct TabPage: View {
...
init() {
print("-> TabPage init")
...
}
var body: some View {
TabView(selection:$currentTabIndex) {
...
}
...
.onAppear {
print("-> currentTabIndex = \(currentTabIndex)")
}
}
}
-> LoginPage init // 未登录展示登录界面
-> TabPage init // 登陆 初始化TabPage
-> HomePage init // 初始化HomePage
-> MyPage init // 初始化 MyPage
-> currentTabIndex = 0 // TabPage onAppear
-> TabPage init // ??? 为啥再次初始化一次
-> HomePage init /// 点击currentTabIndex = 1 重新初始化HomePage
-> MyPage init // 重新初始化 MyPage
-> TabPage init // ??? 不理解为啥再次初始化
-> TabPage init // ??? 不理解为啥再次初始化
-> TabPage init // ??? 不理解为啥再次初始化
-> TabPage init // ??? 不理解为啥再次初始化
TabPage init打印了很多次,但是 currentTabIndex 只打印了一次,那就是 onAppear只执行了一次。我们简单绘制一下渲染树结构,按照Page为单位。
我们通过 @State 将数结构细化一点
从首页切换到我的页面,为啥切换到我的页面会打印这么多次 TabPage init?我的页面和首页的不同就是,首页初始化了工厂列表,我的页面在onAppear方法里面执行了初始化车间和产线还有仓库数据的操作。
难道和这个有关系,我们屏蔽一下初始化的代码。
struct MyPage: View {
...
var body: some View {
...
return PageContentView(title: "我的", viewModel: viewModel) {
...
}
}
.onAppear {
// Task {
// await viewModel.initData()
// }
}
}
...
}
struct HomePage: View {
...
var body: some View {
return NavigationView {
...
}
...
.onAppear {
// Task {
// await viewModel.requestFactoryList()
// }
}
}
}
我们再次看一下日志输出。
-> LoginPage init // 未登录展示登录界面
-> TabPage init // 登陆 初始化TabPage
-> HomePage init // 初始化HomePage
-> MyPage init // 初始化 MyPage
-> currentTabIndex = 0 // TabPage onAppear
-> HomePage init /// 点击currentTabIndex = 1 重新初始化HomePage
-> MyPage init // 重新初始化 MyPage
-> TabPage init // ??? 不理解为啥再次初始化
少了四次 TabPage init,这四次应该就是刷新我的界面的 车间/产线/仓库显示和刷新首页工厂操作引起的。但是这样依然 View 初始化的很多次,按照我们的操作。
/// 下面是理想状态下的输出
-> LoginPage init // 未登录展示登录界面
-> TabPage init // 登陆 初始化TabPage
-> HomePage init // 初始化HomePage
-> MyPage init // 初始化 MyPage
-> currentTabIndex = 0 // TabPage onAppear
上面应该就是在 UIKit系统下面正常的数据,但是SwiftUI不同于UIKit的生命周期,但是和Flutter有类似的作用。我们打印一下Body执行的过程,这个才是真正设计到调用绘制。
-> LoginPage init /// 未登录 初始化 LoginPage
-> LoginPage Body /// 绘制 LoginPage
-> LoginPage Body /// 展示 Loading 绘制 LoginPage
-> LoginPage Body /// 展示登陆成功提示 绘制 LoginPage
-> TabPage init /// 登陆成功 初始化 TabPage
->Tab Page Body /// 绘制 TabPage
->HomePage init /// 初始化 HomePage
->MyPage init /// 初始化 MyPage 因为都没展示我的页面 所以后续不需要绘制
-> currentTabIndex = 0 /// TabPage onAppear
-> HomePage Body /// 绘制 HomePage
->Tab Page Body /// 点击 tab = 1 重新绘制 TabPage
->HomePage init /// 重新初始化 HomePage 因为首页已经绘制 所以不需要重新绘制
->MyPage init /// 重新初始化 MyPage
->MyPage Body /// 绘制 MyPage 页面
->MyPage Body /// 重新绘制 MyPage 页面
-> TabPage init /// 初始化 TabPage
从输出上面看绘制首页一次是正常的,虽然多次初始化,多次初始化对于性能影响不大。但是我的页面绘制了两次?经过不停的调试,发现我的页面比首页多执行一次的原因在于 在 HomePage中添加了 NavigationView, 而 MyPage的 NavigationView 是加在 TabPage里面的。
我们都将 NavigationView 转移到 TabPage,再次看一下输出。
-> LoginPage init
-> LoginPage Body
-> LoginPage Body
-> LoginPage Body
-> TabPage init
->Tab Page Body
->HomePage init
->MyPage init
-> currentTabIndex = 0
-> HomePage Body
->Tab Page Body
->HomePage init
->MyPage init
->MyPage Body
->MyPage Body
-> TabPage init
都转移出来之后,发现刚开始进入的时候就开始初始化了 我的页面了。
我们能够通过树形结构局部刷新数来优化呢?答案是肯定的,但是目前来说也没必要研究那么深入,并且现在的页面就算优化,也没有大的意义。
从上面的输入看,当页面重新初始化和绘制的时候,@State不会随着初始化的,导致我们重新登陆完毕,展示给我们的是我的界面的问题。
因为 @State是 TabPage私有的,所以我们在我的页面退出登录也无法操作 TabPage的 currentTabIndex。目前想到了两种方案,第一种采用通知的形式,第二种采用@Binding。对于Struct,我猜测通知的方式可能不生效,或者麻烦,没有@Binding 方便。
class AppConfig: ObservableObject {
...
/// 当前 Tab 的索引
@Published var currentTabIndex:Int = 0
...
}
struct TabPage: View {
...
@StateObject private var appConfig = AppConfig.share
...
var body: some View {
TabView(selection:$appConfig.currentTabIndex) {
...
}
...
}
}
我们采用在AppConfig中新增一个@Published标识当前选中的Tab,因为AppConfig对象随时可以访问。为了修复重新登录无法重新定位到首页,我们在退出登录重置一下 currentTabIndex。
struct MyPage: View {
...
@StateObject private var appConfig = AppConfig.share
...
private func logoutButton() -> some View {
Button {
...
appConfig.currentTabIndex = 0
} label: {
...
}
}
}