You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI在.safeAreaInset内添加Menu出现异常布局问题求助

解决SwiftUI中SafeAreaInset内Menu随键盘偏移的布局问题

问题背景

在safeAreaInset中嵌入Menu组件时,键盘弹出后Menu按钮会出现轻微上移,点击Menu还会触发异常布局,该问题在iOS15和iOS16版本中均存在。

初始测试代码

import SwiftUI

struct TestMenuInSafeAreaInset: View {
    @State private var message = ""

    var body: some View {
        VStack {
            Rectangle()
                .fill(Color.blue)
        }
        .safeAreaInset(edge: .bottom) {
            HStack {
                TextField("输入消息", text: $message)
                    .padding()
                    .background(Color.brown)
                    .cornerRadius(12)
                    .foregroundColor(.white)
                Menu {
                    Button {

                    } label: {
                        Text("确认")
                    }

                    Button {

                    } label: {
                        Text("取消")
                    }

                } label: {
                    Image(systemName: "square.and.arrow.up.fill")
                        .tint(.white)
                        .padding()
                        .background(Color.brown)
                        .cornerRadius(50)
                }
            }
            .padding()
        }
    }
}

struct TestMenuInSafeAreaInset_Previews: PreviewProvider {
    static var previews: some View {
        TestMenuInSafeAreaInset()
    }
}

问题现象

  • 正常状态:
    正常状态
  • 键盘首次弹出时,右侧Menu按钮上移:
    键盘弹出时Menu上移动图

无效尝试

给Menu添加固定尺寸后问题仍复现(测试环境:iPhone14 Pro模拟器、iOS16):

struct TestMenuInSafeAreaInset: View {
    @State private var message = ""

    var body: some View {
        VStack {
            TextField("输入用户名", text: $message)
                .padding()
                .background(Color.gray.opacity(0.3))
                .cornerRadius(12)
                .padding()
            Spacer()
        }
        .safeAreaInset(edge: .bottom) {
            HStack {
                Spacer()
                    .frame(height: 70)
                Menu {
                    Button {

                    } label: {
                        Text("确认")
                    }

                    Button {

                    } label: {
                        Text("取消")
                    }
                } label: {
                    Image(systemName: "square.and.arrow.up.fill")
                        .padding()
                        .tint(.white)
                        .background(Color.brown)
                        .cornerRadius(50)
                }
                .frame(width: 50, height: 50)
            }
            .padding(.horizontal, 20)
            .background(Color.blue)
        }
    }
}

此时现象:
Menu图标上移

可行解决方案

该问题属于SwiftUI中Menu与safeAreaInset、键盘交互的布局bug,以下是三种有效的解决方式:

方案1:用UIKit Menu替代SwiftUI Menu

通过UIViewRepresentable封装UIKit的UIMenu,规避SwiftUI的布局冲突:

import SwiftUI

struct UIKitMenuButton: UIViewRepresentable {
    let menuActions: [UIAction]
    
    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "square.and.arrow.up.fill"), for: .normal)
        button.tintColor = .white
        button.backgroundColor = .brown
        button.layer.cornerRadius = 25
        button.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        
        let menu = UIMenu(children: menuActions)
        button.showsMenuAsPrimaryAction = true
        button.menu = menu
        return button
    }
    
    func updateUIView(_ uiView: UIButton, context: Context) {}
}

// 使用示例
struct TestMenuInSafeAreaInset: View {
    @State private var message = ""

    var body: some View {
        VStack {
            TextField("输入消息", text: $message)
                .padding()
                .background(Color.gray.opacity(0.3))
                .cornerRadius(12)
                .padding()
            Spacer()
        }
        .safeAreaInset(edge: .bottom) {
            HStack(spacing: 16) {
                TextField("输入消息", text: $message)
                    .padding()
                    .background(Color.brown)
                    .cornerRadius(12)
                    .foregroundColor(.white)
                UIKitMenuButton(menuActions: [
                    UIAction(title: "确认") { _ in
                        // 执行确认逻辑
                    },
                    UIAction(title: "取消") { _ in
                        // 执行取消逻辑
                    }
                ])
            }
            .padding()
        }
    }
}

方案2:改用Toolbar替代SafeAreaInset

将底部操作栏移至toolbar的.bottomBar位置,该方式与键盘的交互逻辑更稳定:

import SwiftUI

struct TestMenuInSafeAreaInset: View {
    @State private var message = ""

    var body: some View {
        VStack {
            TextField("输入消息", text: $message)
                .padding()
                .background(Color.gray.opacity(0.3))
                .cornerRadius(12)
                .padding()
            Spacer()
        }
        .toolbar {
            ToolbarItem(placement: .bottomBar) {
                HStack(spacing: 16) {
                    TextField("输入消息", text: $message)
                        .padding()
                        .background(Color.brown)
                        .cornerRadius(12)
                        .foregroundColor(.white)
                    Menu {
                        Button {
                            // 确认逻辑
                        } label: {
                            Text("确认")
                        }
                        Button {
                            // 取消逻辑
                        } label: {
                            Text("取消")
                        }
                    } label: {
                        Image(systemName: "square.and.arrow.up.fill")
                            .tint(.white)
                            .padding()
                            .background(Color.brown)
                            .cornerRadius(50)
                    }
                }
                .padding()
            }
        }
    }
}

方案3:强制锁定Menu布局

通过设置布局优先级和固定尺寸,强制Menu保持位置:

import SwiftUI

struct TestMenuInSafeAreaInset: View {
    @State private var message = ""

    var body: some View {
        VStack {
            TextField("输入消息", text: $message)
                .padding()
                .background(Color.gray.opacity(0.3))
                .cornerRadius(12)
                .padding()
            Spacer()
        }
        .safeAreaInset(edge: .bottom) {
            HStack(spacing: 16) {
                // 让TextField优先占用可用空间
                TextField("输入消息", text: $message)
                    .padding()
                    .background(Color.brown)
                    .cornerRadius(12)
                    .foregroundColor(.white)
                    .layoutPriority(1)
                
                Menu {
                    Button {
                        // 确认逻辑
                    } label: {
                        Text("确认")
                    }
                    Button {
                        // 取消逻辑
                    } label: {
                        Text("取消")
                    }
                } label: {
                    Image(systemName: "square.and.arrow.up.fill")
                        .tint(.white)
                        .padding()
                        .background(Color.brown)
                        .cornerRadius(50)
                        // 固定图标尺寸
                        .frame(width: 50, height: 50)
                        .fixedSize()
                }
                // 固定Menu容器尺寸
                .frame(width: 50, height: 50)
                .fixedSize()
            }
            .padding()
            // 给底部栏添加背景,避免与键盘交互时的布局闪烁
            .background(Color(UIColor.systemBackground))
        }
    }
}

内容的提问来源于stack exchange,提问作者ZYiOS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 00:35:21