第二十七章 UINavigationBarAppearance|Divider

在我的界面,导航栏和内容视图已经融合在一起了,我们没有办法分清楚。

我们准备让导航条和内容分开,不然这样看起来的UI太丑了。

swift 复制代码
/// 页面的基础试图
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
    }
    
    ...
}

此时我们创建一个默认导航条的配置,可以轻松和内容是如区分。我们设置一下导航条的背景颜色为白色,和我们底部的颜色保持一致。

swift 复制代码
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .white

如图所示,我们在最后一行也显示了线,导致界面上十分的丑,我们将界面可以进行配置这条线。

swift 复制代码
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)
            }
        }
        ...
    }
}
swift 复制代码
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) {
            ...
        }
    }
}
swift 复制代码
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元素用的,我们可以替换我们之前自定义的线。

swift 复制代码
struct MyCellContentView<Right:View>: View {
    ...
    var body: some View {
        VStack(spacing: 0) {
            ...
            if isShowBottomLine {
                Divider()
                    .padding(.leading, 15)
            } else {
                ...
            }
        }
        ...
    }
}

我们自动登录的高度明显要高于其他,主要原因我们设置自动布局,并且设置外边距是 15。这就导致 Switch组件默认高度比较高,加上15的Padding之后,整体放入高度会比较高。

我们将组件限制为50 高度,其余的元素全部居中对齐。

swift 复制代码
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)
    }
}

此时我们的界面已经优化的和设计图差不多了,接下来我们开始优化我们功能。

swift 复制代码
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,我们不妨把用户相关的配置分离出来。

swift 复制代码
/// 用户配置
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
    }
}

但是上面代码报了错误

swift 复制代码
Cannot use instance member 'gatewayUserNameKey' within property initializer; property initializers run before 'self' is available

这样我们无法进行初始化,我们就按照之前我们做的,自己自定义进行初始化。

swift 复制代码
/// 用户配置
class UserConfig: ObservableObject {
    ...
    @AppStorage
    var gatewayUserName:String?
    
    init(server:String, user:String) {
        ...
        self._gatewayUserName = AppStorage("gatewayUserName_\(server)_\(user)")
    }
}

我们将所有和用户有关的配置都转移到 UserConfig 里面。

swift 复制代码
/// 用户配置
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就可以获取到用户配置,从而打破循环。

swift 复制代码
class AppConfig: ObservableObject {
    ...
    /// 当前登录的用户ID
    @AppStorage("currentUserId")
    var currentUserId:String?
    ...
}

字段 currentUserId 来源于我们用户信息中的 employeeNo 字段,我们在用户登录的时候进行保存employeeNo字段到本地。

swift 复制代码
class LoginPageViewModel: BaseViewModel {    
    ...
    func login() async {
        ...
        AppConfig.share.currentUserId = model.data?.user?.employeeNo
    }
}

此时我们看一下我们当前用户登录之后的设置代码。

swift 复制代码
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

我们此时只有一处地方可以登录,我们后续可能还有手机号/微信/微博/苹果等等登录方式,可能登录地方就要写很多这种逻辑。我们不如将登录之后的逻辑放在一个统一的方法里面,以后在其他登录方法或者页面登录之后进行调用。

swift 复制代码
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 初始化的时候做了验证并可能抛出异常,我们初始化这么多验证,如果后续的字段更多,岂不是初始化逻辑就很复杂了。我们修改一下上面初始化方法,将验证进行一次简化。

swift 复制代码
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的其他的初始化方法进行调用。我们还可以对于代码进行提炼进行修改,我们修改成下面的样子。

swift 复制代码
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(领域驱动)沾点边了。

swift 复制代码
class LoginPageViewModel: BaseViewModel {    
    ...    
    func login() async {
        ...
        if let response = model.data, let userManager = try? UserManager(userLogin: response) {
            userManager.login()
        }
    }
		...
}

我们修改了逻辑,已经在登录完毕完成了保存 employeeNo的值,此时我们就要写一下UserConfig的逻辑。

swift 复制代码
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监听值更新的。我们可以在 currentUserIddidSet中去操作设置新的UserConfig,但是我们上面的逻辑就显得有点中断。

我们可以通过Notification进行实现,让流程连贯起来,方便阅读和维护。

swift 复制代码
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如果用户没有登录则无法进行初始化。

swift 复制代码
/// ❌ Cannot convert value of type 'UserConfig?' to specified type 'UserConfig'
@StateObject private var useConfig:UserConfig = AppConfig.share.userConfig

我想通过用户没有登录就创建一个空的UserConfig,当登录或者重新登录就对当前的UserConfig进行重新的赋值,但是这样的操作十分的麻烦。

就当我绝望,觉得只能通过通过一个个更新才能实现的时候,我想到了在Flutter中可以监听整个对象,如果对象变动,则会更新使用此对象属性所有的Widget

那么这个思路是否可以通过SwiftUI中实现吗,我们下一章接下来说。

相关推荐
君赏3 小时前
第二十二章 onAppear|DataPickerView
swiftui
君赏3 小时前
第二十三章 UIHostingController|withAnimation|SwiftUI 默认动画时间
swiftui
君赏3 小时前
第二十四章 init 方法初始化 State
swiftui
君赏3 小时前
第十九章 TabView|accentColor|AnyView|NavigationView|navigationTitle|navigationBarTit
swiftui
君赏3 小时前
第十七章 @MainActor
swiftui
君赏3 小时前
第十六章 RoundedRectangle|aspectRatio|UIViewRepresentable
swiftui
君赏3 小时前
第十八章 封装HUD和完善登录界面逻辑
swiftui
君赏3 小时前
第十五章 Task|NSAppTransportSecurity|keyDecodingStrategy
swiftui
君赏3 小时前
第十四章 async/await|overlay|PreferencrKey|Anchor
swiftui