Swift 5 用TableView实现动态Excel表格Spreadsheet

动态Excel 表格

实现的功能样子如下:

  1. 数据由二维数组驱动model = [[String]]
  2. 行数由有model.count决定
  3. 每一列间隔相等,column列数由数据个数决定model[0].count

在这里插入图片描述

实现方案分析

在google 上搜索 Spreadsheet 有很多强大的表格。
考虑到简单实现,笔者用UITableview来实现,每一行都是一个UIStackView, 边框用UIView来实现。

SpreadSheetView

复用的tableView

//
//  SpreadSheetView.swift
//  SpreadsheetView
//
//  Created by zgpeace on 2021/3/7.
//

import UIKit

class SpreadSheetView: UIView {
    
    

    private lazy var tableView: UITableView = {
    
    
        let tableView = UITableView(frame: .zero)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.separatorStyle = .none
        tableView.delegate = self
        tableView.dataSource = self
        
        return tableView
    }()
    
    private let viewModel: [[String]]

    public init(viewModel: [[String]]) {
    
    
        if viewModel.count < 1 {
    
    
            fatalError("less than one row")
        }
        
        self.viewModel = viewModel
        super.init(frame: .zero)
        
        setupView()
    }
    
    required init?(coder: NSCoder) {
    
    
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
    
    
        addSubview(self.tableView)
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: topAnchor, constant: 50),
            tableView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -50),
            tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
            tableView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12)
        ])
        
        tableView.register(SpreadsheetCell.self, forCellReuseIdentifier: "\(SpreadsheetCell.self)")
        tableView.estimatedRowHeight = 44
        tableView.rowHeight = UITableView.automaticDimension
    }
}

extension SpreadSheetView: UITableViewDataSource {
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    
        viewModel.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(SpreadsheetCell.self)", for: indexPath) as? SpreadsheetCell else {
    
    
            return UITableViewCell()
        }
        let model = SpreadsheetItemViewModel(
            items: viewModel[indexPath.row],
            isFirstLine: indexPath.row == 0,
            isLastLine: indexPath.row == viewModel.count - 1)
        cell.update(viewModel: model)
        
        return cell
    }
}

extension SpreadSheetView: UITableViewDelegate {
    
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    
        tableView.deselectRow(at: indexPath, animated: false)
    }
}


SpreadsheetItemViewModel

SpreadsheetItemViewModel

  1. 每一行的一维数组,
  2. 判断是否为第一列(标题加背景色,加粗等),
  3. 是否为最后一列(要加上最下面的边)

BorderViews, 存着上下左右四个分割线,默认每个表格显示Top,Left边线,每一行的最后一个增加Right的线;最后行增加Bottom的线。

//
//  SpreadsheetItemViewModel.swift
//  SpreadsheetView
//
//  Created by zgpeace on 2021/3/7.
//

import Foundation
import UIKit

struct SpreadsheetItemViewModel {
    
    
    let items: [String]
    let isFirstLine: Bool
    let isLastLine: Bool
}

struct BorderViews {
    
    
    let topBorder: UIView
    let bottomBorder: UIView
    let leftBorder: UIView
    let rightBorder: UIView
}

SpreadsheetCell

tableViewCell 的复写,主要是用StackView实现相等的空格大小

//
//  SpreadsheetCell.swift
//  SpreadsheetView
//
//  Created by zgpeace on 2021/3/7.
//

import UIKit

class SpreadsheetCell: UITableViewCell {
    
    
    
//    private var itemView: UIView!
    private var borderViewArray: [BorderViews?] = []
    private var labelArray: [UILabel?] = []
    private var labelContentViewArray: [UIView?] = []
    
    private lazy var mainStackView: UIStackView = {
    
    
       let stack = UIStackView()
        stack.axis = .horizontal
        stack.distribution = .fillEqually
        stack.alignment = .top
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        return stack
    }()
    
    private var viewModel: SpreadsheetItemViewModel!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    
    
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    
    required init?(coder: NSCoder) {
    
    
        fatalError("init(coder:) has not been implemented")
    }
    
    override func awakeFromNib() {
    
    
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
    
    
        super.setSelected(selected, animated: animated)
    }
    
    public func update(viewModel: SpreadsheetItemViewModel) {
    
    
        self.viewModel = viewModel
        if !mainStackView.isDescendant(of: self) {
    
    
            setupView()
        }
        
        applyViewModel()
        
        layoutIfNeeded()
    }
    
    func applyViewModel() {
    
    
        let lastIndex = viewModel.items.capacity - 1
        for (index, item) in viewModel.items.enumerated() {
    
    
            labelArray[index]?.text = item
            labelArray[index]?.sizeToFit()
            hideUIViewBorder(withIsLastLine: viewModel.isLastLine,
                             isLastIndex: index == lastIndex,
                             bottomBorder: borderViewArray[index]?.bottomBorder ?? UIView(),
                             rightBorder: borderViewArray[index]?.rightBorder ?? UIView())
        }
    }
    
