动态Excel 表格
实现的功能样子如下:
- 数据由二维数组驱动
model = [[String]]
- 行数由有
model.count
决定 - 每一列间隔相等,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
- 每一行的一维数组,
- 判断是否为第一列(标题加背景色,加粗等),
- 是否为最后一列(要加上最下面的边)
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