Home
avatar

SNOUX·雪飞

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() // 初始化数据
        }
    }
}

功能分析

  1. 数据初始化: 在 onAppear 生命周期方法中调用 getValues(),动态加载时间轴条目数据。

  2. 滚动视图: 使用 ScrollView 创建垂直滚动视图,保证条目数量较多时用户可以上下滑动查看。

  3. 时间轴核心视图调用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 / 2
  • CircleWidth:连接线从圆点的底部开始。
  • (top_height - CircleWidth) / 2:动态适配标题栏高度,使连接线垂直居中对齐标题内容。
  • stroke_width / 2:考虑圆点描边的额外偏移,确保线条与描边外缘相切。

3. 连续性原理

  • 每个条目的连接线长度是动态计算的,基于整个子视图高度减去圆点直径。
  • 动态适配条目高度 (whole_height) 和标题栏高度 (top_height),无论内容多少,都能确保线条精准对齐。

SwiftUI 技术