Files
Pulse/Sources/PulseUI/Views/TimingView.swift
T
2024-04-13 13:57:23 -04:00

193 lines
5.3 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
import SwiftUI
struct TimingView: View {
let viewModel: TimingViewModel
var body: some View {
#if os(tvOS)
ForEach(viewModel.sections) { item in
Section {
Button(action: {}) {
TimingSectionView(viewModel: item, parent: viewModel)
}
}
}
#else
VStack(spacing: 16) {
ForEach(viewModel.sections) {
TimingSectionView(viewModel: $0, parent: viewModel)
}
}
#endif
}
}
private struct TimingSectionView: View {
let viewModel: TimingRowSectionViewModel
let parent: TimingViewModel
var body: some View {
VStack(spacing: 6) {
HStack {
Text(viewModel.title)
.font(.subheadline)
.lineLimit(1)
.foregroundColor(viewModel.isHeader ? .secondary : .primary)
Spacer()
}.padding(.top, viewModel.isHeader ? 16 : 0)
if viewModel.isHeader {
Divider()
}
if !viewModel.items.isEmpty {
ForEach(viewModel.items) { item in
TimingRowView(viewModel: item, parent: parent)
}
}
}
}
}
private struct TimingRowView: View {
let viewModel: TimingRowViewModel
let parent: TimingViewModel
#if os(tvOS)
let barHeight: CGFloat = 20
let spacing: CGFloat = 48
#else
let barHeight: CGFloat = 14
let spacing: CGFloat = 12
#endif
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
HStack(alignment: .center, spacing: spacing) {
ZStack(alignment: .leading) {
makeTitle(viewModel.title)
makeTitle(parent.longestTitle).invisible() // Calculates width
}.layoutPriority(1)
bar
#if !os(watchOS)
ZStack(alignment: .trailing) {
makeValue(viewModel.value)
makeValue(parent.longestValue).invisible() // Calculates width
}.layoutPriority(1)
#endif
}
}
private var bar: some View {
GeometryReader { proxy in
let start = max(0, min(1, viewModel.start))
let length = min(1 - start, viewModel.length)
RoundedRectangle(cornerRadius: 2 * sizeCategory.scale)
.fill(Color(viewModel.color))
.frame(width: max(2, proxy.size.width * length))
.padding(.leading, proxy.size.width * start)
}
.frame(height: barHeight * sizeCategory.scale)
}
private func makeTitle(_ text: String) -> some View {
Text(text)
.font(.footnote)
.lineLimit(1)
.foregroundColor(.secondary)
}
private func makeValue(_ text: String) -> some View {
Text(text)
.font(.system(.caption, design: .monospaced))
.lineLimit(1)
.foregroundColor(.secondary)
}
}
final class TimingViewModel {
let sections: [TimingRowSectionViewModel]
init(sections: [TimingRowSectionViewModel]) {
self.sections = sections
}
private(set) lazy var longestTitle: String = {
allRows.map(\.title).max { $0.count < $1.count } ?? ""
}()
private(set) lazy var longestValue: String = {
allRows.map(\.value).max { $0.count < $1.count } ?? ""
}()
private var allRows: [TimingRowViewModel] {
sections.flatMap(\.items)
}
}
final class TimingRowSectionViewModel: Identifiable {
let title: String
let items: [TimingRowViewModel]
var isHeader = false
var id: ObjectIdentifier { ObjectIdentifier(self) }
init(title: String, items: [TimingRowViewModel], isHeader: Bool = false) {
self.title = title
self.items = items
self.isHeader = isHeader
}
}
final class TimingRowViewModel: Identifiable {
let title: String
let value: String
let color: UXColor
// [0, 1]
let start: CGFloat
// [0, 1]
let length: CGFloat
var id: ObjectIdentifier { ObjectIdentifier(self) }
init(title: String, value: String, color: UXColor, start: CGFloat, length: CGFloat) {
self.title = title
self.value = value
self.color = color
self.start = start
self.length = length
}
}
#if DEBUG
struct TimingView_Previews: PreviewProvider {
static var previews: some View {
ScrollView {
TimingView(viewModel: .init(sections: mockSections))
}
.padding()
#if !os(tvOS)
.previewLayout(.sizeThatFits)
#endif
}
}
private let mockSections = [
TimingRowSectionViewModel(title: "Response", items: [
TimingRowViewModel(title: "Scheduling", value: "0.01ms", color: .systemBlue, start: 0.0, length: 0.001),
TimingRowViewModel(title: "Waiting", value: "41.2ms", color: .systemBlue, start: 0.0, length: 0.4),
TimingRowViewModel(title: "Download", value: "0.2ms", color: .systemRed, start: 0.4, length: 0.05)
]),
TimingRowSectionViewModel(title: "Cache Lookup", items: [
TimingRowViewModel(title: "Waiting", value: "50.2ms", color: .systemYellow, start: 0.45, length: 0.3),
TimingRowViewModel(title: "Download", value: "30.0ms", color: .systemGreen, start: 0.75, length: 100.0)
])
]
#endif