fatbobman / blogComments

1 stars 0 forks source link

掌握 SwiftUI 的 Safe Area | 肘子的Swift记事本 #128

Open fatbobman opened 2 years ago

fatbobman commented 2 years ago

https://www.fatbobman.com/posts/safeArea/

Safe Area(安全区域)是指不与导航栏、标签栏、工具栏或其他视图控制器提供的视图重叠的内容空间。本文将探讨如何在 SwiftUI 中获取 SafeAreaInsets、将视图绘制到安全区域之外、修改视图的安全区域等内容。

Horse888 commented 2 years ago

如果要让底部的内容做成圆角呢?我尝试了以下代码是不行的

.safeAreaInset(edge: .bottom, spacing: 0) {
                Text("底部状态条")

                    .font(.title3)
                    .foregroundColor(.indigo)
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40)
                    .padding()
                    .background(.gray)
                    .cornerRadius(30)

圆角应该是对 background 进行了clip, 导致不会再自动向非safeArea区域自动扩展了。

fatbobman commented 2 years ago

没有太看懂你的意思,我用了你提供的代码

image

safeArea 区域已经被设置为圆角

Horse888 commented 2 years ago

@fatbobman 没有太看懂你的意思,我用了你提供的代码

image

safeArea 区域已经被设置为圆角

是的,我的意思是使用圆角修饰后,下面的非安全区域没有被自动填充。如果去掉圆角,则会自动填充。是不是因为圆角对背景进等了clip的缘故?那怎样实现这个圆角(只需要看到上面的圆角, 下面非安全区背景自动扩展)呢?

fatbobman commented 2 years ago

换成 .containerShape(RoundedRectangle(cornerRadius: 30)) 就可以了

Horse888 commented 2 years ago

明白了。非常感谢。看了你写的文章,受益非浅。

fatbobman commented 2 years ago

不客气

Horse888 commented 2 years ago

@fatbobman 发现一个奇怪的问题. 我试图在你最后的 demo 中添加一个 Menu, 当键盘弹出时, 出现了奇怪的layout问题. 代码如下:

HStack(alignment: .firstTextBaseline) { // 即便修改alignment也无用
    // 输入框
    TextField("输入", text: $text)
        .focused($focused)
        .textFieldStyle(.roundedBorder)
        .padding(.horizontal, 10)
        .padding(.top, 10)
        .onSubmit {
            addMessage()
            scrollToBottom()
        }
        .onChange(of: focused) { value in
            if value {
                scrollToBottom()
            }
        }
    // 添加 Menu 后, 键盘弹出后, Menu 出现 layout 问题
    Menu {
        Text("A")
        Text("B")
        Text("C")
    } label: {
        Image(systemName: "lock")
            .frame(width: 60, height: 60) // 似乎和设置 frame 有关系?
    }

    // 回复按钮
    Button("回复") {
        addMessage()
        scrollToBottom()
        focused = false
    }
    .buttonStyle(.bordered)
    .controlSize(.small)
    .tint(.green)
}
.padding(.horizontal, 30)
}
Horse888 commented 2 years ago

换成 .containerShape(RoundedRectangle(cornerRadius: 30)) 就可以了

这里修改cornerRadius为更小的值(比如10), 但为什么并不起作用呢? 圆角依旧很大.