    func setupView() {
    
    
        addSubview(mainStackView)
        for item in viewModel.items {
    
    
            let label = buildLabel(with: item)
            let view = buildLabelView(with: label)
            let topBorder = view.addBorder(.top, color: .darkGray, thickness: 1)
            let bottomBorder = view.addBorder(.bottom, color: .darkGray, thickness: 1)
            let leftBorder = view.addBorder(.left, color: .darkGray, thickness: 1)
            let rightBorder = view.addBorder(.right, color: .darkGray, thickness: 1)
            mainStackView.addArrangedSubview(view)
            
            borderViewArray.append(BorderViews(topBorder: topBorder, bottomBorder: bottomBorder, leftBorder: leftBorder, rightBorder: rightBorder))
            labelArray.append(label)
            labelContentViewArray.append(view)
            
            NSLayoutConstraint.activate([
                view.topAnchor.constraint(equalTo: mainStackView.topAnchor),
                view.bottomAnchor.constraint(equalTo: mainStackView.bottomAnchor)
            ])
        }
        
        NSLayoutConstraint.activate([
            mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
            mainStackView.topAnchor.constraint(equalTo: topAnchor),
            mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    private func hideUIViewBorder(
        withIsLastLine isLastLine: Bool,
        isLastIndex: Bool,
        bottomBorder: UIView,
        rightBorder: UIView) {
    
    
        bottomBorder.isHidden = !isLastLine
        rightBorder.isHidden = !isLastIndex
    }
    
    private func buildLabel(with text: String) -> UILabel {
    
    
        let label = UILabel()
        label.text = text
        label.numberOfLines = 0
        label.textAlignment = .left
        label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        label.setContentCompressionResistancePriority(.required, for: .horizontal)
        label.translatesAutoresizingMaskIntoConstraints = false
        
        return label
    }
    
    private func buildLabelView(with label: UILabel) -> UIView {
    
    
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            label.topAnchor.constraint(equalTo: view.topAnchor, constant: 8),
            label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8)
        ])
        
        return view
    }

}

画边线 UIView+Border

//
//  UIView+Border.swift
//  SpreadsheetView
//
//  Created by zgpeace on 2021/3/7.
//

import Foundation
import UIKit

extension UIView {
    
    
    func addBorder(_ edge: UIRectEdge, color: UIColor, thickness: CGFloat) -> UIView {
    
    
        let subview = UIView()
        subview.translatesAutoresizingMaskIntoConstraints = false
        subview.backgroundColor = color
        addSubview(subview)
        switch edge {
    
    
        case .top, .bottom:
            subview.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
            subview.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
            subview.heightAnchor.constraint(equalToConstant: thickness).isActive = true
            if edge == .top {
    
    
                subview.topAnchor.constraint(equalTo: topAnchor).isActive = true
            } else {
    
    
                subview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
            }
        case .left, .right:
            subview.topAnchor.constraint(equalTo: topAnchor).isActive = true
            subview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
            subview.widthAnchor.constraint(equalToConstant: thickness).isActive = true
            if edge == .left {
    
    
                subview.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
            } else {
    
    
                subview.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
            }
        default:
            break
        }
        
        return subview
    }
}

例子演示 ViewController

//
//  ViewController.swift
//  SpreadsheetView
//
//  Created by zgpeace on 2021/3/4.
//

import UIKit

class ViewController: UIViewController {
    
    
    
    private lazy var spreadsheetView: SpreadSheetView = {
    
    
        let viewModel: [[String]] = [
            ["index", "name", "hobby"],
            ["1", "John", "Reading"],
            ["2", "Lee", "Play Guitar"]
        ]
        
        let view = SpreadSheetView(viewModel: viewModel)
        view.translatesAutoresizingMaskIntoConstraints = false
        
        return view
    }()
    
    private lazy var spreadsheetView1: SpreadSheetView = {
    
    
        let viewModel: [[String]] = [
            ["index", "name", "hobby", "quotes\ndetail"],
            ["1", "John", "Reading", "The fool doth think he is wise, but the wise man knows himself to be a fool."],
            ["2", "Lee", "Play Guitar", "Love all, trust a few, do wrong to none."],
            ["3", "Elsa", "Create ice", "Be not afraid of greatness. ..."]
        ]
        
        let view = SpreadSheetView(viewModel: viewModel)
        view.translatesAutoresizingMaskIntoConstraints = false
        
        return view
    }()
    
    

    override func viewDidLoad() {
    
    
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        view.addSubview(spreadsheetView)
        view.addSubview(spreadsheetView1)
        
        NSLayoutConstraint.activate([
            spreadsheetView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16),
            spreadsheetView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -16),
            spreadsheetView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 16),
            spreadsheetView.heightAnchor.constraint(equalToConstant: 250.0),
            
            spreadsheetView1.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16),
            spreadsheetView1.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -16),
            spreadsheetView1.topAnchor.constraint(equalTo: spreadsheetView.bottomAnchor, constant: 16),
            spreadsheetView1.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
            
        ])
    }


}

代码下载

https://github.com/zgpeace/SpreadsheetView

其它实现

https://github.com/bannzai/SpreadsheetView

https://www.youtube.com/watch?v=l8VxogiHCY8&ab_channel=iOSAcademy

猜你喜欢

转载自blog.csdn.net/zgpeace/article/details/114500470