在我的界面,导航栏和内容视图已经融合在一起了,我们没有办法分清楚。
我们准备让导航条和内容分开,不然这样看起来的UI太丑了。
/// 页面的基础试图
struct PageContentView<Content:View,
Leading:View,
Trailing:View,
ViewModel:BaseViewModel>: View {
...
/// 初始化页面试图
/// - Parameters:
/// - title: 导航标题
/// - contentBuilder: 内容
/// - leadingBuilder: 导航左侧按钮
/// - trailingBuildeder: 导航右侧按钮
init(title:String,
viewModel:ViewModel,
@ViewBuilder contentBuilder:() -> Content,
@ViewBuilder leadingBuilder:() -> Leading,
@ViewBuilder trailingBuildeder:() -> Trailing) {
...
let appearance = UINavigationBarAppearance()
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
...
}
此时我们创建一个默认导航条的配置,可以轻松和内容是如区分。我们设置一下导航条的背景颜色为白色,和我们底部的颜色保持一致。
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .white
如图所示,我们在最后一行也显示了线,导致界面上十分的丑,我们将界面可以进行配置这条线。
struct MyCellContentView<Right:View>: View {
...
private let isShowBottomLine:Bool
...
init(title:String,
isShowBottomLine:Bool = true,
@ViewBuilder rightBuilder:() -> Right) {
...
self.isShowBottomLine = isShowBottomLine
...
}
var body: some View {
VStack(spacing: 0) {
...
if isShowBottomLine {
...
} else {
/// 是为了填充让控件一样的高度
Color.clear
.frame(height: 0.5)
}
}
...
}
}
struct MyDetailStyle1CellContentView: View {
...
private let isShowBottomLine:Bool
init(title:String,
detail:String,
isShowBottomLine:Bool = true) {
...
self.isShowBottomLine = isShowBottomLine
}
var body: some View {
MyCellContentView(title: title,
isShowBottomLine: isShowBottomLine) {
...
}
}
}
struct MyDetailCellContentView: View {
...
private let isShowBottomLine:Bool
init(title:String,
detail:String,
isShowBottomLine:Bool = true) {
...
self.isShowBottomLine = isShowBottomLine
}
var body: some View {
MyCellContentView(title: title,
isShowBottomLine: isShowBottomLine) {
...
}
}
}
突然我们发现有 Divider 这个组件,就是分割 UI元素用的,我们可以替换我们之前自定义的线。
struct MyCellContentView<Right:View>: View {
...
var body: some View {
VStack(spacing: 0) {
...
if isShowBottomLine {
Divider()
.padding(.leading, 15)
} else {
...
}
}
...
}
}
我们自动登录的高度明显要高于其他,主要原因我们设置自动布局,并且设置外边距是 15。这就导致 Switch组件默认高度比较高,加上15的Padding之后,整体放入高度会比较高。
我们将组件限制为50 高度,其余的元素全部居中对齐。
struct MyCellContentView<Right:View>: View {
...
var body: some View {
ZStack {
VStack(spacing: 0) {
HStack {
...
}
...
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
VStack {
Spacer()
if isShowBottomLine {
...
} else {
...
}
}
}
...
.frame(height:50)
}
}
此时我们的界面已经优化的和设计图差不多了,接下来我们开始优化我们功能。
class AppConfig: ObservableObject {
...
@AppStorage("gatewayUserName")
var gatewayUserName:String?
/// 当前选中的工厂代码
@AppStorage("currentFactoryCode")
var currentFactoryCode:String?
@AppStorage("userInfo")
private var userInfo:String?
...
/// 是否自动登录
@AppStorage("isAutoLogin")
var isAutoLogin = false
/// 选中的车间代码
@AppStorage("workShopCode")
/// 因为 _workShopCode 已经被系统使用 我们用workShopCode_
private var workShopCode_:String?
...
/// 选中的产线 code
@AppStorage("productLineCode")
var productLineCode:String?
/// 选中仓库 code
@AppStorage("storeHouseCode")
var storeHouseCode:String?
...
}
观察上述的代码,我们的本地存储是有问题的。因为服务器地址,用户发生了改变,这些值就要跟着发生改变。那就意味着,我们不能用这些不变的作为Key,需要加入服务器地址和用户的唯一ID作为条件。
但是我们在 AppConfig 初始化Key又拿不到当前保存的服务器地址和用户的唯一ID,我们不妨把用户相关的配置分离出来。
/// 用户配置
class UserConfig: ObservableObject {
private let server:String
private let user:String
@AppStorage(gatewayUserNameKey)
var gatewayUserName:String?
private var gatewayUserNameKey:String {
return "gatewayUserName_\(server)_\(user)"
}
init(server:String, user:String) {
self.server = server
self.user = user
}
}
但是上面代码报了错误
Cannot use instance member 'gatewayUserNameKey' within property initializer; property initializers run before 'self' is available
这样我们无法进行初始化,我们就按照之前我们做的,自己自定义进行初始化。
/// 用户配置
class UserConfig: ObservableObject {
...
@AppStorage
var gatewayUserName:String?
init(server:String, user:String) {
...
self._gatewayUserName = AppStorage("gatewayUserName_\(server)_\(user)")
}
}
我们将所有和用户有关的配置都转移到 UserConfig 里面。
/// 用户配置
class UserConfig: ObservableObject {
...
init(server:String, user:String) {
self.server = server
self.user = user
let userKey = "\(server)_\(user)"
self._gatewayUserName = AppStorage("gatewayUserName_\(userKey)")
self._currentFactoryCode = AppStorage("currentFactoryCode_\(userKey)")
self._userInfo = AppStorage("userInfo_\(userKey)")
self._isAutoLogin = AppStorage(wrappedValue: false, "isAutoLogin_\(userKey)")
self._workShopCode_ = AppStorage("workShopCode_\(userKey)")
self._productLineCode = AppStorage("productLineCode_\(userKey)")
self._storeHouseCode = AppStorage("storeHouseCode_\(userKey)")
self.workShopCode = workShopCode_
}
}
我们要获取用户的配置的时候必须要拿到用户ID,获取用户ID的时候必须拿到用户配置。这个似乎陷入了死循环中,我们看下面的流程。
我们在整个流程中发现,只有当用户没有登录,重新登录可以拿到用户ID获取到用户配置,才能打破这个死循环。但是在已经登录的流程,想要获取到用户配置就是一个死循环。
想要打破这个循环,就要改变上面的逻辑。
我们将判断是否登录换成了判断本地是否有用户ID,有了用户ID就可以获取到用户配置,从而打破循环。
class AppConfig: ObservableObject {
...
/// 当前登录的用户ID
@AppStorage("currentUserId")
var currentUserId:String?
...
}
字段 currentUserId 来源于我们用户信息中的 employeeNo 字段,我们在用户登录的时候进行保存employeeNo字段到本地。
class LoginPageViewModel: BaseViewModel {
...
func login() async {
...
AppConfig.share.currentUserId = model.data?.user?.employeeNo
}
}
此时我们看一下我们当前用户登录之后的设置代码。
if let gatewayUserName = model.data?.gatewayUserName {
/// 放在 [AppConfig.share.gatewayUserName] 赋值的前面 这样 gatewayUserName 通知时候才能获取 isGatewayUserNameFromCache 最新值
AppConfig.share.isGatewayUserNameFromCache = false
AppConfig.share.userConfig?.gatewayUserName = gatewayUserName
}
AppConfig.share.userConfig?.userInfoModel = model.data?.user
AppConfig.share.currentUserId = model.data?.user?.employeeNo
我们此时只有一处地方可以登录,我们后续可能还有手机号/微信/微博/苹果等等登录方式,可能登录地方就要写很多这种逻辑。我们不如将登录之后的逻辑放在一个统一的方法里面,以后在其他登录方法或者页面登录之后进行调用。
struct UserManager {
/// 登录时候 类似于JWT的值
private let gatewayUserName:String
/// 用户唯一的 ID 当前值代表员工的工号
private let employeeNo:String
/// 用户的信息
private let user:UserInfoModel
/// 初始化用户管理中心 如果初始化失败 则返回异常
/// - Parameter response: 用户登录的返回内容
init(userLogin response:UserLoginResponse) throws {
guard let gatewayUserName = response.gatewayUserName, !gatewayUserName.isEmpty else {
throw "[gatewayUserName]返回为空"
}
self.gatewayUserName = gatewayUserName
guard let user = response.user else {
throw "[user]返回为空"
}
self.user = user
guard let employeeNo = response.user?.employeeNo, !employeeNo.isEmpty else {
throw "[employeeNo]返回为空"
}
guard let _ = Int(employeeNo) else { throw "[employeeNo]必须是纯数字" }
self.employeeNo = employeeNo
}
/// 进行登录
func login() {
AppConfig.share.isGatewayUserNameFromCache = false
AppConfig.share.userConfig?.gatewayUserName = gatewayUserName
AppConfig.share.userConfig?.userInfoModel = user
AppConfig.share.currentUserId = employeeNo
}
}
我们在 UserManager 初始化的时候做了验证并可能抛出异常,我们初始化这么多验证,如果后续的字段更多,岂不是初始化逻辑就很复杂了。我们修改一下上面初始化方法,将验证进行一次简化。
struct UserManager {
...
init(userLogin response:UserLoginResponse) throws {
self.gatewayUserName = try UserManager.verify(gatewayUserName: response.gatewayUserName)
self.user = try UserManager.verify(user: response.user)
self.employeeNo = try UserManager.verify(employeeNo: self.user.employeeNo)
}
/// 验证 gatewayUserName 的值
/// - Parameter name: gatewayUserName 值
/// - Returns: 验证通过的 gatewayUserName 值
private static func verify(gatewayUserName name:String?) throws -> String {
guard let gatewayUserName = name, !gatewayUserName.isEmpty else {
throw "[gatewayUserName]返回为空"
}
return gatewayUserName
}
/// 验证用户信息
/// - Parameter user: 用户信息
/// - Returns: 验证通过的用户信息
private static func verify(user model:UserInfoModel?) throws -> UserInfoModel {
guard let user = model else {
throw "[user]返回为空"
}
return user
}
/// 验证 employeeNo 的值
/// - Parameter no: employeeNo 值
/// - Returns: 验证通过的 employeeNo 值
private static func verify(employeeNo no:String?) throws -> String {
guard let employeeNo = no, !employeeNo.isEmpty else {
throw "[employeeNo]返回为空"
}
guard let _ = Int(employeeNo) else { throw "[employeeNo]必须是纯数字" }
return employeeNo
}
...
}
此时我们将验证提炼出来,可以给 UserManager的其他的初始化方法进行调用。我们还可以对于代码进行提炼进行修改,我们修改成下面的样子。
struct UserManager {
...
init(userLogin response:UserLoginResponse) throws {
self.gatewayUserName = try GatewayUserName(response.gatewayUserName).value
self.user = try User(response.user).value
self.employeeNo = try EmployeeNo(response.user?.employeeNo).value
}
...
}
fileprivate protocol UserResponseVerify {
associatedtype T
var value:T { get }
init(_ value:T?) throws
}
extension UserManager {
struct GatewayUserName: UserResponseVerify {
let value: String
init(_ value: String?) throws {
... 验证过程
self.value = gatewayUserName
}
}
struct User: UserResponseVerify {
let value: UserInfoModel
init(_ value: UserInfoModel?) throws {
... 验证过程
self.value = user
}
}
struct EmployeeNo: UserResponseVerify {
let value: String
init(_ value: String?) throws {
... 验证过程
self.value = employeeNo
}
}
}
我们修改成这个样子之后,已经渐渐的和 DDD(领域驱动)沾点边了。
class LoginPageViewModel: BaseViewModel {
...
func login() async {
...
if let response = model.data, let userManager = try? UserManager(userLogin: response) {
userManager.login()
}
}
...
}
我们修改了逻辑,已经在登录完毕完成了保存 employeeNo的值,此时我们就要写一下UserConfig的逻辑。
class AppConfig: ObservableObject {
...
var userConfig:UserConfig?
/// 当前登录的用户ID
@AppStorage("currentUserId")
var currentUserId:String?
init() {
...
/// 初始化 UserConfig
self.userConfig = getUserConfig()
/// 监听 currentUserId 的变化
/// @AppStorage是无法进行监听的
}
private func getUserConfig() -> UserConfig? {
guard !currentAppServer.isEmpty else { return nil }
guard let currentUserId = try? UserManager.EmployeeNo(currentUserId).value else { return nil }
return UserConfig(server: currentAppServer, user: currentUserId)
}
}
我们使用 @AppStorage 是无法通过 sink监听值更新的。我们可以在 currentUserId的didSet中去操作设置新的UserConfig,但是我们上面的逻辑就显得有点中断。
我们可以通过Notification进行实现,让流程连贯起来,方便阅读和维护。
class AppConfig: ObservableObject {
...
private var cancellabelSet:Set<AnyCancellable> = []
init() {
...
/// 初始化 UserConfig
self.userConfig = getUserConfig()
/// 监听 currentUserId 的变化
/// `@AppStorage` 是无法进行监听的 因此这里采用 `Notification`
NotificationCenter.default.publisher(for: .currentUserIdChanged, object: nil)
.sink {[weak self] no in
/// 监听到 `currentUserId` 改变的时候 更新 `UserConfig`
guard let self = self else { return }
self.userConfig = self.getUserConfig()
}
.store(in: &cancellabelSet)
}
...
}
fileprivate extension Notification.Name {
static let currentUserIdChanged = Notification.Name("currentUserIdChanged")
}
写到这里我们发现了userConfig是一个Optional可选值,是无法通过@StateObject初始化的。但是UserConfig如果用户没有登录则无法进行初始化。
/// ❌ Cannot convert value of type 'UserConfig?' to specified type 'UserConfig'
@StateObject private var useConfig:UserConfig = AppConfig.share.userConfig
我想通过用户没有登录就创建一个空的UserConfig,当登录或者重新登录就对当前的UserConfig进行重新的赋值,但是这样的操作十分的麻烦。
就当我绝望,觉得只能通过通过一个个更新才能实现的时候,我想到了在Flutter中可以监听整个对象,如果对象变动,则会更新使用此对象属性所有的Widget。
那么这个思路是否可以通过SwiftUI中实现吗,我们下一章接下来说。