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

Swift中如何用SpreadsheetView展示JSON数据并实现行点击跳转

问题描述

我正在使用SpreadsheetView组件,希望将从JSON接口获取的数据展示为网格形式,但不清楚如何将数据对应到各列;同时需要实现点击行内单元格时跳转到详情页展示相关数据。我已通过ConsultaModelo完成JSON数据的下载并存储到feedItems数组中,以下是我已编写的代码:

用于展示数据的视图

import UIKit
import SpreadsheetView

class TiendasViewController: UIViewController, SpreadsheetViewDataSource, SpreadsheetViewDelegate, ConsultaModeloProtocol{

let headers = ["Sucursal", "Venta total", "Tickets", "Piezas", "Pzs/Ticket", "Ticket prom.", "Utilidad", "Última venta"]
  
    var feedItems = [DetallesConsulta]()
    
    func itemConsulta(LaConsulta: [DetallesConsulta]) {
        feedItems = LaConsulta
        self.tablaTiendas.reloadData()
        
    }
    
    var selectDato : DetallesConsulta = DetallesConsulta()
    
    private let tablaTiendas = SpreadsheetView()
    
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        
        let consultaModelo = ConsultaModelo()
        consultaModelo.ElDelegado = self
        consultaModelo.downloadConsulta()
        
        tablaTiendas.dataSource = self
        tablaTiendas.delegate = self
        
        tablaTiendas.contentInset = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0)
        tablaTiendas.intercellSpacing = CGSize(width: 4, height: 1)
        tablaTiendas.gridStyle = .none
        
        
        tablaTiendas.gridStyle = .solid(width: 1, color: .blue)
        tablaTiendas.register(SucursalesCell.self, forCellWithReuseIdentifier: String(describing: SucursalesCell.self))
        tablaTiendas.register(DateCell.self, forCellWithReuseIdentifier: String(describing: DateCell.self))
        
        
        view.addSubview(tablaTiendas)
        print("Imprimiendo los feeditems: ", feedItems)
        // Do any additional setup after loading the view.
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tablaTiendas.frame = CGRect(x: 0, y:216, width: view.frame.size.width, height: view.frame.size.height-100)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        tablaTiendas.flashScrollIndicators()
    }
    
    func spreadsheetView(_ spreadsheetView: SpreadsheetView, cellForItemAt indexPath: IndexPath) -> Cell? {
        
        if case (0...(headers.count), 0) = (indexPath.column, indexPath.row) {
            
            let cell = spreadsheetView.dequeueReusableCell(withReuseIdentifier: String(describing: DateCell.self), for: indexPath) as! DateCell
            cell.label.text = headers[indexPath.column - 0]
            return cell
            
        } else if case(0, 1...(sucursales.count + 1)) = (indexPath.column, indexPath.row){
            
            let cell = spreadsheetView.dequeueReusableCell(withReuseIdentifier: String(describing: SucursalesCell.self), for: indexPath) as! SucursalesCell
            
            cell.label.text = sucursales[indexPath.row - 1]
            return cell
            
        }
       
       /*
        let cell = tablaTiendas.dequeueReusableCell(withReuseIdentifier: MyLabelCell.identifier, for: indexPath) as! MyLabelCell
        if indexPath.row == 0 {
            cell.setup(with: headers[indexPath.column])
            cell.backgroundColor = .systemBlue
        }
        return cell
        */
          
      return nil

    }
    

    func numberOfColumns(in spreadsheetView: SpreadsheetView) -> Int {
        return headers.count
    }
    
    func numberOfRows(in spreadsheetView: SpreadsheetView) -> Int {
        return 1 + sucursales.count
    }
    
    func spreadsheetView(_ spreadsheetView: SpreadsheetView, widthForColumn column: Int) -> CGFloat {
        return 200
    }
    
    func spreadsheetView(_ spreadsheetView: SpreadsheetView, heightForRow row: Int) -> CGFloat {
        if case 0 = row{
            return 24
        }else{
            return 55
        }
        
    }
    
    func frozenColumns(in spreadsheetView: SpreadsheetView) -> Int {
        return 1
    }

}

class MyLabelCell: Cell {
    
    private let label = UILabel()
    
    public func setup(with text: String){
        label.text = text
        label.textAlignment = .center
        contentView.addSubview(label)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        label.frame = contentView.bounds
    }
}

class DateCell: Cell {
    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.frame = bounds
        label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        label.font = UIFont.boldSystemFont(ofSize: 15)
        label.textAlignment = .center