fatbobman commented 2 years ago
HStack(alignment: .firstTextBaseline) {

目前的解决方法是,取消对齐设定

HStack(alignment: .firstTextBaseline) {

// 改成

HStack {
fatbobman commented 2 years ago

换成 .containerShape(RoundedRectangle(cornerRadius: 30)) 就可以了

这里修改cornerRadius为更小的值(比如10), 但为什么并不起作用呢? 圆角依旧很大.

image

我这里正常

image
Horse888 commented 2 years ago
HStack(alignment: .firstTextBaseline) {

目前的解决方法是,取消对齐设定

HStack(alignment: .firstTextBaseline) {

// 改成

HStack {

奇怪, 我在模拟器和真机上试了好像都不行, 当键盘弹出时, menu跑飞了.

image image

整个测试代码如下:

struct ChatBarDemo: View {
    @State var messages: [Message] = (0...60).map { Message(text: "message:\($0)") }
    @State var text = ""
    @FocusState var focused: Bool
    @State var bottomTrigger = false
    var body: some View {
        NavigationView {
            ScrollViewReader { proxy in
                List {
                    ForEach(messages) { message in
                        Text(message.text)
                            .id(message.id)
                    }
                }
                .listStyle(.inset)
                .safeAreaInset(edge: .bottom) {
                    ZStack(alignment: .top) {
                        Color.clear
                        Rectangle().fill(.secondary).opacity(0.3).frame(height: 0.6) // 上部线条
                        HStack {
                            // 输入框
                            TextField("输入", text: $text)
                                .focused($focused)
                                .textFieldStyle(.roundedBorder)
                                .padding(.horizontal, 10)
                                .padding(.top, 10)
                                .onSubmit {
                                    addMessage()
                                    scrollToBottom()
                                }
                                .onChange(of: focused) { value in
                                    if value {
                                        scrollToBottom()
                                    }
                                }
                            // 添加 Menu 后, 键盘弹出后, Menu 出现 layout 问题
                            Menu {
                                Text("A")
                                Text("B")
                                Text("C")
                            } label: {
                                Image(systemName: "lock")
                                    .frame(width: 60, height: 60) // 似乎和设置 frame 有关系?
                            }

                            // 回复按钮
                            Button("回复") {
                                addMessage()
                                scrollToBottom()
                                focused = false
                            }
                            .buttonStyle(.bordered)
                            .controlSize(.small)
                            .tint(.green)
                        }
                        .padding(.horizontal, 30)
                    }
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 53)
                    .background(.regularMaterial)
                    .containerShape(RoundedRectangle(cornerRadius: 2, style: .continuous))
                    .shadow(color: .yellow, radius: 10, x: 0, y: 5)
                }
                .onChange(of: bottomTrigger) { _ in
                    withAnimation(.spring()) {
                        if let last = messages.last {
                            proxy.scrollTo(last.id, anchor: .bottom)
                        }
                    }
                }
                .onAppear {
                    if let last = messages.last {
                        proxy.scrollTo(last.id, anchor: .bottom)
                    }
                }
            }
            .navigationBarTitle("SafeArea Chat Demo")
        }
    }

    func scrollToBottom() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            bottomTrigger.toggle()
        }
    }

    func addMessage() {
        if !text.isEmpty {
            withAnimation {
                messages.append(Message(text: text))
            }
            text = ""
        }
    }
}

struct Message: Identifiable, Hashable {
    let id = UUID()
    let text: String
}

圆角我设置为2, 但结果如下:

image
atsixian commented 11 months ago

非常感谢,解决了我的大问题。不过现在用 keyWindow 会导致 preview 报错,因为 UIApplication 已经有 keyWindow 这个属性在了。换成 currentWindow 可以解决:

extension UIApplication {
-    var keyWindow: UIWindow? {
+    var currentWindow: UIWindow? {

        connectedScenes
            .compactMap {
                $0 as? UIWindowScene
            }
            .flatMap {
                $0.windows
            }
            .first {
                $0.isKeyWindow
            }
    }
}

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
-        UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
+        UIApplication.shared.currentWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
    }
}
fatbobman commented 11 months ago

非常感谢,解决了我的大问题。不过现在用 keyWindow 会导致 preview 报错,因为 UIApplication 已经有 keyWindow 这个属性在了。换成 currentWindow 可以解决:

extension UIApplication {
-    var keyWindow: UIWindow? {
+    var currentWindow: UIWindow? {

        connectedScenes
            .compactMap {
                $0 as? UIWindowScene
            }
            .flatMap {
                $0.windows
            }
            .first {
                $0.isKeyWindow
            }
    }
}

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
-        UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
+        UIApplication.shared.currentWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
    }
}

谢谢你的反馈。 UIApplication 的 keyWindow 应该在 iOS 13 中软废弃了。不过确实可能导致其他的问题。我调整一下代码。

07akioni commented 7 months ago

我观察滚动到底部这块的代码是通过延迟才做到的,有没有什么办法可以让滚动区域是和键盘的弹出是同步的?

func scrollToBottom() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
        bottomTrigger.toggle()
    }
}

我今天研究了一下各种软件的聊天界面,都是可以做到键盘拉起的时候消息始终和底部对齐的,但是相关的资料确实太少了,望大佬赐教,🙏

而且 scrollDismissesKeyboard 设为 .interactively 的时候,safeAreaInset 是有 bug 的: https://stackoverflow.com/questions/73770425/scrollview-scrolldismisseskeyboard-interactively-feels-weird

我看到有一个解决方式,似乎非常的复杂,所以还没有尝试 https://github.com/frogcjn/BottomInputBarSwiftUI/blob/main/BottomInputBarSwiftUI/BottomBar/UIBottomBar.Constraints.swift

还有一个类似的 stackoverflow 问题: https://stackoverflow.com/questions/45708312/detect-keyboard-height-while-uiscrollview-is-scrolled-down-and-the-keypad-is-bei

fatbobman commented 7 months ago

时间有点久了,一些细节记不清了。 这篇文章是介绍 safearea 的,文章中的例子主要是演示 safearea 的用法。因此在获取焦点后会修改两次状态。在 SwiftUI 中,在一个 loop 中如果修改多个状态有时会出现问题。因此在两种状态之间添加了延迟。 如果想尝试同步弹出,驱动的方式肯定要调整。 另外,目前例子里通过给所有 row 添加 id 的方式也不是太好的方式,只滚动到底部的话,可以尝试只在最下面添加一个包含 id 的底部 row,另一篇介绍 List 数据集优化的文章中有提及。 总之,想实现同时弹出的效果肯定是可以的,但用纯 SwiftUI 的方式会有些问题,甚至在键盘弹出时,safearea 的区域也会有一点延迟。这是当前响应式框架的问题。

2024年2月27日 19:45,07akioni @.***> 写道:

我观察滚动到底部这块的代码是通过延迟才做到的,有没有什么办法可以让滚动区域是和键盘的弹出是同步的?

func scrollToBottom() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { bottomTrigger.toggle() } } — Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/128#issuecomment-1966368816, or unsubscribe https://github.com/notifications/unsubscribe-auth/ANIYIGLV2BXZX36EBKNFPCDYVXBMVAVCNFSM5IPWITB2U5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOJWGYZTMOBYGE3A. You are receiving this because you were mentioned.

07akioni commented 7 months ago

给所有 row 添加 id 的方式也不是太好的方式,只滚动到底部的话,可以尝试只在最下面添加一个包含 id 的底部 row,另一篇介绍 List 数据集优化的文章中有提

感觉 SwiftUI 还是任重道远。主要是我自己观察大厂的 app 都能做到同步的表现,但是搜索却很难找到答案,这个交互对于一个初学者来说感觉确实有点核心机密的程度了。