SwiftUI制作简单时间轴视图
生活中我们经常会使用手机查询物流信息、历史记录浏览、项目进度跟踪,展示这些数据的方式多种多样,而最经典的莫过于带有时间轴的列表视图,它能够清晰的表达出数据的实时动态。本文将简单制作一个常见的SwiftUI时间轴列表,可按照自己的需求调整

提取码:6e7j
1. 整体视图结构
TimeLineView 是整个时间轴组件的入口,包含以下几个主要部分:
TimeLineView:主视图,包含时间轴的滚动视图。GeneraTimeLineView:时间轴的核心视图,负责展示所有条目。timeLineSubView:时间轴的单条子视图,包含左侧圆点、右侧时间和详细信息。GeneraTimeLineViewItemModel:用于描述每个时间轴条目的数据模型。
代码预览
struct TimeLineView: View {
@State private var values: [GeneraTimeLineViewItemModel] = [] // 时间轴条目数据
var body: some View {
ScrollView(.vertical, showsIndicators: false) { // 垂直滚动视图
getTimeLineView()
.padding()
}
.navigationTitle("时间轴")
.onAppear {
getValues() // 初始化数据
}
}
}功能分析
数据初始化: 在
onAppear生命周期方法中调用getValues(),动态加载时间轴条目数据。滚动视图: 使用
ScrollView创建垂直滚动视图,保证条目数量较多时用户可以上下滑动查看。时间轴核心视图调用:
getTimeLineView()方法返回时间轴核心视图。
2. 数据模型
每个时间轴条目都由 GeneraTimeLineViewItemModel 描述:
class GeneraTimeLineViewItemModel: Identifiable, Equatable {
static func == (lhs: GeneraTimeLineViewItemModel, rhs: GeneraTimeLineViewItemModel) -> Bool {
lhs.id == rhs.id
}
let id = UUID() // 唯一标识
var lTitle: String // 左侧标题
var rTitle: String // 右侧时间
var details: String // 详情内容
init(lTitle: String, rTitle: String, details: String) {
self.lTitle = lTitle
self.rTitle = rTitle
self.details = details
}
}数据字段解析
id:每个条目的唯一标识,用于在ForEach中绑定。lTitle:左侧显示的标题。rTitle:右侧显示的时间信息。details:条目详情描述。
示例数据
private func getValues() {
values = [
GeneraTimeLineViewItemModel(lTitle: "已核实", rTitle: "2024-09-20 10:21", details: "测试文字"),
GeneraTimeLineViewItemModel(lTitle: "平台核实中", rTitle: "2024-09-20 10:21", details: "测试文字"),
GeneraTimeLineViewItemModel(lTitle: "接到用户投诉", rTitle: "2024-09-20 10:21", details: "测试文字")
]
}3. 核心时间轴视图
GeneraTimeLineView 是展示时间轴的核心视图:
struct GeneraTimeLineView: View {
let items: [GeneraTimeLineViewItemModel] // 条目数组
var body: some View {
LazyVStack(spacing: 0) { // 使用 LazyVStack 节省性能
ForEach(items) { model in
timeLineSubView(items: items, model: model) // 子视图
}
}
}
}4. 时间轴子视图
timeLineSubView 是时间轴的单条条目视图。
struct timeLineSubView: View {
let items: [GeneraTimeLineViewItemModel]
@State var model: GeneraTimeLineViewItemModel
@State private var whole_height: CGFloat = 0 // 整体高度
@State private var top_height: CGFloat = 0 // 标题栏高度
private let CircleWidth: CGFloat = 15 // 圆点宽度
private let stroke_width: CGFloat = 4 // 圆点描边宽度
/// 连接线偏移量
private var connectionLineOffset: CGSize {
CGSize(
width: (CircleWidth - 2) / 2,
height: CircleWidth + (top_height - CircleWidth) / 2 + stroke_width / 2
)
}
var body: some View {
VStack(spacing: 10) {
// 标题栏
HStack {
Circle()
.stroke(lineWidth: stroke_width)
.foregroundStyle(Color(.systemOrange))
.opacity(model == items.first ? 1 : 0.4)
.frame(width: CircleWidth, height: CircleWidth)
Text(model.lTitle)
Spacer()
Text(model.rTitle)
.foregroundStyle(Color(.systemGray))
.font(.footnote)
}
.background {
GeometryReader { geometry in
Color.clear
.onAppear { top_height = geometry.size.height }
.onChange(of: geometry.size) { newSize in
top_height = newSize.height
}
}
}
// 详情内容
HStack {
Color.clear.frame(width: CircleWidth)
Text(model.details)
.font(.footnote)
.foregroundStyle(Color(.systemGray))
.padding()
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 15))
.shadow(radius: 5)
}
}
.background {
GeometryReader { geometry in
Color.clear
.onAppear { whole_height = geometry.size.height }
.onChange(of: geometry.size) { newSize in
whole_height = newSize.height }
}
}
.overlay(alignment: .topLeading) {
if model != items.last {
Rectangle()
.frame(width: 2, height: whole_height - CircleWidth - stroke_width)
.foregroundStyle(Color(.systemOrange))
.opacity(model == items.first ? 1 : 0.4)
.offset(connectionLineOffset)
}
}
}
}偏移量分析:如何保持连接线连续
1. 水平偏移计算
width: (CircleWidth - 2) / 2- 连接线宽度固定为
2。 - 圆点宽度为
CircleWidth。 - 偏移量计算公式
(CircleWidth - 2) / 2确保连接线水平居中对齐到圆点中心。
2. 垂直偏移计算
height: CircleWidth + (top_height - CircleWidth) / 2 + stroke_width / 2CircleWidth:连接线从圆点的底部开始。(top_height - CircleWidth) / 2:动态适配标题栏高度,使连接线垂直居中对齐标题内容。stroke_width / 2:考虑圆点描边的额外偏移,确保线条与描边外缘相切。
3. 连续性原理
- 每个条目的连接线长度是动态计算的,基于整个子视图高度减去圆点直径。
- 动态适配条目高度 (
whole_height) 和标题栏高度 (top_height),无论内容多少,都能确保线条精准对齐。