        contentView.addSubview(label)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

class SucursalesCell: Cell {
    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.frame = bounds
        label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        label.font = UIFont.monospacedDigitSystemFont(ofSize: 12, weight: UIFont.Weight.medium)
        label.textAlignment = .center

        contentView.addSubview(label)
    }

    override var frame: CGRect {
        didSet {
            label.frame = bounds.insetBy(dx: 6, dy: 0)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

下载JSON数据的模型

import UIKit
import Foundation


protocol ConsultaModeloProtocol: AnyObject {
    func itemConsulta (LaConsulta: [DetallesConsulta])
}

var fechaPresente: String = ""
var fechaPasada: String = ""


let elToken : String = UserDefaults.standard.string(forKey: "token")!
let helper = Helper()

class ConsultaModelo: NSObject {
    
    weak var ElDelegado : ConsultaModeloProtocol!
    
    let URLPath = helper.host+"tiendas"
    
    func downloadConsulta(){
        
        var request = URLRequest(url: URL(string: URLPath)!)
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("Bearer \(elToken)", forHTTPHeaderField: "Authorization")
        
        request.httpMethod = "POST"
        
        let SessionDefault = Foundation.URLSession(configuration: URLSessionConfiguration.ephemeral)
        URLCache.shared.removeAllCachedResponses()
        
        let task = SessionDefault.dataTask(with: request){
            (data, response, error)in
            if error != nil {
                print("Error al descargar la consulta")
            }else{
                print("Datos descargados")
                self.parseJSON(data!)
            }
        }
        task.resume()
    }
    
    func parseJSON(_ data: Data){
        var jsonResult = NSArray()
        
        do{
            jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! NSArray
        }catch let error as NSError{
            print(error)
        }
        
        var jsonElement = NSDictionary()
        var detalles = [DetallesConsulta]()
        
        for i in 0 ..< jsonResult.count{
            
            jsonElement = jsonResult[i] as! NSDictionary
            let detalle = DetallesConsulta()
            
            let Fecha = jsonElement["Fecha"]
            let Sucursal = jsonElement["Sucursal"]
            let Suc = jsonElement["Suc"]
            let VentaTotal = jsonElement["Venta_Total"]
            let NoFolios = jsonElement["N_Folios"]
            let Piezas = jsonElement["Piezas"]
            let PzaxTicket = jsonElement["PzaxTicket"]
            let TicketPromedio = jsonElement["TicketPromedio"]
         
            detalle.Fecha = Fecha as? String
            detalle.Sucursal = Sucursal as? String
            detalle.Suc = Suc as? String
            detalle.VentaTotal = VentaTotal as? String
            detalle.NoFolios = NoFolios as? Int
            detalle.Piezas = Piezas as? String
            detalle.PzaxTicket = PzaxTicket as? String
            detalle.TicketPromedio = TicketPromedio as? String
            
            detalles.append(detalle)
            
        }
        
        DispatchQueue.main.async(execute: { ()-> Void in
            
            self.ElDelegado.itemConsulta(LaConsulta: detalles)
        })
        
    }
 
    
    
}

数据模型类

import UIKit

class DetallesConsulta: NSObject {

    var Fecha: String?
    var Sucursal: String?
    var Suc: String?
    var VentaTotal: String?
    var NoFolios: Int?
    var Piezas: String?
    var PzaxTicket: String?
    var TicketPromedio: String?

    override init(){

    }

    init(Fecha: String, Sucursal: String, Suc: String, VentaTotal: String, NoFolios: Int, Piezas: String, PzaxTicket: String, TicketPromedio: String){
        self.Fecha = Fecha
        self.Sucursal = Sucursal
        self.Suc = Suc
        self.VentaTotal = VentaTotal
        self.NoFolios = Int(NoFolios)
        self.Piezas = Piezas
        self.PzaxTicket = PzaxTicket
        self.TicketPromedio = TicketPromedio

    }
    override var description: String{

        return "Fecha: \(Fecha), Sucursal: \(Sucursal), Suc: \(Suc), VentaTotal: \(VentaTotal), NoFolios: \(NoFolios), Piezas: \(Piezas), PzaxTicket: \(PzaxTicket), TicketPromedio: \(TicketPromedio)"

    }

}

JSON响应示例

[
    {
        "Fecha": "2022-11-17",
        "Sucursal": "SCALPERS PUEBLA",
        "Suc": "004",
        "Venta_Total": "xxxxxx.xxxxxxxxx",
        "N_Folios": 12,
        "Piezas": "xx.000",
        "PzaxTicket": "x.x",
        "TicketPromedio": "xxxx.x"
    },
    {
        "Fecha": "2022-11-17",
        "Sucursal": "SCALPERS SATELITE",
        "Suc": "005",
        "Venta_Total": "xxxxx.xxx",
        "N_Folios": xx,
        "Piezas": "xx.000",
        "PzaxTicket": "x.x",
        "TicketPromedio": "xxxxx.xxxx"
    },
    {
        "Fecha": "2022-11-17",
        "Sucursal": "SCALPERS OUTLET QUERETARO",
        "Suc": "006",
        "Venta_Total": "xxx.xxxxxxxxxx",
        "N_Folios": 4,
        "Piezas": "6.000",
        "PzaxTicket": "1.5",
        "TicketPromedio": "1419.5"
    },
    {
        "Fecha": "2022-11-17",
        "Sucursal": "SCALPERS ONLINE",
        "Suc": "xxxx",
        "Venta_Total": "xxxxx.xxxxxxx",
        "N_Folios": 15,
        "Piezas": "45.000",
        "PzaxTicket": "3.0",
        "TicketPromedio": "1930.5"
    }
]
解决方案

1. 实现数据与表格列的对应展示

首先修正原代码中的几个问题:

  • 移除未定义的sucursales数组,改用feedItems.count计算行数
  • 在cellForItemAt方法中补充数据行的字段映射逻辑

修正行数计算

func numberOfRows(in spreadsheetView: SpreadsheetView) -> Int {
    // 表头1行 + 数据行数
    return 1 + feedItems.count
}

完整的cellForItemAt实现

func spreadsheetView(_ spreadsheetView: SpreadsheetView, cellForItemAt indexPath: IndexPath) -> Cell? {
    // 表头行
    if indexPath.row == 0 {
        let cell = spreadsheetView.dequeueReusableCell(withReuseIdentifier: String(describing: DateCell.self), for: indexPath) as! DateCell
        cell.label.text = headers[indexPath.column]
        return cell
    }
    
    // 数据行
    let dataIndex = indexPath.row - 1
    guard dataIndex < feedItems.count else { return nil }
    let item = feedItems[dataIndex]
    
    let cell = spreadsheetView.dequeueReusableCell(withReuseIdentifier: String(describing: SucursalesCell.self), for: indexPath) as! SucursalesCell
    
    // 根据列索引匹配对应字段
    switch indexPath.column {
    case 0:
        cell.label.text = item.Sucursal
    case 1:
        cell.label.text = item.VentaTotal
    case 2:
        cell.label.text = item.NoFolios?.description
    case 3:
        cell.label.text = item.Piezas
    case 4:
        cell.label.text = item.PzaxTicket
    case 5:
        cell.label.text = item.TicketPromedio
    case 6:
        // 原headers中的"Utilidad"字段,JSON中未返回,可留空或补充逻辑
        cell.label.text = "-"
    case 7:
        // "Última venta"对应Fecha字段
        cell.label.text = item.Fecha
    default:
        cell.label.text = ""
    }
    return cell
}

2. 实现点击单元格跳转详情页

实现SpreadsheetViewDelegate的didSelectItemAt方法,获取对应的数据模型后跳转至详情页:

添加点击事件处理

func spreadsheetView(_ spreadsheetView: SpreadsheetView, didSelectItemAt indexPath: IndexPath) {
    // 跳过表头行
    guard indexPath.row > 0 else { return }
    let dataIndex = indexPath.row - 1
    guard dataIndex < feedItems.count else { return }
    
    let selectedItem = feedItems[dataIndex]
    // 假设详情页为DetalleViewController,需提前创建该页面
    let detailVC = DetalleViewController()
    // 将选中的数据传递给详情页
    detailVC.selectedData = selectedItem
    // 跳转页面(可根据需求用push或present)
    navigationController?.pushViewController(detailVC, animated: true)
}

详情页示例(参考)

class DetalleViewController: UIViewController {
    var selectedData: DetallesConsulta!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        // 这里添加UI组件展示selectedData的详细信息
        let sucursalLabel = UILabel(frame: CGRect(x: 20, y: 100, width: view.frame.width-40, height: 30))
        sucursalLabel.text = "Sucursal: \(selectedData.Sucursal ?? "")"
        view.addSubview(sucursalLabel)
        
        // 其他字段同理添加展示
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 12:31:00