Thursday 12 November 2020

`UIViewControllerRepresentable ` table view disappears after long tap

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



from `UIViewControllerRepresentable ` table view disappears after long tap

No comments:

Post a Comment