You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift结合Combine网络请求:UITableView无法填充数据问题

Combine框架下UITableView无法填充数据

调用ViewController中的viewModel.request()后,UITableView始终无法加载数据。单独使用URLSession实现时功能正常,但结合Combine框架就出现问题,viewDidLoad()中打印viewModel.sections结果为空数组。


ViewController代码

class ViewController: UIViewController {

    var viewModel = TestViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //TableView Setup
        setUpTableView()
        
        viewModel.request()
        self.tableView.reloadData()

    }
    func numberOfSections(in tableView: UITableView) -> Int {
         return viewModel.sections.count
     }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.sections[section].sports.count
    }

}

TestViewModel代码

class TestViewModel {
    
    @Published var sections: [Sections] = []
    var date: Date = Date()
    var cancellables: AnyCancellable?

    func request(){

        let nfl = NetworkManager.download(endpoint: .nfl, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        let nba = NetworkManager.download(endpoint: .nba, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        let mlb = NetworkManager.download(endpoint: .mlb, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        let nhl = NetworkManager.download(endpoint: .nhl, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        self.cancellables = Publishers.Zip4(nfl, nba, mlb, nhl)
        .eraseToAnyPublisher()
        .sink(receiveCompletion: { (completion) in
            switch completion {
            case .finished:
                break
            case .failure(let error):
                print("Error:",error.localizedDescription)
            }
        }, receiveValue: { [weak self] (nfl, nba, mlb, nhl) in
          
            var section: [Sections] = []

            if !nfl.events.isEmpty {
                section.append(.init(icon: "football.fill", title: "NFL", sports: nfl.events, leagues: nfl.leagues))
            }
            if !nba.events.isEmpty {
                section.append(.init(icon: "basketball.fill", title: "NBA", sports: nba.events, leagues: nba.leagues))
            }
            if !mlb.events.isEmpty {
                section.append(.init(icon: "baseball.fill", title: "MLB", sports: mlb.events, leagues: mlb.leagues))
            }
            if !nhl.events.isEmpty {
                section.append(.init(icon: "hockey.puck.fill", title: "NHL", sports: nhl.events, leagues: nhl.leagues))
            }
            
            self?.sections = section
            
        })
        // 此处打印为空是正常的,因为请求还未完成
        print(self.sections)
    }
}

NetworkManager代码

class NetworkManager {
    
    static func download(endpoint: Endpoint, date: String) -> AnyPublisher<Data, Error> {
        let url = URL(string: "***API URL***")!
        let final = url.appendingPathComponent(endpoint.build())
        
        var components = URLComponents(url: final, resolvingAgainstBaseURL: true)!
        components.queryItems = [
            URLQueryItem(name: "dates", value: date),
        ]
        let request = URLRequest(url: components.url!)
        return
            URLSession.shared.dataTaskPublisher(for: request)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .tryMap{(data, response) -> Data in
                guard let response = response as? HTTPURLResponse,
                      response.statusCode >= 200 && response.statusCode < 300 else {
                    throw URLError(.badServerResponse)
                }
                return data
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

解决方案

1. 修复异步请求的刷新时机

网络请求是异步操作,viewModel.request()调用后立刻执行tableView.reloadData()时,请求还未完成,sections仍为空数组。需要监听sections的变化来触发表格刷新。

2. 订阅@Published属性的更新

ViewModel中的sections是@Published属性,ViewController需要订阅这个发布者,当数据更新时自动刷新表格。

修改后的代码

ViewController修改

class ViewController: UIViewController {

    var viewModel = TestViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpTableView()
        
        // 订阅sections的变化,数据更新时刷新表格
        viewModel.$sections
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.tableView.reloadData()
            }
            .store(in: &cancellables)
        
        viewModel.request()
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
         return viewModel.sections.count
     }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.sections[section].sports.count
    }
}

TestViewModel修改

将单个AnyCancellable?改为Set<AnyCancellable>,以支持存储多个订阅:

class TestViewModel {
    
    @Published var sections: [Sections] = []
    var date: Date = Date()
    var cancellables = Set<AnyCancellable>()

    func request(){

        let nfl = NetworkManager.download(endpoint: .nfl, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        let nba = NetworkManager.download(endpoint: .nba, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        let mlb = NetworkManager.download(endpoint: .mlb, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        let nhl = NetworkManager.download(endpoint: .nhl, date: date.query())
        .decode(type: EventModel.self, decoder: JSONDecoder())
        
        Publishers.Zip4(nfl, nba, mlb, nhl)
        .eraseToAnyPublisher()
        .sink(receiveCompletion: { (completion) in
            switch completion {
            case .finished:
                break
            case .failure(let error):
                print("Error:",error.localizedDescription)
            }
        }, receiveValue: { [weak self] (nfl, nba, mlb, nhl) in
          
            var section: [Sections] = []

            if !nfl.events.isEmpty {
                section.append(.init(icon: "football.fill", title: "NFL", sports: nfl.events, leagues: nfl.leagues))
            }
            if !nba.events.isEmpty {
                section.append(.init(icon: "basketball.fill", title: "NBA", sports: nba.events, leagues: nba.leagues))
            }
            if !mlb.events.isEmpty {
                section.append(.init(icon: "baseball.fill", title: "MLB", sports: mlb.events, leagues: mlb.leagues))
            }
            if !nhl.events.isEmpty {
                section.append(.init(icon: "hockey.puck.fill", title: "NHL", sports: nhl.events, leagues: nhl.leagues))
            }
            
            self?.sections = section
            
        })
        .store(in: &cancellables)
        
        print(self.sections)
    }
}

额外注意事项

  • 确保EventModel和Sections的结构与API返回的JSON格式匹配,否则解码失败会进入failure分支,导致sections无法被赋值。
  • Zip4会等待四个请求全部完成才会触发receiveValue,如果其中一个请求失败,整个流会进入失败状态,可根据业务需求考虑用CombineLatest4替代。

内容的提问来源于stack exchange,提问作者GILL

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 00:10:14