4

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

Cezar
  • 55,636
  • 19
  • 86
  • 87
smeshko
  • 1,184
  • 13
  • 27
  • 2
    It is hardly possible to say something without debugging. Would you provide access to project? – Asperi Nov 10 '20 at 04:54
  • 1
    I added a link to the github repo and some explanations. – smeshko Nov 10 '20 at 06:16
  • I wanted to help you solve this, but I see you've updated the project to iOS 15 which is still in beta. So I can't compile and run it, since the third-party packages are running beta software as well. Surely you can post a version that uses released software, since you reported this last November...? If you can provide me with the version of QuickWorkoutsList you were using last november I can build, run and debug the project. – Mozahler Aug 14 '21 at 22:20
  • CoreLogic, CorePersistance, DomainEntities also refuse to build. QuickWorkoutForm, RunningTimer and WorkoutSettings won't build as a result of the previous errors. – Mozahler Aug 14 '21 at 22:27

0 Answers0