我有一个表格视图,其中标题作为两个报价的标题。
我有一个搜索栏可以从表格视图中搜索报价并加载到表格行中。我想将这两个优惠分为两个部分显示,即第 1 节(包含标准路线优惠)和第 2(包含附近优惠)。问题是我只得到一个带有标题的部分作为标准优惠或附近的优惠,其中一个正在表格视图中搜索。我如何获得两个带有标准优惠和附近优惠标题的部分。这是我的代码。两个报价数组为:
struct AlternativeRoutesResponse: Codable {
let nearbyRoutes: [AlternativeRouteOffer]
let standardRoutes: [AlternativeRouteOffer]
var nearbyRoutesSection: SearchSection? {
nearbyRoutes.isEmpty ? nil : SearchSection(title: L10n.Search.nearbyOffers, offers: nearbyRoutes.map({RouteOffer(alternativeOffer: $0)}))
}
var standardRoutesSection: SearchSection? {
standardRoutes.isEmpty ? nil : SearchSection(title: L10n.Search.standardOffers, offers: standardRoutes.map({RouteOffer(alternativeOffer: $0, isStandard: true)}))
}
}
提供参数:
struct AlternativeRouteOffer: Codable {
struct LocationOffer: Codable {
let name: String
let thingId: Int
}
let departure: LocationOffer
let destination: LocationOffer
}
在搜索模型中:
struct OffersResponse {
let status: Bool
let sections: [SearchSection]
let title: String
}
enum SearchOffersDetailsEvent: Event {
case nothingFoundAt(date: Date, offer: RouteOffer)
}
enum SearchOffersEvent: Event {
case setAlert(departure: AddressLocation?, destination: AddressLocation?)
}
enum SearchOffersModelAction {
case exchangeLocations
case setAlert
}
protocol SearchOffersModelInput: AnyObject {
var numberOfSections: Int { get }
var isGuest: Bool { get }
func send(action: SearchOffersModelAction)
func load()
func itemsIn(section: Int) -> Int
func configureCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell
func didSelect(indexPath: IndexPath)
func headerIn(section: Int, for tableView: UITableView) -> UITableViewHeaderFooterView?
func heightForHeader(in section: Int) -> CGFloat
}
protocol SearchOffersModelOutput: AnyObject {
func didLoad()
func pop()
}
final class SearchOffersModel: TextFieldEventNode {
enum StateResult {
case isFound, noResults, empty
var headerItemsCount: Int {
switch self {
case .empty: return 2
case .noResults: return 3
default: return 2
}
}
func cellReuseIdentifierFor(row: Int) -> String {
switch self {
default:
switch row {
case 0: return SearchRoutesHeaderTableViewCell.className
case 1: return SearchResultInfoTableViewCell.className
case 2: return SearchPlaceholderTableViewCell.className
default: return ""
}
}
}
}
enum SearchTableSection: Int {
case searchHeader = 0
case results = 1
}
weak var output: SearchOffersModelOutput!
private let offersProvider = DataManager<RoutesAPI, SearchRoutesResponse>()
private let alternativeOffersProvider = DataManager<RoutesAPI, AlternativeRoutesResponse>()
private let routesDetailsProvider = DataManager<RoutesAPI, [OfferDetails]>()
private let alternativeRoutesDetailsProvider = DataManager<RoutesAPI, [OfferDetails]>()
private let searchData: SearchRoutesData
private var sections: [SearchSection] {
[data?.searchSection, alternativeData?.nearbyRoutesSection, alternativeData?.standardRoutesSection].compactMap({$0})
}
private var state = StateResult.empty
private var data: SearchRoutesResponse?
private var alternativeData: AlternativeRoutesResponse?
init(parent: EventNode?, data: SearchRoutesData) {
searchData = data
super.init(parent: parent)
addHandler(.onPropagate) {[weak self] (event: SearchOffersDetailsEvent) in
switch event {
case .nothingFoundAt(let date, let offer):
self?.searchData.date = date
self?.searchData.departure = offer.departure
self?.searchData.destination = offer.destination
self?.data = nil
self?.state = .noResults
self?.output.pop()
self?.output.didLoad()
self?.load()
}
}
}
override func didChange(_ textField: TextField) {
switch textField.fieldType {
case .departureCity:
searchData.departure = textField.addressLocation
updateResults()
case .destinationCity:
searchData.destination = textField.addressLocation
updateResults()
case .selectDate:
searchData.date = textField.date
updateResults()
default: break
}
}
func updateResults() {
alternativeData = nil
guard searchData.isEmpty == false, searchData.departure != nil || searchData.destination != nil else {
state = .empty
data = nil
output.didLoad()
return
}
offersProvider.load(target: .offers(parameters: searchData.apiParameters), withActivity: true) {[weak self] result in
switch result {
case .success(let response):
self?.didRecive(response)
case .failure(let error):
print(error.localizedDescription)
}
}
}
private func didRecive(_ response: SearchRoutesResponse) {
data = response
if response.isFound {
alternativeData = nil
state = .isFound
if let o = response.section.offers.first {
loadAlternatives()
load(route: o)
}
} else {
if searchData.isEmpty {
state = .empty
} else if response.section.offers.isEmpty {
loadAlternatives()
} else {
state = .isFound
}
}
output.didLoad()
}
private func loadAlternatives() {
var target: RoutesAPI!
if let departure = searchData.departure?.id, let destination = searchData.destination?.id {
target = .alternative(departure: departure, destination: destination, date: searchData.date)
} else if let departure = searchData.departure?.id {
target = .alternativeDeparture(departure: departure, date: searchData.date)
} else if let destination = searchData.destination?.id {
target = .alternativeDestination(destination: destination, date: searchData.date)
} else {
state = .noResults
output.didLoad()
return
}
alternativeOffersProvider.load(target: target, withActivity: true) {[weak self] result in
switch result {
case .success(let response):
self?.didReciveAlternative(response)
case .failure(let error):
print(error)
}
self?.output.didLoad()
}
}
private func didReciveAlternative(_ response: AlternativeRoutesResponse) {
alternativeData = response
if sections.isEmpty {
state = .noResults
} else {
state = .isFound
}
}
}
extension SearchOffersModel: SearchOffersViewControllerOutput {
var isGuest: Bool {
isAuthorized == false
}
func send(action: SearchOffersModelAction) {
switch action {
case .exchangeLocations:
let departure = searchData.departure
searchData.departure = searchData.destination
searchData.destination = departure
data = nil
alternativeData = nil
output.didLoad()
updateResults()
case .setAlert:
if isAuthorized {
raise(event: SearchOffersEvent.setAlert(departure: searchData.departure, destination: searchData.destination))
} else {
raise(event: UserSessionEvent.setSession(nil))
}
}
}
var searchDataDisplayable: SearchRoutesData {
searchData
}
private var topTitleText: String {
switch state {
case .noResults:
return ""
default:
return searchData.isEmpty ? L10n.Search.emptySearch : data?.title ?? ""
}
}
private var emptyPlaceholderText: String {
if searchData.departure != nil && searchData.destination == nil {
if isAuthorized {
return L10n.Home.enterDestinationCity
} else {
return L10n.Home.guestEnterDestinationCity
}
} else if searchData.destination != nil && searchData.departure == nil {
if isAuthorized {
return L10n.Home.enterDepartureCity
} else {
return L10n.Home.guestEnterDepartureCity
}
} else {
return L10n.Home.noResults
}
}
func load() {
updateResults()
}
func configureCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case SearchTableSection.searchHeader.rawValue:
let cell = tableView.dequeueReusableCell(withIdentifier: state.cellReuseIdentifierFor(row: indexPath.row))
switch cell {
case cell as? SearchRoutesHeaderTableViewCell:
let c = cell as? SearchRoutesHeaderTableViewCell
apply([c?.departureTextField, c?.destinationTextField, c?.dateTextField].compactMap({$0}))
c?.apply(searchData: searchData)
return c!
case cell as? SearchResultInfoTableViewCell:
(cell as? SearchResultInfoTableViewCell)?.apply(isFound: data?.isFound ?? false, text: topTitleText)
return cell!
case cell as? SearchPlaceholderTableViewCell:
(cell as? SearchPlaceholderTableViewCell)?.apply(text: emptyPlaceholderText)
return cell!
default:
return cell!
}
default:
let cell = tableView.dequeueReusableCell(withIdentifier: SearchItemTableViewCell.className) as? SearchItemTableViewCell
if !sections.isEmpty {
cell?.apply(DisplayableRouteOffer(offer: sections[indexPath.section - 1].offers[indexPath.row]))}
return cell!
}
}
var numberOfSections: Int {
sections.count + 1
}
func itemsIn(section: Int) -> Int {
switch section {
case SearchTableSection.searchHeader.rawValue: return state.headerItemsCount
default: return sections[section - 1].offers.count
}
}
func headerIn(section: Int, for tableView: UITableView) -> UITableViewHeaderFooterView? {
let h = tableView.dequeueReusableHeaderFooterView(withIdentifier: SearchSectionHeaderTableViewCell.className)
if !sections.isEmpty{
(h as? SearchSectionHeaderTableViewCell)?.apply(title: sections[section - 1].title)
}
return h
}
func heightForHeader(in section: Int) -> CGFloat {
return section == 0 ? 0.0 : 20.0
}
func didSelect(indexPath: IndexPath) {
guard indexPath.section > 0 else { return }
let section = sections[indexPath.section - 1]
var offer = section.offers[indexPath.row]
offer.selectedDate = searchData.date
if offer.isStandard {
loadStandard(route: offer)
} else {
load(route: offer)
}
}
private func loadStandard(route: RouteOffer) {
routesDetailsProvider.load(target: .standardRouteDetails(departure: route.departure.id, destination: route.destination.id, date: searchData.date), withActivity: true) {[weak self] result in
switch result {
case .success(let response):
self?.raise(event: PopularRoutesEvent.openDetails(details: response, offer: route))
case .failure(let error):
print(error)
}
}
}
private func load(route: RouteOffer) {
routesDetailsProvider.load(target: .routeDetails(departure: route.departure.id, destination: route.destination.id, date: searchData.date), withActivity: true) {[weak self] result in
switch result {
case .success(let response):
self?.raise(event: PopularRoutesEvent.openDetails(details: response, offer: route))
case .failure(let error):
print(error)
}
}
}
}
The default offers are as when no offer found it should load default offers
struct SearchRoutesResponse: Codable {
var title: String
var isFound: Bool
var section: SearchSection
var searchSection: SearchSection? {
section.offers.isEmpty ? nil : SearchSection(title: L10n.Search.defaultOffers, offers: section.offers)
}
}
struct SearchSection: Codable {
let title: String
let offers: [RouteOffer]
}
// From Route Offer it should decode data from api as
struct DisplayableRouteOffer {
var fromName: String
var toName: String
var description: String
init(offer: RouteOffer) {
fromName = offer.fromName ?? ""
toName = offer.toName ?? ""
description = "\(offer.distance ?? 0) km, ca. \(offer.amountOfTime ?? "")"
}
}
struct RouteOffer: Codable, Equatable {
struct Location: Codable, Equatable {
let id: Int
}
// Table view method is as here called in viewcontroller
extension SearchOffersViewController: SearchOffersViewControllerInput {
func didLoad() {
tableView.reloadData()
}
func pop() {
navigationController?.popToViewController(self, animated: true)
}
}
extension SearchOffersViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
model.numberOfSections
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
model.itemsIn(section: section)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
model.configureCell(for: tableView, at: indexPath)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
model.headerIn(section: section, for: tableView)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
model.heightForHeader(in: section)
}
}
extension SearchOffersViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
model.didSelect(indexPath: indexPath)
}
}
简单的解决方案是将 numberOfSections 强制为 2。
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0{
return standardRoutesSection.count
}else{
return nearbyRoutesSection.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if section == 0{
let data = standardRoutesSection[index.row]
return UITableViewCell()
}else{
let data = nearbyRoutesSection[index.row]
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0{
//standar header
}else{
//nearby header
}
return nil
}