Search code examples
iosswiftnetwork-programmingcombineurlsession

Swift - How do I decode json from a REST API


I am trying to make a GET from a REST API in swift. When I use the print statement (print(clubs)) I see the expected response in the proper format. But in the VC is gives me an empty array.

Here is the code to talk to the API

extension ClubAPI {

    public enum ClubError: Error {
        case unknown(message: String)
    }

    func getClubs(completion: @escaping ((Result<[Club], ClubError>) -> Void)) {
        let baseURL = self.configuration.baseURL
        let endPoint = baseURL.appendingPathComponent("/club")
        print(endPoint)
        API.shared.httpClient.get(endPoint) { (result) in
            switch result {
            case .success(let response):
                let clubs = (try? JSONDecoder().decode([Club].self, from: response.data)) ?? []
                print(clubs)
                completion(.success(clubs))
            case .failure(let error):
                completion(.failure(.unknown(message: error.localizedDescription)))
            }
        }
    }

}

and here is the code in the VC

private class ClubViewModel {
    @Published private(set) var clubs = [Club]()
    @Published private(set) var error: String?

    func refresh() {
        ClubAPI.shared.getClubs { (result) in
            switch result {
            case .success(let club):
                print("We have \(club.count)")
                self.clubs = club
                print("we have \(club.count)")
            case .failure(let error):
                self.error = error.localizedDescription
            }
        }
    }
}

and here is the view controller code (Before the extension)

class ClubViewController: UIViewController {
    private var clubs = [Club]()
    private var subscriptions = Set<AnyCancellable>()
    private lazy var dataSource = makeDataSource()

    enum Section {
        case main
    }
    private var errorMessage: String? {
        didSet {

        }
    }
    private let viewModel = ClubViewModel()
    @IBOutlet private weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.subscriptions = [
            self.viewModel.$clubs.assign(to: \.clubs, on: self),
            self.viewModel.$error.assign(to: \.errorMessage, on: self)
        ]

        applySnapshot(animatingDifferences: false)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.viewModel.refresh()
    }



}




extension ClubViewController {
    typealias DataSource = UITableViewDiffableDataSource<Section, Club>
    typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Club>
    func applySnapshot(animatingDifferences: Bool = true) {
        // Create a snapshot object.
        var snapshot = Snapshot()
        // Add the section
        snapshot.appendSections([.main])
        // Add the player array
        snapshot.appendItems(clubs)
        print(clubs.count)

        // Tell the dataSource about the latest snapshot so it can update and animate.
        dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
    }

    func makeDataSource() -> DataSource {
        let dataSource = DataSource(tableView: tableView) { (tableView, indexPath, club) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: "ClubCell", for: indexPath)
            let club = self.clubs[indexPath.row]
            print("The name is \(club.name)")
            cell.textLabel?.text = club.name
            return cell
        }
        return dataSource
    }
}

Solution

  • You need to apply a new snapshot to your table view once you have fetched the clubs. Your current subscriber simply assigns a value to clubs and nothing more.

    You can use a sink subscriber to assign the new clubs value and then call applySnapshot. You need to ensure that this happens on the main queue, so you can use receive(on:).

    self.subscriptions = [
        self.viewModel.$clubs.receive(on: RunLoop.main).sink { clubs in
            self.clubs = clubs
            self.applySnapshot()
        },
        self.viewModel.$error.assign(to: \.errorMessage, on: self)
    ]