新增 Profile 环境
到此我们已经做完了登录页面 首页 我的页面,但是还是存在一些问题需要进行优化,比如登录页面在第一次安装App的时候,默认没有服务器地址,需要用户手动的选择一个,这样就让用户可能多一次操作,体验不是很好。
为了可以优化体验,我们觉得第一次安装App的时候给服务器地址增加一项默认值,但是默认值设置为那一个?

我们目前可以获取是否是 DEBUG编译还是 RELEASE编译,但是无法区分是PROFILE编译。那么我们基于DEBUG环境新增一套环境作为我们PROFILE环境。

这样我们已经可以区分是否是Profile环境,我们给服务器一个默认的值。
swift
class AppConfig: ObservableObject {
...
init() {
...
if currentAppServer.isEmpty {
#if DEBUG
currentAppServer = AppServer.debug.rawValue
#elseif PROFILE
currentAppServer = AppServer.profile.rawValue
#else
currentAppServer = AppServer.release.rawValue
#endif
}
}
}
登陆页面,记住密码的选项默认也是关闭的,我们修改默认为打开,可以方便用户下次登陆页面不需要重复输入账号和密码。
swift
class LoginPageViewModel: BaseViewModel {
...
///是否记住密码
@AppStorage("isRememberPassword")
var isRememberPassword:Bool = true
...
}
focused 获取输入框是否获取焦点
我们下次启动,用户名和密码已经自动填写,但是我们更换用户名的时候需要一个个的进行删除,如果我们编辑的时候展示删除按钮岂不是可以一键的进行删除。
但是在 SwiftUI 中给 TextField 中添加 ClearMode十分的困难。不过我们可以通过 focused的Modify获取到输入框获取到焦点。
swift
func focused(_ condition: FocusState<Bool>.Binding) -> some View
那么我们就封装一下,当获取焦点的时候并且内容不为空时候,显示Clear按钮。
swift
struct ClearTextField: View {
private let title:String
@Binding private var text:String
/// 获取当前输入框是否获取焦点
@FocusState private var isFocus:Bool
/// 保持和 TextField 一致 好替换
init(_ title:String, text:Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
HStack {
TextField(title, text: $text)
.focused($isFocus)
if isShowClearButton {
/// `X`按钮
Image(systemName: "xmark")
.padding()
.onTapGesture {
/// 点击清空文本
text = ""
}
}
}
}
private var isShowClearButton:Bool {
/// 当获取焦点并且文本不为空才显示清空的按钮
isFocus && !text.isEmpty
}
}
在测试过程中 ClearTextField 组件预览输入文本,无法正常显示 Clear 按钮。但是用在登陆页面,就可以正常显示,这一点很奇怪。
我们的密码不能直接使用上述组件,因为密码需要用到 SecureField 组件。我们给 ClearTextField 新增一个属性,控制使用 TextFiled 还是 ClearTextField。
swift
struct ClearTextField: View {
...
private var isSecure:Bool
/// 保持和 TextField 一致 好替换
init(_ title:String, text:Binding<String>, isSecure:Bool = false) {
...
self.isSecure = isSecure
}
var body: some View {
HStack {
if !isSecure {
TextField(title, text: $text)
.focused($isFocus)
} else {
SecureField(title, text: $text)
.focused($isFocus)
}
...
}
}
...
}
我们登陆页面的用户名和密码输入框换成我们封装的输入框。
swift
struct UserNameValueContentView: View {
...
private var userNameField:some View {
ClearTextField("请输入用户名", text: $viewModel.userName)
}
}
swift
struct PasswordValueContentView: View {
...
private var passwordField:some View {
ClearTextField("请输入密码",
text: $viewModel.password,
isSecure: true)
}
}

我们运行看了一下效果,发现输入框的高度在获取焦点和失去焦点相互跳跃。原来在于我们给最后 Clear 按钮添加 Padding的时候,上下也添加了 Padding,导致 Clear出现的时候比 输入框的高度还搞 就自动拉伸了整个控件的高度。
为了解决高度跳动的问题,我们将输入框的高度固定在33,因为在UIKit中 UITextField的默认高度就是33。之后设置Clear按钮的高度也是33。
swift
struct ClearTextField: View {
...
var body: some View {
HStack {
...
if isShowClearButton {
/// `X`按钮
Image(systemName: "xmark")
.frame(maxHeight: .infinity)
.padding(.leading, 10)
.padding(.trailing, 10)
...
}
}
.frame(height:33)
}
...
}

