第十四章 async/await|overlay|PreferencrKey|Anchor

接下来我们在 LoginPageViewModel 完成 LoginPage 页面的业务逻辑。对于获取到用户输入的用户名和密码,保存记住密码状态,我们都已经通过 属性包装器完成了。

剩下的业务逻辑,就是根据我们设置的服务器地址进行登陆。

对于目前的第三方请求库还不支持 async/await 特性,我们准备使用我们的 Request 请求框架引入这个特性来支持。

我们通过 Swift Package Managergithub.com/josercc/Req... 库添加到工程里面。

通过 withCheckedThrowingContinuation 将之前闭包转化为 async/await

我们看到 Request 通过下面代码对于 async/await 的支持的。

swift 复制代码
@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 文件,用来管理我们请求。

swift 复制代码
class Api: API {
    static var host: String {""}
    static var defaultHeadersConfig: ((inout HTTPHeaders) -> Void)?
}

对于 Api 这个类我们需要在 host 的静态属性返回我们选择的服务器地址,刚才我们保存在 @AppStorage 里面了。

但是我们声明的属性在 LoginPageViewModel 里面,我们在 Api 这个类拿不到。为了可以方便数据的访问,我们新建一个单利保存App的公共变量。

我们在 Define 文件夹新建一个文件 AppConfig.swift

swift 复制代码
class AppConfig: ObservableObject {
    static let share = AppConfig()
    
    /// 当前 App 的服务器地址
    @AppStorage("currentAppServer")
    var currentAppServer:String = ""
}

我们删除掉 LoginPageViewModelcurrentAppServer 变量,将读取和设置的逻辑修改为 AppConfig 中的 currentAppServer

那么我们就可以在 Api 中设置我们选中的服务器地址了。

swift 复制代码
static var host: String {AppConfig.share.currentAppServer}

在切换环境的过程中,我们发现我们弹出 PopMenuButton,会调整自身的高度,导致我们登陆页面的用户名和密码输入框试图被向下转移。

通过 PreferenceKey 在 GeometryReader 准确定位

如果内容很多,岂不是下面的输入框都被推下面看不见了,这个问题十分的严重。我们暂时毫无头绪,只能谷歌一下相关的内容。

很遗憾的是,在我谷歌了很久,并且加了很多技术群,依然毫无进展,就在我放弃的时候。我突然想到 GeometryReader 可以确定我们小组件的位置,那么我们可以获取位置时候将数据提交给我们最外层的 PopMenuButtonModify 组件,之后将位置偏移到对应的位置,这样是否就可以得以解决了?

这只是我的猜测,但是我觉得这个思路实现起来没有任何的知识盲区,而且流程是通畅的,应该问题不大。

根据这个思路,我找到了 PreferenceKey的相关知识,从而延伸的看到了 Anchor的内容,经过摸索之前,实验了一下成功了。

我们先从我们预览测试的组件开始修改,我们的预览组件下面代码。

swift 复制代码
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的引入到最外层,这样就可以不被其他的字组件进行遮挡了。

swift 复制代码
var body: some View {
    VStack {
        Text(currentItem)
        Text("1234")
    }
    .popMenuButton(items: item,
                   currentItem: $currentItem)
}

看似很完美,但是我们将我们的最外层组件扩大到全屏幕。

swift 复制代码
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 的源代码改造一下。

swift 复制代码
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 显示不出来内容了,因为底部的组件宽度太小了。我们暂时调整一下底部组件的大小。

swift 复制代码
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,我们新建一个协议。

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

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

我们在需要弹出组件的地方添加下面代码。

swift 复制代码
Text(currentItem)
    .anchorPreference(key: PopMenuButtonSourceKey.self,
                      value: .bounds,
                      transform: {$0})

GeometryReader 读取 Anchor 中的值

此时我们可以拿到弹出组件的大小和位置信息了。可以使用我们拿到的 Anchor的信息,我们需要用一个 GeometryReader进行包裹 PopMenuButton

swift 复制代码
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 交给外部控制。

swift 复制代码
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 组件并不会出现。

我当场就慌了,这和我预想的不太一样,我们组件是解包完毕会绘制,难道我们的就不存在,我们修改一下当值不存在显示一个错误信息。

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

果然我们的界面出现了错误提示。

当我以为这是SwiftUIBug的时候,在复杂的页面不工作的时候,我看到这一篇文章

coderedirect.com/questions/4...

难道我们真的需要包装一层皮,带着试试态度,我们继续修改代码。

swift 复制代码
fileprivate struct PopMenuButtonSourceKey: PreferenceKey {
    static var defaultValue:[Anchor<CGRect>] = []
    static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) {
        value.append(contentsOf: nextValue())
    }
}

换成数组,之后每次添加进去,果然可以了。

我猜测如果直接相等,如果试图复杂,那么可能在一个分支走完就结束了,就拿不到对应的值,这是我的一个猜测,知道的大神指导一下。

相关推荐
君赏4 小时前
第十五章 Task|NSAppTransportSecurity|keyDecodingStrategy
swiftui
君赏4 小时前
第十三章 Button|cornerRadius
swiftui
君赏4 小时前
第六章 Published|ObservedObject|EnvironmentObject|Environment
swiftui
君赏4 小时前
第八章 封装MVVM|onTapGesture|AppStorage
swiftui
君赏4 小时前
第十二章 TextField|EmptyView|SecureField
swiftui
君赏4 小时前
第四章 Preview Device|Expand|Alignment|LineLimit|Rectangle|ForegroundColor
swiftui
君赏4 小时前
第七章 组件提炼|代码清爽|Padding
swiftui
君赏4 小时前
第五章 如何使用Xcode Package Injection加速依赖Swift Package Manager
swiftui
君赏4 小时前
第十章 ScrollView|Top布局|SF符号|@StateObject|@State|@Binding
swiftui