How to connect SwiftUI state with UITable?

I use @Binding to sync data between SwiftUI component state and UITable state.

my observations:

  • while reordering @State variable of the TestView is updating, it can be checked by taping the button
  • inside the table the data is not updating. 'log2' is always the same on reorder and which is more important the app crashes when I try to remove item.

What to I missing?

import SwiftUI
import UIKit

struct TestView: View {
    @State var items = ["item 1", "item 2", "item 3", "item 4", "item 5"]
    
    var body: some View {
        VStack {
            Button("Print Items") { print("log1", items) }
            ReorderableListView(
                items: $items
            ) { item in
                Text(item)
            }
        }
    }
}

private struct ReorderableListView<Content: View>: UIViewControllerRepresentable {
    @Binding var items: [String]
    let content: (String) -> Content

    class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
        var parent: ReorderableListView

        init(parent: ReorderableListView) {
            self.parent = parent
        }

        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            print("log2", parent.items)
            return parent.items.count
        }

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            let hostingController = UIHostingController(rootView: parent.content(parent.items[indexPath.row]))
            hostingController.view.backgroundColor = .clear

            cell.contentView.subviews.forEach { $0.removeFromSuperview() }
            cell.contentView.addSubview(hostingController.view)
            cell.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
            if indexPath.row == self.parent.items.count - 1 {
                cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude)
            }
            hostingController.view.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                hostingController.view.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
                hostingController.view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
                hostingController.view.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
                hostingController.view.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)
            ])
            return cell
        }

        func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
            let movedObject = parent.items.remove(at: sourceIndexPath.row)
            parent.items.insert(movedObject, at: destinationIndexPath.row)
        }

        func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
            return true
        }

        func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                parent.items.remove(at: indexPath.row)
                tableView.deleteRows(at: [indexPath], with: .automatic)
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIViewController(context: Context) -> UITableViewController {
        let tableViewController = UITableViewController()
        tableViewController.tableView.dataSource = context.coordinator
        tableViewController.tableView.delegate = context.coordinator
        tableViewController.tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "Cell")
        tableViewController.tableView.isEditing = true
        return tableViewController
    }

    func updateUIViewController(_ uiViewController: UITableViewController, context: Context) {
        uiViewController.tableView.reloadData()
    }
}

private class CustomTableViewCell: UITableViewCell {
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        superview?.subviews.filter({ "\(type(of: $0))" == "UIShadowView" }).forEach { (sv: UIView) in
            sv.removeFromSuperview()
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


#Preview {
    TestView()
}

Hi,

Please see Use SwiftUI with UIKit and feel free to reply here with follow up questions.

Rico

WWDR - DTS - Software Engineer

How to connect SwiftUI state with UITable?
 
 
Q