Мне нужно реализовать функцию чата с такими же взаимодействиями, как в iMessages. Мы решили использовать SwiftUI, потому что он должен быть быстрее. Но теперь я застрял при реализации представления реакций. ContextMenu прост, но когда я хочу, чтобы представление реакций, которое является параметром в предварительном просмотре contextMenu, работало, оно просто закрывает весь contextMenu, не запуская действие.
MessageView(
isMyMessage: element.sender.id == currentUserID,
senderAvatar: Image(.testUser),
messageType: element.kind,
previousMessageFromSameSender: messages[previousIndex].sender.id == element.sender.id
).contextMenu {
Button(),
Button(),
Button()
} preview: {
VSStack {
reactionsView // this doesn't interact but it should
messageView // again the same chat bubble
}
}
Ожидаемый результат
@BenzyNeez, но тогда ты не сможешь добавить размытие фона :/





Как я предлагал в комментарии, способом реализации этого было бы настраиваемое всплывающее окно. Ответ на вопрос iOS SwiftUI Необходимость отображения всплывающего окна без «стрелки» показывает, как .matchedGeometryEffect можно использовать для позиционирования всплывающего окна (это был мой ответ).
Вы упомянули, что вам также нужен эффект размытия при отображении всплывающего окна. Это возможно, если сделать его зависимым от видимости всплывающего окна. Вы также можете добавить полупрозрачный черный слой, чтобы обеспечить эффект затемнения. К этому слою может быть прикреплен жест касания, чтобы всплывающее окно очищалось при касании в любом месте фона.
Вот пример, иллюстрирующий его работу:
struct Message: Identifiable, Equatable {
let id = UUID()
let text: String
}
struct MessageView: View {
let message: Message
var body: some View {
Text(message.text)
.padding(10)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(.background)
}
}
}
struct EmojiButton: View {
let emoji: Character
@State private var animate = false
var body: some View {
Text(String(emoji))
.font(.largeTitle)
.scaleEffect(animate ? 1.3 : 1)
.onTapGesture {
print("\(emoji) tapped")
withAnimation(.bouncy(duration: 0.2, extraBounce: 0.7)) {
animate = true
} completion: {
withAnimation(.bouncy(duration: 0.05)) {
animate = false
}
}
}
}
}
struct Demo: View {
@State private var selectedMessage: Message?
@Namespace private var nsPopover
private let demoMessages: [Message] = [
Message(text: "Once upon a time"),
Message(text: "the quick brown fox jumps over the lazy dog"),
Message(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
Message(text: "and they all lived happily ever after.")
]
private var reactionsView: some View {
HStack {
ForEach(Array("👍👎😄🔥💕⚠️❓"), id: \.self) { char in
EmojiButton(emoji: char)
}
}
.padding(10)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(.bar)
}
}
@ViewBuilder
private var messageView: some View {
if let selectedMessage {
MessageView(message: selectedMessage)
.allowsHitTesting(false)
}
}
private func optionLabel(label: String, imageName: String) -> some View {
HStack(spacing: 0) {
Text(label)
Spacer()
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
}
.padding(.vertical, 10)
.padding(.horizontal)
.contentShape(Rectangle())
}
private var optionsMenu: some View {
VStack(alignment: .leading, spacing: 0) {
Button {
print("Reply tapped")
} label: {
optionLabel(label: "Reply", imageName: "arrowshape.turn.up.left.fill")
}
Divider()
Button {
print("Copy tapped")
} label: {
optionLabel(label: "Copy", imageName: "doc.on.doc.fill")
}
Divider()
Button {
print("Unsend tapped")
} label: {
optionLabel(label: "Unsend", imageName: "location.slash.circle.fill")
}
}
.buttonStyle(.plain)
.frame(width: 220)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(.bar)
}
}
private var customPopover: some View {
VStack(alignment: .leading) {
reactionsView
messageView
optionsMenu
}
.padding(.top, -70)
.padding(.trailing)
.padding(.trailing)
}
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: 100) {
ForEach(demoMessages) { message in
MessageView(message: message)
.matchedGeometryEffect(
id: message.id,
in: nsPopover,
anchor: .topLeading,
isSource: true
)
.onLongPressGesture {
selectedMessage = message
}
}
}
.blur(radius: selectedMessage == nil ? 0 : 5)
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
if let selectedMessage {
Color.black
.opacity(0.15)
.ignoresSafeArea()
.onTapGesture { self.selectedMessage = nil }
customPopover
.matchedGeometryEffect(
id: selectedMessage.id,
in: nsPopover,
properties: .position,
anchor: .topLeading,
isSource: false
)
.transition(
.opacity.combined(with: .scale)
.animation(.bouncy(duration: 0.25, extraBounce: 0.2))
)
}
}
.animation(.easeInOut(duration: 0.25), value: selectedMessage)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(red: 0.31, green: 0.15, blue: 0.78))
}
}

Другой способ добиться эффекта размытия — использовать слой Material на заднем плане. Я тоже попробовал это, но даже с .ultraThinMaterial размытие оказалось очень сильным.
Одна из проблем, с которой вы можете столкнуться, заключается в том, что всплывающее окно имеет большой размер, поэтому, когда оно отображается поверх сообщения, расположенного вверху или внизу экрана, часть содержимого всплывающего окна может оказаться за пределами экрана. Однако я ожидаю, что сообщения можно прокручивать, поэтому пользователю просто нужно будет переместить сообщение сверху или снизу. Для первого и последнего сообщения в списке вы можете добавить дополнительные поля, чтобы освободить место для отображения всплывающего окна. Конечно, если у вас есть другой контент вверху и внизу экрана (например, элементы управления навигацией), это тоже поможет освободить место.
Вот странная ошибка: если вы замените VStack (тот, который содержит сообщения) списком, то при анимации «свертывания» всплывающее окно исчезнет, а не исчезнет. На самом деле, если присмотреться, он отодвигается за список (и все еще анимируется). Каким-то образом List причиняет вред... решение: поместите всплывающее окно в наложение.
@Colin Да, у вас нет особого контроля над анимацией, когда контент добавляется или удаляется из List, в том числе когда разделы разворачиваются/сворачиваются — см. также Неработающая анимация при использовании пользовательской группы DisclosureGroup в списке SwiftUI. Однако если у вас возникли проблемы, когда содержимое List не изменилось, вы можете создать для него новую публикацию.
Вместо этого вы всегда можете использовать собственный всплывающий элемент, тогда вы сможете показывать любой контент, который вам нравится, и он также может быть интерактивным. Ответ на вопрос iOS SwiftUI Необходимо отображать всплывающее окно без «стрелки» показывает способ сделать это.