接下来我们在 LoginPageViewModel 完成 LoginPage 页面的业务逻辑。对于获取到用户输入的用户名和密码,保存记住密码状态,我们都已经通过 属性包装器完成了。
剩下的业务逻辑,就是根据我们设置的服务器地址进行登陆。
对于目前的第三方请求库还不支持 async/await 特性,我们准备使用我们的 Request 请求框架引入这个特性来支持。
我们通过 Swift Package Manager 将 github.com/josercc/Req… 库添加到工程里面。
通过 withCheckedThrowingContinuation 将之前闭包转化为 async/await
我们看到 Request 通过下面代码对于 async/await 的支持的。
@available(iOS 15.0.0, *)
@available(macOS 12.0.0, *)
public static func request<M:Model, A:APIConfig>(type:M.Type,
config:A) async throws -> M {
try await withCheckedThrowingContinuation({ continuation in
request(type: type, config: config, success: { model in
continuation.resume(returning: model)
}, failure: { code, message in
continuation.resume(throwing: NSError(domain: message,
code: code,
userInfo: nil))
})
})
}
核心是通过 withCheckedThrowingContinuation 这个函数做到的,还有其他的函数我们先不去深入研究。
其实对于之前闭包的支持,感觉和 Future的机制很像,比如我的一个支持低版本类似 Future的异步并发框架 github.com/josercc/Asy…
我们在 Common 目录创建一个 Api.swift 文件,用来管理我们请求。
class Api: API {
static var host: String {""}
static var defaultHeadersConfig: ((inout HTTPHeaders) -> Void)?
}
对于 Api 这个类我们需要在 host 的静态属性返回我们选择的服务器地址,刚才我们保存在 @AppStorage 里面了。
但是我们声明的属性在 LoginPageViewModel 里面,我们在 Api 这个类拿不到。为了可以方便数据的访问,我们新建一个单利保存App的公共变量。
我们在 Define 文件夹新建一个文件 AppConfig.swift。
class AppConfig: ObservableObject {
static let share = AppConfig()
/// 当前 App 的服务器地址
@AppStorage("currentAppServer")
var currentAppServer:String = ""
}
我们删除掉 LoginPageViewModel 中 currentAppServer 变量,将读取和设置的逻辑修改为 AppConfig 中的 currentAppServer。
那么我们就可以在 Api 中设置我们选中的服务器地址了。
static var host: String {AppConfig.share.currentAppServer}
在切换环境的过程中,我们发现我们弹出 PopMenuButton,会调整自身的高度,导致我们登陆页面的用户名和密码输入框试图被向下转移。
通过 PreferenceKey 在 GeometryReader 准确定位
如果内容很多,岂不是下面的输入框都被推下面看不见了,这个问题十分的严重。我们暂时毫无头绪,只能谷歌一下相关的内容。
很遗憾的是,在我谷歌了很久,并且加了很多技术群,依然毫无进展,就在我放弃的时候。我突然想到 GeometryReader 可以确定我们小组件的位置,那么我们可以获取位置时候将数据提交给我们最外层的 PopMenuButtonModify 组件,之后将位置偏移到对应的位置,这样是否就可以得以解决了?
这只是我的猜测,但是我觉得这个思路实现起来没有任何的知识盲区,而且流程是通畅的,应该问题不大。
根据这个思路,我找到了 PreferenceKey的相关知识,从而延伸的看到了 Anchor的内容,经过摸索之前,实验了一下成功了。
我们先从我们预览测试的组件开始修改,我们的预览组件下面代码。
struct PopMenuButtonModifyPreview: View {
let item:[String] = [
"item 1",
"item 2"
]
@State var currentItem:String = "Hello World!"
@State var location:CGSize = .zero
var body: some View {
VStack {
Text(currentItem)
.popMenuButton(items: item,
currentItem: $currentItem)
Text("1234")
}
}
}
我们将 PopMenuButton的引入到最外层,这样就可以不被其他的字组件进行遮挡了。
var body: some View {
VStack {
Text(currentItem)
Text("1234")
}
.popMenuButton(items: item,
currentItem: $currentItem)
}
看似很完美,但是我们将我们的最外层组件扩大到全屏幕。
var body: some View {
VStack {
Spacer()
.frame(height:200)
Text(currentItem)
Text("1234")
Spacer()
}
.popMenuButton(items: item,
currentItem: $currentItem)
}
但是我们 PopMenuButton的区域在最中间,可是我们想显示在 HelloWorld的文本上面。
既然想自定义设置位置,那么一定需要用到我们 GeometryReader 的组件,还要获取 Hello World 组件的位置。
overlay 遮罩布局|opacity透明度
我们将 PopMenuButtonModify 的源代码改造一下。
struct PopMenuButtonModify: ViewModifier {
let items:[String]
@Binding var currentItem:String
@State private var isShowPopMenuButton:Bool = false
func body(content: Content) -> some View {
content
.onTapGesture {
isShowPopMenuButton = true
}
.overlay {
PopMenuButton(items: items,
currentItem: $currentItem) { item in
currentItem = item
isShowPopMenuButton = false
}
.opacity(isShowPopMenuButton ? 1 : 0)
}
}
}
我们的 PopMenuButton 显示不出来内容了,因为底部的组件宽度太小了。我们暂时调整一下底部组件的大小。
var body: some View {
VStack {
Spacer()
.frame(height:200)
Text(currentItem)
Text("1234")
Spacer()
}
.frame(maxWidth: .infinity)
.popMenuButton(items: item,
currentItem: $currentItem)
}
overlayPreferenceValue 获取 Preference 设置的值
但是我们刚才用 ZStack 的效果一模一样,但是不要着急,我们对于 overlay 可以换成 overlayPreferenceValue。这个是可以方便监听 Preference 值的组件,但是需要一个 PreferenceKey 的协议。
我们目的是接受 HelloWorld 组件的 Bound,我们新建一个协议。
fileprivate struct PopMenuButtonSourceKey: PreferenceKey {
static var defaultValue: Anchor<CGRect>?
static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
value = nextValue()
}
}
Anchor获取视图位置
Anchor<T> 是一个范型结构体,而 Anchor<CGRect>可以很方便的让我们获取其他组件的 Bound。
我们修改一下 PopMenuButtonModify的代码
func body(content: Content) -> some View {
content
.onTapGesture {
isShowPopMenuButton = true
}
.overlayPreferenceValue(PopMenuButtonSourceKey.self) { preference in
PopMenuButton(items: items,
currentItem: $currentItem) { item in
currentItem = item
isShowPopMenuButton = false
}
.opacity(isShowPopMenuButton ? 1 : 0)
}
}
我们在需要弹出组件的地方添加下面代码。
Text(currentItem)
.anchorPreference(key: PopMenuButtonSourceKey.self,
value: .bounds,
transform: {$0})
GeometryReader 读取 Anchor 中的值
此时我们可以拿到弹出组件的大小和位置信息了。可以使用我们拿到的 Anchor的信息,我们需要用一个 GeometryReader进行包裹 PopMenuButton。
GeometryReader { geometry in
preference.map { anchor in
PopMenuButton(items: items,
currentItem: $currentItem) { item in
currentItem = item
isShowPopMenuButton = false
}
.opacity(isShowPopMenuButton ? 1 : 0)
.offset(x: 0, y: geometry[anchor].minY)
}
}
Anchor 需要我们通过 GeometryProxy访问,我们 preference.map 是为了解包,让我们方便用上值 Anchor。
此时终于满足我们的需求了。
但是整个外部试图控制试图的弹出,不符合我们的交互,所以是否弹出就做成 @Binding 交给外部控制。
struct PopMenuButtonModify: ViewModifier {
let items:[String]
@Binding var currentItem:String
@Binding var isShowPopMenuButton:Bool
func body(content: Content) -> some View {
content
.overlayPreferenceValue(PopMenuButtonSourceKey.self) { preference in
GeometryReader { geometry in
preference.map { anchor in
PopMenuButton(items: items,
currentItem: $currentItem) { item in
currentItem = item
isShowPopMenuButton = false
}
.opacity(isShowPopMenuButton ? 1 : 0)
.offset(x: 0, y: geometry[anchor].minY)
}
}
}
}
}
Preference 的一个Bug
我们迫不及待的修改了 LoginPage 的逻辑,但是遗憾的是,我们的 PopMenuButton 组件并不会出现。
我当场就慌了,这和我预想的不太一样,我们组件是解包完毕会绘制,难道我们的就不存在,我们修改一下当值不存在显示一个错误信息。
if let p = preference {
preference.map { anchor in
PopMenuButton(items: items,
currentItem: $currentItem) { item in
currentItem = item
isShowPopMenuButton = false
}
.opacity(isShowPopMenuButton ? 1 : 0)
.offset(x: 0, y: geometry[anchor].minY)
}
} else {
Text("Error preference not exit")
}
果然我们的界面出现了错误提示。
当我以为这是SwiftUI 的 Bug的时候,在复杂的页面不工作的时候,我看到这一篇文章
难道我们真的需要包装一层皮,带着试试态度,我们继续修改代码。
fileprivate struct PopMenuButtonSourceKey: PreferenceKey {
static var defaultValue:[Anchor<CGRect>] = []
static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) {
value.append(contentsOf: nextValue())
}
}
换成数组,之后每次添加进去,果然可以了。
我猜测如果直接相等,如果试图复杂,那么可能在一个分支走完就结束了,就拿不到对应的值,这是我的一个猜测,知道的大神指导一下。