`UIViewControllerRepresentable` table view disappears after long tap

Viewed 254

I have a UIViewControllerRepresentable wrapper for UITableViewController and am using swift composable architecture, which is probably irrelevant to the issue.

Here's my table view wrapper code, including the context menu code (I have omitted quite a lot of setup code):

public struct List<EachState, EachAction, RowContent, RowPreview, Destination, Data, ID>: UIViewControllerRepresentable, KeyPathUpdateable
    where Data: Collection, RowContent: View, RowPreview: View, Destination: View, EachState: Identifiable, EachState.ID == ID {

    private var actionProvider: (IndexSet) -> UIMenu? = { _ in nil }
    private var previewProvider: (Store<EachState, EachAction>) -> RowPreview? = { _ in nil }

    // setup code

    public func makeUIViewController(context: Context) -> UITableViewController {
        let tableViewController = UITableViewController()
        tableViewController.tableView.translatesAutoresizingMaskIntoConstraints = false
        tableViewController.tableView.dataSource = context.coordinator
        tableViewController.tableView.delegate = context.coordinator
        tableViewController.tableView.separatorStyle = .none
        tableViewController.tableView.register(HostingCell<RowContent>.self, forCellReuseIdentifier: "Cell")
        return tableViewController
    }

    public func updateUIViewController(_ controller: UITableViewController, context: Context) {
        context.coordinator.rows = data.enumerated().map { offset, item in
            store.scope(state: { $0[safe: offset] ?? item },
                        action: { (item.id, $0) })
        }
        controller.tableView.reloadData()
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator(rows: [],
                    content: content,
                    onDelete: onDelete,
                    actionProvider: actionProvider,
                    previewProvider: previewProvider,
                    destination: destination)
    }

    public func previewProvider(_ provider: @escaping (Store<EachState, EachAction>) -> RowPreview?) -> Self {
        update(\.previewProvider, value: provider)
    }

    public func destination(_ provider: @escaping (Store<EachState, EachAction>) -> Destination?) -> Self {
        update(\.destination, value: provider)
    }

    public class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {

        fileprivate var rows: [Store<EachState, EachAction>]
        private var content: (Store<EachState, EachAction>) -> RowContent
        private var actionProvider: (IndexSet) -> UIMenu?
        private var previewProvider: (Store<EachState, EachAction>) -> RowPreview?

        public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            rows.count
        }

        public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

            guard let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? HostingCell<RowContent>,
                  let view = rows[safe: indexPath.row] else {
                return UITableViewCell()
            }

            tableViewCell.setup(with: content(view))

            return tableViewCell
        }

        public func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                onDelete(IndexSet(integer: indexPath.item))
            }
        }

        public func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
            guard let store = rows[safe: indexPath.row] else { return nil }

            return UIContextMenuConfiguration(
                identifier: nil,
                previewProvider: {
                    guard let preview = self.previewProvider(store) else { return nil }
                    let hosting = UIHostingController<RowPreview>(rootView: preview)
                    return hosting
                },
                actionProvider: { _ in
                    self.actionProvider(IndexSet(integer: indexPath.item))
            })
        }
    }
}
private class HostingCell<Content: View>: UITableViewCell {
    var host: UIHostingController<Content>?

    func setup(with view: Content) {
        if host == nil {
            let controller = UIHostingController(rootView: view)
            host = controller

            guard let content = controller.view else { return }
            content.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(content)

            content.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
            content.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
            content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
            content.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
        } else {
            host?.rootView = view
        }
        setNeedsLayout()
    }
}

And here's an example usage:

private struct ClassView: View {
    let store = Store<ClassState, ClassAction>(
        initialState: ClassState(),
        reducer: classReducer,
        environment: ClassEnv()
    )

    var body: some View {
        WithViewStore(store) { viewStore in
            CoreInterface.List(store.scope(state: \.people, action: ClassAction.personAction)) { store in
                PersonView(store: store)
            }
            .actionProvider { indices in
                let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in
                    viewStore.send(.remove(indices))
                }

                return UIMenu(title: "", children: [delete])
            }
            .previewProvider { viewStore in
                Text("preview")
            }
        }
    }
}

The issue is as follows: when I long tap on a cell to show the context menu, then dismiss it and scroll up, the table view disappears. This only happens when it's inside a NavigationView. Here is a short video of the issue.

The project is on github. The table view wrapper is in InternalFrameworks/Core/CoreInterface/Views/List, usage is in InternalFrameworks/Screens/QuickWorkoutsList/Source/QuickWorkoutsList. In order to run the project, you'll need xcodegen. Run

brew install xcodegen xcodegen generate

0 Answers
Related