此时我们的输入框的高度不会来回的跳跃了。
虽然我们用户名和密码可以一键的清空,但是我们我们修改用户名的时候,密码按道理说应该被清空。我们不考虑不同账号同一个密码的情况,这种极少存在。
那么我们监听到用户名输入框内容修改的时候,我们就清空密码输入框。
swift
class LoginPageViewModel: BaseViewModel {
...
/// 监听用户名输入
private var userNameCancellabel:AnyCancellable?
override init() {
...
userNameCancellabel = $userName.sink(receiveValue: {[weak self] _ in
guard let self = self else {return}
self.password = ""
})
}
...
}

我们运行发现,我们本来记住密码的功能失效了,密码被清空了。不明白为什么sink之后立马得到回掉,这个是导致密码被清空的原因所在。
不过我在研究的过程中,发现了一个规律可以解决这个问题。
swift
sink value = admin username = admin /// 相等不需要进行操作
sink value = admin1 username = admin /// 不想等需要进行操作
sink value = admin1 username = admin1 /// 相等不需要进行操作
虽然我们初始化和执行一次用户名更改调用了三次,但是需要执行的只有一次。我们只需要在判断 sink value != userName的情况下去操作我们的密码。
swift
class LoginPageViewModel: BaseViewModel {
...
/// 监听用户名输入
private var userNameCancellabel:AnyCancellable?
override init() {
...
userNameCancellabel = self.$userName.sink(receiveValue: {[weak self] name in
guard let self = self else {return}
/// 如果更新的用户名发生了变动,则清空密码输入框
guard name != self.userName else {return}
self.password = ""
})
}
...
}

我们输入框在被用户变更之后已经可以正常的删除密码,但是我们的登录按钮在用户名和密码都为空的情况下,竟然依然可以点击操作,这是不合理的。
我们希望在用户名和密码没有输入的情况下,按钮的背景颜色灰色看起来不可点击,当用户输入用户名和密码之后,登录按钮变亮可以点击。

在我们封装登录按钮的时候我们无法感知外界对于按钮的因素变化,现在只需要判断用户名和密码是否存在,后面可能会需要判断用户名的组成格式是否正确。
我们修改一下流程图。

这样我们登录按钮只需要关心外面传入的是否可以点击控制自己的状态。
swift
struct LoginButton: View {
@StateObject private var appColor = AppColor.share
/// 是否激活按钮
@Binding private var isActive:Bool
private let action:() -> Void
init(isActive:Binding<Bool>, action:@escaping () -> Void) {
self._isActive = isActive
self.action = action
}
var body: some View {
Button(action:action) {
Text("登录")
.frame(maxWidth:.infinity)
.frame(height: 45)
.background(background)
.foregroundColor(.white)
.cornerRadius(5)
}
.disabled(!isActive)
}
@ViewBuilder private var background:some View {
/// 按钮激活 背景色 #209090 按钮禁用 背景色 #cccccc
if isActive {
Color(uiColor: appColor.c_209090)
} else {
Color(uiColor: appColor.c_cccccc)
}
}
}
为了可以在登录页面根据输入的用户名和密码变化,更改登录按钮的激活状态。
swift
struct LoginPage: View {
@StateObject private var viewModel:LoginPageViewModel = LoginPageViewModel()
...
var body: some View {
PageContentView(title: "登陆", viewModel: viewModel) {
VStack {
...
VStack(spacing:30) {
...
LoginButton(isActive: $viewModel.isLoginButtonActive) {
Task {
await viewModel.login()
}
}
}
...
}
...
}
...
}
}
swift
class LoginPageViewModel: BaseViewModel {
...
private var cancellabel:Set<AnyCancellable> = []
/// 登录按钮是否激活
@Published var isLoginButtonActive:Bool = false
override init() {
...
self.$userName.sink(receiveValue: {[weak self] value in
guard let self = self else {return}
self.updateLoginButtonActive()
...
}).store(in: &cancellabel)
self.$password.sink {[weak self] value in
guard let self = self else {return}
self.updateLoginButtonActive()
}.store(in: &cancellabel)
}
...
/// 更新登录按钮的状态
private func updateLoginButtonActive() {
/// 只有用户名和密码通知不为空的时候才可以激活登录按钮
isLoginButtonActive = !userName.isEmpty && !password.isEmpty
}
}
