簡體   English   中英

SwiftUI 聊天應用:反轉列表和上下文菜單的困境

[英]SwiftUI chat app: the woes of reversed List and Context Menu

我正在 SwiftUI 中構建一個聊天應用程序。要在聊天中顯示消息,我需要一個反向列表(在底部顯示最新條目並自動滾動到底部的列表)。 我通過翻轉列表及其每個條目( 標准方法)制作了一個反向列表。

現在我想在消息中添加上下文菜單。 但長按后,菜單顯示的消息被翻轉了。 我認為這是有道理的,因為它從列表中刪除了翻轉的消息。

關於如何讓它工作的任何想法?

在此處輸入圖像描述

import SwiftUI

struct TestView: View {
    var arr = ["1aaaaa","2bbbbb", "3ccccc", "4aaaaa","5bbbbb", "6ccccc", "7aaaaa","8bbbbb", "9ccccc", "10aaaaa","11bbbbb", "12ccccc"]

    var body: some View {
        List {
            ForEach(arr.reversed(), id: \.self) { item in
                VStack {
                    Text(item)
                        .height(100)
                        .scaleEffect(x: 1, y: -1, anchor: .center)
                }
                .contextMenu {
                    Button(action: { }) {
                        Text("Reply")
                    }
                }
            }
        }
        .scaleEffect(x: 1, y: -1, anchor: .center)

    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

翻轉的問題是您需要翻轉上下文菜單,而 SwiftUI 並沒有提供這么多的控制。

處理此問題的更好方法是訪問嵌入式 UITableView(您將擁有更多控制權)並且您無需添加額外的 hack。

在此處輸入圖像描述

這是演示代碼:

import SwiftUI
import UIKit
struct TestView: View {
    @State var arr = ["1aaaaa","2bbbbb", "3ccccc", "4aaaaa","5bbbbb", "6ccccc", "7aaaaa","8bbbbb", "9ccccc", "10aaaaa","11bbbbb", "12ccccc"]

    @State var tableView: UITableView? {
        didSet {
            self.tableView?.adaptToChatView()

            DispatchQueue.main.asyncAfter(deadline: .now()) {
                self.tableView?.scrollToBottom(animated: true)
            }
        }
    }

    var body: some View {
        NavigationView {
        List {
            UIKitView { (tableView) in
                DispatchQueue.main.async {
                    self.tableView = tableView
                }
            }
            ForEach(arr, id: \.self) { item in
                Text(item).contextMenu {
                    Button(action: {
                        // change country setting
                    }) {
                        Text("Choose Country")
                        Image(systemName: "globe")
                    }

                    Button(action: {
                        // enable geolocation
                    }) {
                        Text("Detect Location")
                        Image(systemName: "location.circle")
                    }
                }
            }
        }
        .navigationBarTitle(Text("Chat View"), displayMode: .inline)
            .navigationBarItems(trailing:
          Button("add chat") {
            self.arr.append("new Message: \(self.arr.count)")

            self.tableView?.adaptToChatView()

            DispatchQueue.main.async {
                self.tableView?.scrollToBottom(animated: true)
            }

          })

        }

    }
}


extension UITableView {
    func adaptToChatView() {
        let offset = self.contentSize.height - self.visibleSize.height
        if offset < self.contentOffset.y {
            self.tableHeaderView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: self.contentSize.width, height: self.contentOffset.y - offset))
        }
    }
}


extension UIScrollView {
    func scrollToBottom(animated:Bool) {
        let offset = self.contentSize.height - self.visibleSize.height
        if offset > self.contentOffset.y {
            self.setContentOffset(CGPoint(x: 0, y: offset), animated: animated)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}



final class UIKitView : UIViewRepresentable {
    let callback: (UITableView) -> Void //return TableView in CallBack

    init(leafViewCB: @escaping ((UITableView) -> Void)) {
      callback = leafViewCB
    }

    func makeUIView(context: Context) -> UIView  {
        let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
        y: CGFloat.leastNormalMagnitude,
        width: CGFloat.leastNormalMagnitude,
        height: CGFloat.leastNormalMagnitude))
        view.backgroundColor = .clear
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {


        if let tableView = uiView.next(UITableView.self) {
            callback(tableView) //return tableview if find
        }
    }
}
extension UIResponder {
    func next<T: UIResponder>(_ type: T.Type) -> T? {
        return next as? T ?? next?.next(type)
    }
}

您可以創建一個自定義模式進行回復,並通過長按列表的每個元素來顯示它,而不顯示contextMenu

@State var showYourCustomReplyModal = false
@GestureState var isDetectingLongPress = false
var longPress: some Gesture {
    LongPressGesture(minimumDuration: 0.5)
        .updating($isDetectingLongPress) { currentstate, gestureState,
                transaction in
            gestureState = currentstate
        }
        .onEnded { finished in
            self.showYourCustomReplyModal = true
        }
}

像這樣應用它:

        ForEach(arr, id: \.self) { item in
            VStack {
                Text(item)
                    .height(100)
                    .scaleEffect(x: 1, y: -1, anchor: .center)
            }.gesture(self.longPress)
        }

如果我理解正確,您為什么不在 for each 循環或之前訂購您的數組。 那么您根本不必使用任何 scaleEffect 。 稍后,如果您收到消息 object,您可能已為其分配了一個日期,因此您可以按日期訂購。 在上述情況下,您可以使用:

ForEach(arr.reverse(), id: \.self) { item in

...
}

它將在頂部打印12ccccc作為第一條消息,並將1aaaaa作為最后一條消息。

如果有人在 UIKit 中搜索解決方案:而不是cell ,您應該使用contentViewcontentView的子視圖作為UITargetedPreview的參數。 像這樣:

extension CustomScreen: UITableViewDelegate {
    func tableView(_ tableView: UITableView,
                   contextMenuConfigurationForRowAt indexPath: IndexPath,
                   point: CGPoint) -> UIContextMenuConfiguration? {
        UIContextMenuConfiguration(identifier: indexPath as NSCopying,
                                   previewProvider: nil) { _ in
            // ...
            return UIMenu(title: "", children: [/* actions */])
        }
    }

    func tableView(
        _ tableView: UITableView,
        previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
    ) -> UITargetedPreview? {
        getTargetedPreview(for: configuration.identifier as? IndexPath)
    }

    func tableView(
        _ tableView: UITableView,
        previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
    ) -> UITargetedPreview? {
        getTargetedPreview(for: configuration.identifier as? IndexPath)
    }
}


extension CustomScreen {
    private func getTargetedPreview(for indexPath: IndexPath?) -> UITargetedPreview? {
        guard let indexPath = indexPath,
              let cell = tableView.cellForRow(at: indexPath) as? CustomTableViewCell else { return nil }

        return UITargetedPreview(view: cell.contentView,
                                 parameters: UIPreviewParameters().then { $0.backgroundColor = .clear })
    }
}

截至iOS 14,SwiftUI有ScrollViewReader,可用於position的滾動。 GeometryReader 連同 minHeight 和 Spacer() 可以創建一個使用全屏同時從底部開始顯示消息的 VStack。 項目以通常的先進先出順序從數組中讀取和附加到數組中。

在此處輸入圖像描述

SwiftUI 示例:

struct ContentView: View {
    @State var items: [Item] = []
    @State var text: String = ""
    @State var targetItem: Item?
    
    var body: some View {
        VStack {
            ScrollViewReader { scrollView in
                ChatStyleScrollView() {
                    ForEach(items) { item in
                        ItemView(item: item)
                        .id(item.id)
                    }
                }
                .onChange(of: targetItem) { item in
                    if let item = item {
                        withAnimation(.default) {
                            scrollView.scrollTo(item.id)
                        }
                    }
                }
                TextEntryView(items: $items, text: $text, targetItem: $targetItem)
            }
        }
    }
}

//MARK: - Item Model with unique identifier
struct Item: Codable, Hashable, Identifiable {
    var id: UUID
    var text: String
}

//MARK: - ScrollView that pushes text to the bottom of the display
struct ChatStyleScrollView<Content: View>: View {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        GeometryReader { proxy in
            ScrollView(.vertical, showsIndicators: false) {
                VStack {
                    Spacer()
                    content
                }
                .frame(minHeight: proxy.size.height)
            }
        }
    }
}

//MARK: - A single item and its layout
struct ItemView: View {
    var item: Item
    
    var body: some View {
        HStack {
            Text(item.text)
                .frame(height: 100)
                .contextMenu {
                    Button(action: { }) {
                        Text("Reply")
                    }
                }
            Spacer()
        }
    }
}

//MARK: - TextField and Send button used to input new items
struct TextEntryView: View {
    @Binding var items: [Item]
    @Binding var text: String
    @Binding var targetItem: Item?

    var body: some View {
        HStack {
            TextField("Item", text: $text)
                .frame(height: 44)
            Button(action: send) { Text("Send") }
        }
        .padding(.horizontal)
    }
    
    func send() {
        guard !text.isEmpty else { return }
        let item = Item(id: UUID(), text: text)
        items.append(item)
        text = ""
        targetItem = item
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM