SwiftUI中如何让HStack在消息回复场景匹配父视图宽度?
SwiftUI 复刻 WhatsApp 消息回复功能的宽度适配问题
我正在用 SwiftUI 复刻 WhatsApp 的消息回复功能,消息视图最大宽度设为 300。当消息关联了被回复的旧消息时,旧消息会显示在当前消息上方,现在的问题是:我希望旧消息的宽度和当前消息保持一致——旧消息内容比当前消息短时,它的背景要延伸到当前消息的宽度,但又不能把宽度设为 .infinity,否则当前消息右侧会出现大量空白。
现有代码
struct DMMessageBubbleReceiver: View{ let impactMed = UIImpactFeedbackGenerator(style: .medium) @State var message: Message @State private var showActionSheet = false @State var userUID: String @State var showEmojis = false @State private var scale: CGFloat = 0.5 @State var emojis = ["😂", "❤️", "👍", "👎", "✈️", "☠️"] @Binding var blurEverything: String @ObservedObject var homeData = HomeViewModel.shared @State private var offsetX: CGFloat = 0 @Binding var isReplying: Bool @Binding var replyToMessage: MsgReply var body: some View { VStack(alignment: .leading, spacing: 0) { if showEmojis { HStack(spacing: 17) { ForEach(emojis, id: \.self) { emoji in Button(action: { impactMed.impactOccurred() withAnimation { showEmojis.toggle() blurEverything = "" } homeData.addRemoveEmoji(message: message, emoji: emoji, userUID: userUID) }) { Text(emoji) .font(.custom("Poppins-Regular", fixedSize: 18)) } } } .padding(.horizontal).padding(.vertical, 5) .background(Color(UIColor.systemGray5).cornerRadius(50)) .padding(.bottom, 5) .scaleEffect(scale) .onAppear { withAnimation(.easeInOut(duration: 0.3)) { scale = 1.2 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { impactMed.impactOccurred() withAnimation(.easeInOut(duration: 0.3)) { scale = 1.0 } } } } } HStack { HStack { if message.deleted != true { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 0){ if let reply = message.reply{ HStack{ VStack(alignment: .leading, spacing: 5){ Text("You") .font(.custom("Gilroy-Medium", fixedSize: 12)) .foregroundColor(.blue) .lineSpacing(5) Text(reply.msg) .font(.custom("Gilroy-Medium", fixedSize: 13)) .foregroundColor(.black) .lineSpacing(5) .lineLimit(3) } }.padding(10).background(Color.blue.opacity(0.07).cornerRadius(5)).padding(.horizontal, 5).padding(.top, 5) } Text(filterLinksInText(message.text)) .font(.custom("Gilroy-Medium", fixedSize: 15)) .foregroundColor(.black) .lineSpacing(5) .padding(.vertical, 14).padding(.leading, 10).padding(.trailing, 15) } .background(Color(UIColor.systemGray5).opacity(0.6)) .cornerRadius(12, corners: [.topLeft, .topRight, .bottomRight]) .offset(x: offsetX) .gesture( DragGesture() .onChanged { value in if message.deleted == false { if value.translation.width > 0 { offsetX = value.translation.width } } } .onEnded { value in if offsetX > 100 { impactMed.impactOccurred() withAnimation { isReplying = true replyToMessage = MsgReply(id: message.id, msg: message.text, name: "name") offsetX = 0 } } else { withAnimation { offsetX = 0 } } } ) if message.emoji != "" { Text(message.emoji) .font(.system(size: 12)) .foregroundColor(.red) .padding(3) .background(Color("messagebackground")) .cornerRadius(50) .offset(y: 8) .offset(x: 8) } } } else { Text("This message was deleted") .font(Font.system(size: 13)) .italic() .fontWeight(.medium) .foregroundColor(.black.opacity(0.6)) .padding(.horizontal) .padding(.vertical, 10) .background(Color(UIColor.systemGray5)) .cornerRadius(12, corners: [.topLeft, .topRight, .bottomRight]) } } .frame(maxWidth: 300, alignment: .leading) Spacer() ZStack(alignment: .bottomTrailing) { Text(" ") }.padding(.trailing) } Text("\(message.timestamp.formatted(.dateTime.hour().minute()))") .font(.custom("Gilroy-Medium", fixedSize: 11)) .foregroundColor(.black.opacity(0.3)) .padding(.leading, 5).padding(.top, 8).padding(.bottom, 10) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading) .padding(.horizontal, 10) .blur(radius: (blurEverything != "" && blurEverything != message.id) ? 2 : 0) .onTapGesture(count: 2) { if message.deleted != true{ self.likeMessage(message: message) } } .onTapGesture(count: 1) { if message.deleted == false { withAnimation { showEmojis.toggle() if blurEverything != "" { blurEverything = "" } else { blurEverything = message.id } } } } .onLongPressGesture { showActionSheet = true } .actionSheet(isPresented: $showActionSheet) { ActionSheet(title: Text("Message Options"), buttons: getButtons(message: message)) } } func getButtons(message: Message) -> [ActionSheet.Button] { var buttons: [ActionSheet.Button] = [] if message.deleted != true { buttons.append(.default(Text("Copy"), action: { self.copyText(message: message.text) })) } if message.deleted != true { if message.numLikes > 0 { buttons.append(.default(Text("Unlike"), action: { self.likeMessage(message: message) })) } else { buttons.append(.default(Text("Like"), action: { self.likeMessage(message: message) })) } } if message.deleted == false { buttons.append(.destructive(Text("Delete"), action: { self.deleteMessage() })) } buttons.append(.cancel(Text("Cancel"))) return buttons } func copyText(message: String) { UIPasteboard.general.string = message } func likeMessage(message: Message) { impactMed.impactOccurred() homeData.likeDMMessage(message: message, userUID: userUID) } func deleteMessage() { impactMed.impactOccurred() homeData.deleteDMMessage(userUID: userUID, message: message) } func filterLinksInText(_ text: String) -> String { do { let regexPattern = try NSRegularExpression(pattern: "https?://([-\\w\\.]+)+(:\\d+)?(/([\\w/_\\.]*(\\?\\S+)?)?)?", options: .caseInsensitive) let modifiedText = regexPattern.stringByReplacingMatches( in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "****" ) return modifiedText } catch { return text } } }
解决方案
核心思路是让回复消息的容器匹配当前消息的实际渲染宽度,同时保留最大宽度300的限制,避免右侧空白。具体实现步骤如下:
1. 定义偏好键捕获当前消息宽度
通过PreferenceKey捕获当前消息文本的实际宽度:
struct MessageWidthPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } }
2. 添加状态变量存储宽度
在DMMessageBubbleReceiver结构体中添加状态变量,用于保存捕获到的消息宽度:
@State private var messageWidth: CGFloat = 0
3. 修改消息气泡结构,绑定宽度
调整回复消息和当前消息的布局,让回复消息容器继承当前消息的实际宽度:
// 替换原代码中ZStack内的VStack部分 VStack(alignment: .leading, spacing: 0){ if let reply = message.reply{ HStack{ VStack(alignment: .leading, spacing: 5){ Text("You") .font(.custom("Gilroy-Medium", fixedSize: 12)) .foregroundColor(.blue) .lineSpacing(5) Text(reply.msg) .font(.custom("Gilroy-Medium", fixedSize: 13)) .foregroundColor(.black) .lineSpacing(5) .lineLimit(3) } .frame(maxWidth: .infinity, alignment: .leading) // 内容左对齐,背景填充父容器 } .padding(10) .background(Color.blue.opacity(0.07).cornerRadius(5)) .padding(.horizontal, 5) .padding(.top, 5) .frame(width: messageWidth, alignment: .leading) // 绑定到当前消息宽度 } Text(filterLinksInText(message.text)) .font(.custom("Gilroy-Medium", fixedSize: 15)) .foregroundColor(.black) .lineSpacing(5) .padding(.vertical, 14) .padding(.leading, 10) .padding(.trailing, 15) .background(Color(UIColor.systemGray5).opacity(0.6)) .cornerRadius(12, corners: [.topLeft, .topRight, .bottomRight]) .background( // 捕获当前消息的实际宽度,限制最大300 GeometryReader { proxy in Color.clear.preference( key: MessageWidthPreferenceKey.self, value: min(proxy.size.width, 300) ) } ) } .onPreferenceChange(MessageWidthPreferenceKey.self) { newWidth in messageWidth = newWidth }
4. 调整外层容器的宽度限制
确保外层的HStack最大宽度限制保留,避免整体超出300:
// 原代码中的这个frame保留,确保整个气泡最大宽度300 .frame(maxWidth: 300, alignment: .leading)
这样修改后,回复消息的背景会自动延伸到当前消息的实际宽度,既不会因为内容过短而留白,也不会突破300的最大宽度限制,完全贴合 WhatsApp 的效果。
内容的提问来源于stack exchange,提问作者Nate
相关产品推荐
相关产品推荐

