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
相关产品推荐
相关产品推荐

