Skip to content

Commit cceeed5

Browse files
committed
Add new styling options and header item management to STMarkdown components
- Introduced new properties in STMarkdownStyle for table header bar background color, minimum row height, font, header items, button width, and corner mask. - Updated STMarkdownTableCell to apply custom font from style during configuration. - Added STMarkdownTableHeaderItem struct for customizable header buttons and integrated it into STMarkdownTableView. - Enhanced header item management in STMarkdownTableView to support dynamic button creation and styling.
1 parent d21b530 commit cceeed5

3 files changed

Lines changed: 176 additions & 59 deletions

File tree

Sources/STMarkdown/Core/STMarkdownStyle.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ public struct STMarkdownStyle: @unchecked Sendable {
5454
public var tableHeaderTextColor: UIColor?
5555
public var tableBorderColor: UIColor?
5656
public var tableBackgroundColor: UIColor?
57+
/// 顶部工具条背景色(nil 时回退到 tableBackgroundColor → secondarySystemBackground)
58+
public var tableHeaderBarBackgroundColor: UIColor?
59+
/// 表格行最小高度,同步影响 UICollectionView 布局与 computeSize
60+
public var tableMinimumRowHeight: CGFloat
61+
/// 单元格字体,非 nil 时在 configure 阶段覆盖 attributedContent 中的字体大小(保留粗/斜等修饰)
62+
public var tableFont: UIFont?
63+
/// 工具条按钮项(nil → makeDefaultHeaderItems();外界通过 style 统一分发)
64+
public var tableHeaderItems: [STMarkdownTableHeaderItem]?
65+
/// 工具条按钮触控区宽度
66+
public var tableHeaderButtonWidth: CGFloat
67+
/// 容器圆角蒙版,默认四角全圆
68+
public var tableCornerMask: CACornerMask
5769
public var imagePlaceholderTextColor: UIColor?
5870
public var imagePlaceholderBackgroundColor: UIColor?
5971
public var imagePlaceholderCaptionColor: UIColor?
@@ -158,6 +170,13 @@ public struct STMarkdownStyle: @unchecked Sendable {
158170
tableHeaderTextColor: UIColor? = nil,
159171
tableBorderColor: UIColor? = nil,
160172
tableBackgroundColor: UIColor? = nil,
173+
tableHeaderBarBackgroundColor: UIColor? = nil,
174+
tableMinimumRowHeight: CGFloat = 35,
175+
tableFont: UIFont? = nil,
176+
tableHeaderItems: [STMarkdownTableHeaderItem]? = nil,
177+
tableHeaderButtonWidth: CGFloat = 30,
178+
tableCornerMask: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner,
179+
.layerMinXMaxYCorner, .layerMaxXMaxYCorner],
161180
imagePlaceholderTextColor: UIColor? = nil,
162181
imagePlaceholderBackgroundColor: UIColor? = nil,
163182
imagePlaceholderCaptionColor: UIColor? = nil,
@@ -223,6 +242,12 @@ public struct STMarkdownStyle: @unchecked Sendable {
223242
self.tableHeaderTextColor = tableHeaderTextColor
224243
self.tableBorderColor = tableBorderColor
225244
self.tableBackgroundColor = tableBackgroundColor
245+
self.tableHeaderBarBackgroundColor = tableHeaderBarBackgroundColor
246+
self.tableMinimumRowHeight = tableMinimumRowHeight
247+
self.tableFont = tableFont
248+
self.tableHeaderItems = tableHeaderItems
249+
self.tableHeaderButtonWidth = tableHeaderButtonWidth
250+
self.tableCornerMask = tableCornerMask
226251
self.imagePlaceholderTextColor = imagePlaceholderTextColor
227252
self.imagePlaceholderBackgroundColor = imagePlaceholderBackgroundColor
228253
self.imagePlaceholderCaptionColor = imagePlaceholderCaptionColor

Sources/STMarkdown/Table/STMarkdownTableCell.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,20 @@ public final class STMarkdownTableCell: UICollectionViewCell {
5454
}
5555

5656
func configure(with cellData: STMarkdownTableCellData, style: STMarkdownStyle) {
57-
self.contentLabel.attributedText = cellData.attributedContent
57+
let displayText: NSAttributedString
58+
if let tableFont = style.tableFont {
59+
let mutable = NSMutableAttributedString(attributedString: cellData.attributedContent)
60+
let fullRange = NSRange(location: 0, length: mutable.length)
61+
mutable.enumerateAttribute(.font, in: fullRange, options: []) { value, range, _ in
62+
if let originalFont = value as? UIFont {
63+
mutable.addAttribute(.font, value: originalFont.withSize(tableFont.pointSize), range: range)
64+
}
65+
}
66+
displayText = mutable
67+
} else {
68+
displayText = cellData.attributedContent
69+
}
70+
self.contentLabel.attributedText = displayText
5871
let bgColor = style.tableBackgroundColor ?? UIColor.secondarySystemBackground
5972
self.contentView.backgroundColor = cellData.role.isHeader
6073
? bgColor.withAlphaComponent(0.92)

Sources/STMarkdown/Table/STMarkdownTableView.swift

Lines changed: 137 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,60 @@
77

88
import UIKit
99

10-
public final class STMarkdownTableView: UIView {
10+
// MARK: - STMarkdownTableHeaderItem
11+
12+
/// 顶部工具条的单个按钮描述。外界可组合内置工厂方法或自定义,赋给 `STMarkdownTableView.headerItems`。
13+
public struct STMarkdownTableHeaderItem {
14+
public let identifier: String
15+
public let image: UIImage?
16+
/// handler 在主线程调用,参数为触发按钮的 tableView,便于访问数据或调用 renderFullTableImage()。
17+
public let handler: (STMarkdownTableView) -> Void
18+
19+
public init(identifier: String, image: UIImage?, handler: @escaping (STMarkdownTableView) -> Void) {
20+
self.identifier = identifier
21+
self.image = image
22+
self.handler = handler
23+
}
24+
25+
/// 复制按钮:将表格纯文本写入剪贴板,并触发 onCopyTable 回调。
26+
public static func copy() -> STMarkdownTableHeaderItem {
27+
STMarkdownTableHeaderItem(
28+
identifier: "copy",
29+
image: UIImage(systemName: "doc.on.doc")
30+
) { tableView in
31+
guard let tableData = tableView.tableData else { return }
32+
UIPasteboard.general.string = tableData.plainText()
33+
tableView.onCopyTable?()
34+
tableView.showCopyFeedback()
35+
}
36+
}
37+
38+
/// 下载按钮:触发 onDownloadTable 回调,由宿主实现具体保存逻辑。
39+
public static func download() -> STMarkdownTableHeaderItem {
40+
STMarkdownTableHeaderItem(
41+
identifier: "download",
42+
image: UIImage(systemName: "square.and.arrow.down")
43+
) { tableView in
44+
guard let tableData = tableView.tableData else { return }
45+
tableView.onDownloadTable?(tableData)
46+
}
47+
}
48+
49+
/// 全屏按钮:触发 onExpandTable 回调,与长按展开行为一致。
50+
public static func fullscreen() -> STMarkdownTableHeaderItem {
51+
STMarkdownTableHeaderItem(
52+
identifier: "fullscreen",
53+
image: UIImage(systemName: "arrow.up.left.and.arrow.down.right")
54+
) { tableView in
55+
guard let tableData = tableView.tableData else { return }
56+
tableView.onExpandTable?(tableData)
57+
}
58+
}
59+
}
60+
61+
// MARK: - STMarkdownTableView
62+
63+
open class STMarkdownTableView: UIView {
1164

1265
/// 表格数据 façade。流式逐行追加(同列数、已有行内容不变、行数严格增多)时,
1366
/// 用 `performBatchUpdates` 插入新 section 并逐行淡入;否则全量 `reloadData()`。
@@ -31,21 +84,36 @@ public final class STMarkdownTableView: UIView {
3184
private var isApplyingAppend = false
3285

3386
public var style: STMarkdownStyle = .default {
34-
didSet { self.applyStyle(); self.reloadData() }
87+
didSet {
88+
self.applyStyle()
89+
self.applyStyleHeaderItems()
90+
self.reloadData()
91+
}
3592
}
3693

3794
public var onCitationTap: ((String) -> Void)?
3895
public var onExpandTable: ((STMarkdownTableViewModel) -> Void)?
3996
public var onCopyTable: (() -> Void)?
4097
public var onDownloadTable: ((STMarkdownTableViewModel) -> Void)?
4198

99+
/// 顶部工具条按钮列表。默认为 [copy, download, fullscreen],赋新值后立即重建按钮栈。
100+
/// 赋空数组可隐藏所有按钮但保留标题行;配合 showsHeader=false 可完全隐藏工具条。
101+
public var headerItems: [STMarkdownTableHeaderItem] = [] {
102+
didSet { self.rebuildButtonStack() }
103+
}
104+
105+
/// 顶部工具条左侧标题,默认 "表格"。
106+
public var headerTitle: String = "表格" {
107+
didSet { self.titleLabel.text = self.headerTitle }
108+
}
109+
42110
/// 顶部工具条高度(圆角卡片化后预留给「表格 / 复制 / 下载 / 全屏」)。
43-
public static let headerHeight: CGFloat = 44
111+
public static let headerHeight: CGFloat = 41
44112
/// 整块表格圆角半径。
45113
public var cornerRadius: CGFloat = 10 {
46114
didSet { self.layer.cornerRadius = self.cornerRadius }
47115
}
48-
/// 是否展示顶部工具条。全屏详情页关闭(自带关闭按钮,避免重复表头与全屏中再全屏)。
116+
/// 是否展示顶部工具条。全屏详情页关闭(自带关闭按钮,避免重复表头与"全屏中再全屏")。
49117
public var showsHeader: Bool = true {
50118
didSet {
51119
guard oldValue != self.showsHeader else { return }
@@ -55,18 +123,18 @@ public final class STMarkdownTableView: UIView {
55123
}
56124
}
57125

126+
/// 单元格内边距,影响列宽/行高计算,与 computeSize 保持一致。
127+
public var cellInsets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
128+
58129
private let gridLayout: STMarkdownTableGridLayout
59130
private let collectionView: UICollectionView
60-
private let cellInsets = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
61131

62132
private let headerBar = UIView()
63133
private let titleLabel = UILabel()
64134
private let buttonStack = UIStackView()
65-
private let copyButton = UIButton(type: .system)
66-
private let downloadButton = UIButton(type: .system)
67-
private let fullscreenButton = UIButton(type: .system)
68135
private let headerSeparator = UIView()
69136
private var copyResetWorkItem: DispatchWorkItem?
137+
private weak var copyButtonRef: UIButton?
70138
private weak var expandGesture: UILongPressGestureRecognizer?
71139

72140
/// 全屏详情态:开启上下/左右滚动条与回弹,并关闭内置「长按展开」手势(由详情页接管长按菜单)。
@@ -88,14 +156,14 @@ public final class STMarkdownTableView: UIView {
88156
super.init(frame: .zero)
89157
self.clipsToBounds = true
90158
self.layer.cornerRadius = self.cornerRadius
91-
self.layer.borderWidth = 0.5
159+
self.layer.borderWidth = 1
92160
self.setupCollectionView()
93161
self.setupHeader()
162+
self.headerItems = style.tableHeaderItems ?? self.makeDefaultHeaderItems()
94163
self.applyStyle()
95164
}
96165

97-
@available(*, unavailable)
98-
required init?(coder: NSCoder) {
166+
public required init?(coder: NSCoder) {
99167
fatalError("init(coder:) has not been implemented")
100168
}
101169

@@ -122,28 +190,12 @@ public final class STMarkdownTableView: UIView {
122190
}
123191

124192
private func setupHeader() {
125-
self.titleLabel.text = "表格"
193+
self.titleLabel.text = self.headerTitle
126194
self.titleLabel.font = UIFont.st_systemFont(ofSize: 14, weight: .medium)
127195

128-
self.copyButton.setImage(UIImage(systemName: "doc.on.doc"), for: .normal)
129-
self.copyButton.addTarget(self, action: #selector(self.handleCopy), for: .touchUpInside)
130-
self.downloadButton.setImage(UIImage(systemName: "square.and.arrow.down"), for: .normal)
131-
self.downloadButton.addTarget(self, action: #selector(self.handleDownload), for: .touchUpInside)
132-
self.fullscreenButton.setImage(UIImage(systemName: "arrow.up.left.and.arrow.down.right"), for: .normal)
133-
self.fullscreenButton.addTarget(self, action: #selector(self.handleFullscreen), for: .touchUpInside)
134-
135-
let imageConfig = UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
136-
for button in [self.copyButton, self.downloadButton, self.fullscreenButton] {
137-
button.setPreferredSymbolConfiguration(imageConfig, forImageIn: .normal)
138-
button.widthAnchor.constraint(equalToConstant: 30).isActive = true
139-
}
140-
141196
self.buttonStack.axis = .horizontal
142197
self.buttonStack.alignment = .center
143198
self.buttonStack.spacing = 6
144-
self.buttonStack.addArrangedSubview(self.copyButton)
145-
self.buttonStack.addArrangedSubview(self.downloadButton)
146-
self.buttonStack.addArrangedSubview(self.fullscreenButton)
147199

148200
self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
149201
self.buttonStack.translatesAutoresizingMaskIntoConstraints = false
@@ -168,24 +220,61 @@ public final class STMarkdownTableView: UIView {
168220
])
169221
}
170222

223+
/// 从 headerItems 重建 buttonStack 中的所有按钮,每次 headerItems 变更时调用。
224+
private func rebuildButtonStack() {
225+
self.buttonStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
226+
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
227+
let buttonWidth = self.style.tableHeaderButtonWidth
228+
let secondaryColor = (self.style.tableHeaderTextColor ?? self.style.textColor).withAlphaComponent(0.6)
229+
230+
for item in self.headerItems {
231+
let button = UIButton(type: .system)
232+
let isSystemSymbol = item.image?.isSymbolImage ?? false
233+
if isSystemSymbol {
234+
button.setPreferredSymbolConfiguration(symbolConfig, forImageIn: .normal)
235+
}
236+
button.setImage(item.image, for: .normal)
237+
button.tintColor = secondaryColor
238+
button.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
239+
button.accessibilityIdentifier = item.identifier
240+
button.addAction(UIAction { [weak self] _ in
241+
guard let self else { return }
242+
item.handler(self)
243+
}, for: .touchUpInside)
244+
if item.identifier == "copy" {
245+
self.copyButtonRef = button
246+
}
247+
self.buttonStack.addArrangedSubview(button)
248+
}
249+
}
250+
171251
private func applyStyle() {
172252
let borderColor = self.style.tableBorderColor ?? UIColor.separator
173253
self.collectionView.backgroundColor = borderColor
174254
self.backgroundColor = borderColor
175255
self.gridLayout.interItemSpacing = 0.5
176256
self.gridLayout.lineSpacing = 0.5
257+
self.gridLayout.minimumRowHeight = self.style.tableMinimumRowHeight
177258
self.layer.borderColor = borderColor.cgColor
259+
self.layer.maskedCorners = self.style.tableCornerMask
178260

179-
let headerBg = self.style.tableBackgroundColor ?? UIColor.secondarySystemBackground
261+
let headerBg = self.style.tableHeaderBarBackgroundColor
262+
?? self.style.tableBackgroundColor
263+
?? UIColor.secondarySystemBackground
180264
let secondaryColor = (self.style.tableHeaderTextColor ?? self.style.textColor).withAlphaComponent(0.6)
181265
self.headerBar.backgroundColor = headerBg
182266
self.headerSeparator.backgroundColor = borderColor
183267
self.titleLabel.textColor = secondaryColor
184-
for button in [self.copyButton, self.downloadButton, self.fullscreenButton] {
185-
button.tintColor = secondaryColor
268+
self.buttonStack.arrangedSubviews.compactMap { $0 as? UIButton }.forEach {
269+
$0.tintColor = secondaryColor
186270
}
187271
}
188272

273+
/// style 变更时同步按钮项(优先用 style.tableHeaderItems,否则 makeDefaultHeaderItems)。
274+
private func applyStyleHeaderItems() {
275+
self.headerItems = self.style.tableHeaderItems ?? self.makeDefaultHeaderItems()
276+
}
277+
189278
public override func layoutSubviews() {
190279
super.layoutSubviews()
191280
let headerHeight = self.showsHeader ? Self.headerHeight : 0
@@ -243,7 +332,7 @@ public final class STMarkdownTableView: UIView {
243332
metrics: STMarkdownTableGridLayout.ComputeSizeMetrics(
244333
fillWidth: true,
245334
containerWidth: containerWidth,
246-
minimumRowHeight: 35,
335+
minimumRowHeight: style.tableMinimumRowHeight,
247336
minimumColumnWidth: 56,
248337
maximumColumnWidth: 360,
249338
interItemSpacing: 0.5,
@@ -419,28 +508,29 @@ public final class STMarkdownTableView: UIView {
419508

420509
@objc private func handleExpandGesture(_ gestureRecognizer: UILongPressGestureRecognizer) {
421510
guard gestureRecognizer.state == .began else { return }
422-
self.expandTableIfPossible()
423-
}
424-
425-
private func expandTableIfPossible() {
426511
guard let tableData else { return }
427512
self.onExpandTable?(tableData)
428513
}
429514

430-
@objc private func handleCopy() {
431-
guard let tableData else { return }
432-
UIPasteboard.general.string = tableData.plainText()
433-
self.onCopyTable?()
434-
self.showCopyFeedback()
435-
}
515+
// MARK: - Open Overridable
436516

437-
@objc private func handleDownload() {
438-
guard let tableData else { return }
439-
self.onDownloadTable?(tableData)
517+
/// 返回顶部工具条的默认按钮列表 [复制, 下载, 全屏]。子类可 override 替换默认集合。
518+
/// 外界也可在初始化后直接赋 headerItems 覆盖,无需子类化。
519+
open func makeDefaultHeaderItems() -> [STMarkdownTableHeaderItem] {
520+
[.copy(), .download(), .fullscreen()]
440521
}
441522

442-
@objc private func handleFullscreen() {
443-
self.expandTableIfPossible()
523+
/// 复制成功后的视觉反馈。默认将图标切换为对勾,~1.2s 后还原。
524+
/// 子类可 override 接入宿主 Toast/HUD 系统。
525+
open func showCopyFeedback() {
526+
self.copyResetWorkItem?.cancel()
527+
let originalImage = self.copyButtonRef?.image(for: .normal)
528+
self.copyButtonRef?.setImage(UIImage(systemName: "checkmark"), for: .normal)
529+
let workItem = DispatchWorkItem { [weak self] in
530+
self?.copyButtonRef?.setImage(originalImage, for: .normal)
531+
}
532+
self.copyResetWorkItem = workItem
533+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2, execute: workItem)
444534
}
445535

446536
/// 将整张表格(含离屏行列)渲染为图片,供「复制为图片 / 保存到相册」使用。
@@ -470,17 +560,6 @@ public final class STMarkdownTableView: UIView {
470560
self.collectionView.layoutIfNeeded()
471561
return image
472562
}
473-
474-
/// 复制成功后将图标临时切换为对勾,~1.2s 后还原,提供轻量内建反馈(无需宿主接线)。
475-
private func showCopyFeedback() {
476-
self.copyResetWorkItem?.cancel()
477-
self.copyButton.setImage(UIImage(systemName: "checkmark"), for: .normal)
478-
let workItem = DispatchWorkItem { [weak self] in
479-
self?.copyButton.setImage(UIImage(systemName: "doc.on.doc"), for: .normal)
480-
}
481-
self.copyResetWorkItem = workItem
482-
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2, execute: workItem)
483-
}
484563
}
485564

486565
extension STMarkdownTableView: UICollectionViewDelegate {

0 commit comments

Comments
 (0)