From 4dbe64ccf589a3d0878a2095754940de2bc778de Mon Sep 17 00:00:00 2001 From: Tim Xie Date: Tue, 26 Mar 2024 01:44:59 -0700 Subject: [PATCH] tidy up cody + ui improvements --- CubeTime.xcodeproj/project.pbxproj | 26 +- CubeTime/Sessions/SessionCard.swift | 2 +- CubeTime/Stats/StatsDetailView.swift | 3 +- .../Stats/{ => TimeTrend}/TimeTrend.swift | 0 .../TimeTrendDetailView.swift} | 10 +- .../TimeTrendViewController.swift} | 482 ++++++++---------- CubeTime/TimeList/TimeDetailView.swift | 4 +- CubeTime/Timer/TimerHeader.swift | 2 +- 8 files changed, 243 insertions(+), 286 deletions(-) rename CubeTime/Stats/{ => TimeTrend}/TimeTrend.swift (100%) rename CubeTime/Stats/{TimeTrendDetail.swift => TimeTrend/TimeTrendDetailView.swift} (89%) rename CubeTime/Stats/{ScrollableLineChart.swift => TimeTrend/TimeTrendViewController.swift} (80%) diff --git a/CubeTime.xcodeproj/project.pbxproj b/CubeTime.xcodeproj/project.pbxproj index 80759d8..7b13e92 100644 --- a/CubeTime.xcodeproj/project.pbxproj +++ b/CubeTime.xcodeproj/project.pbxproj @@ -27,7 +27,7 @@ 239560A629C3056E00735035 /* SplashPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 239560A529C3056E00735035 /* SplashPad.storyboard */; }; 2397131F2791032300268DFA /* AveragePhases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2397131E2791032300268DFA /* AveragePhases.swift */; }; 23A1CDBD29399FE000F0895D /* SwiftfulLoadingIndicators in Frameworks */ = {isa = PBXBuildFile; productRef = 23A1CDBC29399FE000F0895D /* SwiftfulLoadingIndicators */; }; - 23AAF99629B0219200C5C1FF /* ScrollableLineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AAF99529B0219200C5C1FF /* ScrollableLineChart.swift */; }; + 23AAF99629B0219200C5C1FF /* TimeTrendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AAF99529B0219200C5C1FF /* TimeTrendViewController.swift */; }; 23AAF99829B072F900C5C1FF /* StatsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AAF99729B072F900C5C1FF /* StatsDetailView.swift */; }; 23AB648429385D310049993B /* NewSessionRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AB648329385D310049993B /* NewSessionRootView.swift */; }; 23AB648C293867EA0049993B /* 12sec-audio.wav in Resources */ = {isa = PBXBuildFile; fileRef = 23AB648A293867EA0049993B /* 12sec-audio.wav */; }; @@ -60,7 +60,7 @@ 23E81E73275D8DEA00B741E3 /* AboutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E81E72275D8DEA00B741E3 /* AboutSettings.swift */; }; 23EB8A1E2779B6D3000F6663 /* TimeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EB8A1D2779B6D3000F6663 /* TimeBar.swift */; }; 23F4113829ADDA77002C4E23 /* ToolsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F4113729ADDA77002C4E23 /* ToolsViewModel.swift */; }; - 23F445812B46552D002B7B81 /* TimeTrendDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F445802B46552D002B7B81 /* TimeTrendDetail.swift */; }; + 23F445812B46552D002B7B81 /* TimeTrendDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F445802B46552D002B7B81 /* TimeTrendDetailView.swift */; }; 7D2B805E279422BD0006AEDD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2B805D279422BD0006AEDD /* AppDelegate.swift */; }; 7D3D3154278D3CA400BBDF55 /* TimerTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3D3153278D3CA400BBDF55 /* TimerTouchView.swift */; }; 7D6159F92751B53E00DC9A4C /* TimeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D6159F82751B53E00DC9A4C /* TimeCard.swift */; }; @@ -133,7 +133,7 @@ 238FB41F29B8175C008EC1D4 /* GradientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientManager.swift; sourceTree = ""; }; 239560A529C3056E00735035 /* SplashPad.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SplashPad.storyboard; sourceTree = ""; }; 2397131E2791032300268DFA /* AveragePhases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AveragePhases.swift; sourceTree = ""; }; - 23AAF99529B0219200C5C1FF /* ScrollableLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLineChart.swift; sourceTree = ""; }; + 23AAF99529B0219200C5C1FF /* TimeTrendViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTrendViewController.swift; sourceTree = ""; }; 23AAF99729B072F900C5C1FF /* StatsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDetailView.swift; sourceTree = ""; }; 23AB648329385D310049993B /* NewSessionRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewSessionRootView.swift; sourceTree = ""; }; 23AB648A293867EA0049993B /* 12sec-audio.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "12sec-audio.wav"; sourceTree = ""; }; @@ -170,7 +170,7 @@ 23E81E72275D8DEA00B741E3 /* AboutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettings.swift; sourceTree = ""; }; 23EB8A1D2779B6D3000F6663 /* TimeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeBar.swift; sourceTree = ""; }; 23F4113729ADDA77002C4E23 /* ToolsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolsViewModel.swift; sourceTree = ""; }; - 23F445802B46552D002B7B81 /* TimeTrendDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTrendDetail.swift; sourceTree = ""; }; + 23F445802B46552D002B7B81 /* TimeTrendDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTrendDetailView.swift; sourceTree = ""; }; 73B7136529BA3FF00064E02F /* CubeTime.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CubeTime.entitlements; sourceTree = ""; }; 7D2B805D279422BD0006AEDD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7D3D3153278D3CA400BBDF55 /* TimerTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerTouchView.swift; sourceTree = ""; }; @@ -241,6 +241,16 @@ path = Fonts; sourceTree = ""; }; + 23418F5D2BB2C39700398EE2 /* TimeTrend */ = { + isa = PBXGroup; + children = ( + 2319F8B8276872F200644EB3 /* TimeTrend.swift */, + 23F445802B46552D002B7B81 /* TimeTrendDetailView.swift */, + 23AAF99529B0219200C5C1FF /* TimeTrendViewController.swift */, + ); + path = TimeTrend; + sourceTree = ""; + }; 235D6A6429C2BEFA002D90D8 /* Helper */ = { isa = PBXGroup; children = ( @@ -464,9 +474,7 @@ 7D7A9EBF274F38CD00C2E125 /* StatsView.swift */, 23C85EAB29388C9C00C7C3EC /* StatsSubviews.swift */, 23AAF99729B072F900C5C1FF /* StatsDetailView.swift */, - 2319F8B8276872F200644EB3 /* TimeTrend.swift */, - 23F445802B46552D002B7B81 /* TimeTrendDetail.swift */, - 23AAF99529B0219200C5C1FF /* ScrollableLineChart.swift */, + 23418F5D2BB2C39700398EE2 /* TimeTrend */, 23E647C52775B2AC00561780 /* TimeDistribution.swift */, 23CA845F2783EBA1003901FB /* ReachedTargets.swift */, 2397131E2791032300268DFA /* AveragePhases.swift */, @@ -649,7 +657,7 @@ 231827E729B5E86B00A40C78 /* FloatingPanel.swift in Sources */, 23CA8A0829AAF96C006676BF /* StopwatchManager+Stats.swift in Sources */, 23DE3BE4274DDBE300254D29 /* Persistence.swift in Sources */, - 23AAF99629B0219200C5C1FF /* ScrollableLineChart.swift in Sources */, + 23AAF99629B0219200C5C1FF /* TimeTrendViewController.swift in Sources */, 7D3D3154278D3CA400BBDF55 /* TimerTouchView.swift in Sources */, 231F37C52B81FD290016A137 /* ImportExport.swift in Sources */, E5A559602B83456F004093B3 /* ExportViewModel.swift in Sources */, @@ -663,7 +671,7 @@ 23EB8A1E2779B6D3000F6663 /* TimeBar.swift in Sources */, AF8874C129AAC8C0001E49D0 /* ToolsList.swift in Sources */, 235D6A6929C2C0D3002D90D8 /* HelperShare.swift in Sources */, - 23F445812B46552D002B7B81 /* TimeTrendDetail.swift in Sources */, + 23F445812B46552D002B7B81 /* TimeTrendDetailView.swift in Sources */, AF8B305B29B5C7010023D23B /* SettingsManager.swift in Sources */, AF8A157429AF15B500F6818E /* ScrambleOnlyTool.swift in Sources */, 2359689327A367A0003ED9E1 /* StopwatchManager.swift in Sources */, diff --git a/CubeTime/Sessions/SessionCard.swift b/CubeTime/Sessions/SessionCard.swift index 784b80f..56d6813 100644 --- a/CubeTime/Sessions/SessionCard.swift +++ b/CubeTime/Sessions/SessionCard.swift @@ -181,7 +181,7 @@ struct SessionCard: View { .tint(Color("accent")) } - .confirmationDialog(String("Are you sure you want to delete \"\(self.item.name)\"? All solves will be deleted and this cannot be undone."), isPresented: $isShowingDeleteDialog, titleVisibility: .visible) { + .confirmationDialog(String("Are you sure you want to delete \"\(self.item.name ?? "this session")\"? All solves will be deleted and this cannot be undone."), isPresented: $isShowingDeleteDialog, titleVisibility: .visible) { Button("Confirm", role: .destructive) { if item == stopwatchManager.currentSession { var next: Session? = nil diff --git a/CubeTime/Stats/StatsDetailView.swift b/CubeTime/Stats/StatsDetailView.swift index fbb14f7..6e94c10 100644 --- a/CubeTime/Stats/StatsDetailView.swift +++ b/CubeTime/Stats/StatsDetailView.swift @@ -111,12 +111,11 @@ struct StatsDetailView: View { .frame(width: 16, height: 16) } else { - Text(PUZZLE_TYPES[Int(session.scrambleType)].name) - Image(PUZZLE_TYPES[Int(session.scrambleType)].name) .resizable() .frame(width: 16, height: 16) + Text(PUZZLE_TYPES[Int(session.scrambleType)].name) } } diff --git a/CubeTime/Stats/TimeTrend.swift b/CubeTime/Stats/TimeTrend/TimeTrend.swift similarity index 100% rename from CubeTime/Stats/TimeTrend.swift rename to CubeTime/Stats/TimeTrend/TimeTrend.swift diff --git a/CubeTime/Stats/TimeTrendDetail.swift b/CubeTime/Stats/TimeTrend/TimeTrendDetailView.swift similarity index 89% rename from CubeTime/Stats/TimeTrendDetail.swift rename to CubeTime/Stats/TimeTrend/TimeTrendDetailView.swift index 26d1be2..f0522b2 100644 --- a/CubeTime/Stats/TimeTrendDetail.swift +++ b/CubeTime/Stats/TimeTrend/TimeTrendDetailView.swift @@ -102,7 +102,7 @@ struct TimeTrendDetail: View { } - #warning("TODO URGENT FIX: READD COLOURFORBUTTONTYPE") +#warning("TODO URGENT FIX: READD COLOURFORBUTTONTYPE") LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 36, maximum: 100), spacing: 16), count: 3), alignment: .leading) { ForEach(self.labels, id: \.0) { label, type in LegendLabel(colour: .accentColor, label: label) @@ -114,10 +114,10 @@ struct TimeTrendDetail: View { GeometryReader { proxy in if proxy.size.height > 0 { - DetailTimeTrendBase(rawDataPoints: stopwatchManager.solvesByDate, - limits: (stopwatchManager.solvesByDate.min(by: { $0.timeIncPen < $1.timeIncPen })!.timeIncPen, stopwatchManager.solvesByDate.max(by: { $0.timeIncPen < $1.timeIncPen })!.timeIncPen), - averageValue: 5, interval: interval, - proxy: proxy) + TimeTrendViewRepresentable(rawDataPoints: stopwatchManager.solvesByDate, + limits: (stopwatchManager.solvesByDate.min(by: { $0.timeIncPen < $1.timeIncPen })!.timeIncPen, stopwatchManager.solvesByDate.max(by: { $0.timeIncPen < $1.timeIncPen })!.timeIncPen), + averageValue: 5, interval: interval, + proxy: proxy) } } .padding(.top, 8) diff --git a/CubeTime/Stats/ScrollableLineChart.swift b/CubeTime/Stats/TimeTrend/TimeTrendViewController.swift similarity index 80% rename from CubeTime/Stats/ScrollableLineChart.swift rename to CubeTime/Stats/TimeTrend/TimeTrendViewController.swift index 8a537b1..21e91e4 100644 --- a/CubeTime/Stats/ScrollableLineChart.swift +++ b/CubeTime/Stats/TimeTrend/TimeTrendViewController.swift @@ -4,21 +4,82 @@ // // Created by Tim Xie on 2/03/23. // + import UIKit import SwiftUI -fileprivate let CHART_BOTTOM_PADDING: CGFloat = 50 // Allow for x axis +fileprivate let CHART_BOTTOM_PADDING: CGFloat = 50 // Allow for x axis fileprivate let CHART_TOP_PADDING: CGFloat = 100 -fileprivate let axisLabelFont = FontManager.fontFor(size: 10, weight: 400) +fileprivate let AXIS_LABEL_FONT = FontManager.fontFor(size: 10, weight: 400) + +fileprivate let DOT_DIAMETER: CGFloat = 6 // possible to modify for accessibility purposes + + +extension CGPoint { + static func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { + return CGPoint(x: (p1.x + p2.x) / 2, + y: (p1.y + p2.y) / 2) + } + + static func controlPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { + var controlPoint = CGPoint.midPointForPoints(p1: p1, p2: p2) + + let diffY = abs(p2.y - controlPoint.y) + + if (p1.y < p2.y){ + controlPoint.y += diffY + } else if (p1.y > p2.y) { + controlPoint.y -= diffY + } + + return controlPoint + } +} + -class HighlightedPoint: UIView { +struct TimeTrendPoint { + var min: CGFloat + var max: CGFloat + + var idx: Int + + var solve: Solve + + init(solve: Solve, + idx: Int, + min: Double, max: Double) { + self.solve = solve + self.idx = idx + self.min = min + self.max = max + } + + + func getPointFor(interval: Int, imageHeight: CGFloat) -> CGPoint { + return CGPoint(x: CGFloat(interval * self.idx), + y: imageHeight - (((CGFloat(solve.timeIncPen) - min) / (max - min)) * (imageHeight - 2) + 1)) + } + + + func pointIn(interval: Int, imageHeight: CGFloat, other: CGPoint) -> Bool { + let point = getPointFor(interval: interval, imageHeight: imageHeight) + let rect = CGRect(x: point.x - DOT_DIAMETER / 2, y: point.y - DOT_DIAMETER / 2, width: DOT_DIAMETER, height: DOT_DIAMETER) + return rect.contains(other) + } +} + + + + +// MARK: - HOVER VIEWS +class TimeTrendHoverPointView: UIView { let path = UIBezierPath(ovalIn: CGRect(x: 2, y: 2, width: 8, height: 8)) var isRegular = true { - didSet { + didSet { self.setNeedsDisplay() } } @@ -43,72 +104,141 @@ class HighlightedPoint: UIView { } -private let dotDiameter: CGFloat = 6 - -struct LineChartPoint { - var min: CGFloat - var max: CGFloat - var idx: Int +class TimeTrendHoverCardView: UIStackView { + var solve: Solve? + lazy var iconView: UIImageView = { + var iconView = UIImageView() + + iconView = UIImageView(image: UIImage(named: PUZZLE_TYPES[Int(solve?.scrambleType ?? 0)].name)) + iconView.frame = CGRect(x: 0, y: 0, width: 24, height: 24) + iconView.tintColor = UIColor(Color("dark")) + iconView.translatesAutoresizingMaskIntoConstraints = false + + return iconView + }() - func getPointFor(interval: Int, imageHeight: CGFloat) -> CGPoint { - return CGPoint(x: CGFloat(interval * self.idx), y: getStandardisedYLocation(value: solve.timeIncPen, - min: min, max: max, - imageHeight: imageHeight)) - } + lazy var chevron: UIImageView = { + var chevron = UIImageView() + + chevron = UIImageView(image: UIImage(systemName: "chevron.right")) + chevron.tintColor = UIColor(Color("dark")) + + chevron.preferredSymbolConfiguration = UIImage.SymbolConfiguration(font: .preferredFont(for: .footnote, weight: .medium)) + chevron.translatesAutoresizingMaskIntoConstraints = false + + return chevron + + }() - var solve: Solve + lazy var timeLabel: UILabel = { + var timeLabel = UILabel() + + timeLabel.text = self.solve?.timeText ?? "" + timeLabel.font = FontManager.fontFor(size: 15, weight: 600) + timeLabel.adjustsFontForContentSizeCategory = true + + return timeLabel + }() - init(solve: Solve, - idx: Int, - min: Double, max: Double) { + lazy var dateLabel: UILabel = { + var dateLabel = UILabel() + + if let date = self.solve?.date { + dateLabel.text = getSolveDateFormatter(date).string(from: date) + } else { + dateLabel.text = "Unknown Date" + } + + dateLabel.font = .preferredFont(forTextStyle: .caption1) + dateLabel.textColor = UIColor(Color("grey")) + dateLabel.adjustsFontForContentSizeCategory = true + + return dateLabel + }() + + lazy var infoStack: UIStackView = { + var infoStack = UIStackView(frame: .zero) + + infoStack.axis = .vertical + infoStack.alignment = .leading + infoStack.distribution = .fill + infoStack.spacing = -2 + infoStack.addArrangedSubview(self.timeLabel) + infoStack.addArrangedSubview(self.dateLabel) + infoStack.translatesAutoresizingMaskIntoConstraints = false + + return infoStack + }() + + init(solve: Solve?) { self.solve = solve - self.idx = idx - self.min = min - self.max = max + + super.init(frame: .zero) + + self.setupCard() + + + self.addArrangedSubview(self.iconView) + self.addArrangedSubview(self.infoStack) + self.addArrangedSubview(self.chevron) + + + + NSLayoutConstraint.activate([ + self.heightAnchor.constraint(equalToConstant: 44), + + self.iconView.widthAnchor.constraint(equalToConstant: 24), + self.iconView.heightAnchor.constraint(equalTo: self.iconView.widthAnchor), + ]) } - func pointIn(interval: Int, imageHeight: CGFloat, other: CGPoint) -> Bool { - let point = getPointFor(interval: interval, imageHeight: imageHeight) - let rect = CGRect(x: point.x - dotDiameter / 2, y: point.y - dotDiameter / 2, width: dotDiameter, height: dotDiameter) - return rect.contains(other) - } -} - -func getStandardisedYLocation(value: Double, - min: Double, max: Double, - imageHeight: CGFloat) -> CGFloat { - return imageHeight - (((value - min) / (max - min)) * (imageHeight - 2) + 1) -} - -extension CGPoint { - static func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { - return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2) + required init(coder: NSCoder) { + fatalError("not implemented") } - static func controlPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { - var controlPoint = CGPoint.midPointForPoints(p1: p1, p2: p2) + private func setupCard() { + self.layer.cornerRadius = 6 + self.layer.cornerCurve = .continuous + self.backgroundColor = UIColor(Color("overlay0")) - let diffY = abs(p2.y - controlPoint.y) + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowOpacity = 0.04 + self.layer.shadowRadius = 6 + self.layer.shadowOffset = CGSize(width: 0.0, height: -2.0) - if (p1.y < p2.y){ - controlPoint.y += diffY - } else if (p1.y > p2.y) { - controlPoint.y -= diffY - } + self.translatesAutoresizingMaskIntoConstraints = false - return controlPoint + self.distribution = .fill + self.alignment = .center + self.spacing = 10 + + self.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) + self.isLayoutMarginsRelativeArrangement = true + + self.setCustomSpacing(16, after: self.infoStack) + } + + func updateLabel(with solve: Solve) { + self.timeLabel.text = solve.timeText + + if let date = solve.date { + self.dateLabel.text = getSolveDateFormatter(date).string(from: date) + } else { + self.dateLabel.text = "Unknown Date" + } } } -class LineChartScroll: UIScrollView { + +class TimeTrendLineChartScrollView: UIScrollView { static private let dotSize: CGFloat = 6 var interval: Int - var points: [LineChartPoint] - + var points: [TimeTrendPoint] + - init(frame: CGRect, interval: Int, points: [LineChartPoint]) { + init(frame: CGRect, interval: Int, points: [TimeTrendPoint]) { self.interval = interval self.points = points @@ -119,7 +249,7 @@ class LineChartScroll: UIScrollView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func createDNFPoint() -> UIImage { let config = UIImage.SymbolConfiguration(font: .preferredFont(for: .caption1, weight: .bold), scale: .large) @@ -130,9 +260,6 @@ class LineChartScroll: UIScrollView { static let redrawDistance = 20 override func draw(_ rect: CGRect) { - print("drawing: interval: \(interval), rect: \(rect)") - - var dnfedIndices: [Int] = [] /// draw line @@ -142,7 +269,7 @@ class LineChartScroll: UIScrollView { let context = UIGraphicsGetCurrentContext()! let xAxis = UIBezierPath() - + let leftX = rect.minX let rightX = leftX + rect.width @@ -163,7 +290,6 @@ class LineChartScroll: UIScrollView { trendLine.lineJoinStyle = .round - print("DRAWING FROM \(leftX) to \(rightX)") let pointsSubset = points[max(Int(leftX) / interval - 1, 0)...min(Int(rightX) / interval + 1, points.count - 1)] @@ -188,7 +314,7 @@ class LineChartScroll: UIScrollView { let string = "\(i + 1)" as NSString let attributes = [ - NSAttributedString.Key.font : axisLabelFont, + NSAttributedString.Key.font : AXIS_LABEL_FONT, NSAttributedString.Key.foregroundColor : UIColor(Color("grey")) ] @@ -211,12 +337,12 @@ class LineChartScroll: UIScrollView { intervalLine.addLine(to: CGPoint(x: CGFloat(i * interval), y: CHART_TOP_PADDING + 0.5)) context.setStrokeColor(UIColor(Color("indent1")).cgColor) intervalLine.stroke() - + // String drawing changes color context.restoreGState() } - + if (trendLine.isEmpty) { trendLine.move(to: CGPoint(x: 0, y: curcgpoint.y)) @@ -229,7 +355,7 @@ class LineChartScroll: UIScrollView { trendLine.addQuadCurve(to: mid, controlPoint: CGPoint.controlPointForPoints(p1: mid, p2: prevcgpoint)) gradientLine.addQuadCurve(to: mid, - controlPoint: CGPoint.controlPointForPoints(p1: mid, p2: prevcgpoint)) + controlPoint: CGPoint.controlPointForPoints(p1: mid, p2: prevcgpoint)) if Penalty(rawValue: cur.solve.penalty) == .dnf { @@ -248,7 +374,7 @@ class LineChartScroll: UIScrollView { trendLine.addQuadCurve(to: curcgpoint, controlPoint: CGPoint.controlPointForPoints(p1: mid, p2: curcgpoint)) gradientLine.addQuadCurve(to: curcgpoint, - controlPoint: CGPoint.controlPointForPoints(p1: mid, p2: curcgpoint)) + controlPoint: CGPoint.controlPointForPoints(p1: mid, p2: curcgpoint)) if Penalty(rawValue: cur.solve.penalty) == .dnf { dnfedIndices.append(i) @@ -294,7 +420,7 @@ class LineChartScroll: UIScrollView { let imageRect = CGRect(x: cgpoint.x - 4, y: cgpoint.y - 4, width: 8, height: 8) context.clip(to: imageRect, mask: image.cgImage!) - + context.addRect(imageRect) context.drawPath(using: .fill) @@ -304,139 +430,10 @@ class LineChartScroll: UIScrollView { } -class TimeDistributionPointCard: UIStackView { - var solve: Solve? - - lazy var iconView: UIImageView = { - var iconView = UIImageView() - - iconView = UIImageView(image: UIImage(named: PUZZLE_TYPES[Int(solve?.scrambleType ?? 0)].name)) - iconView.frame = CGRect(x: 0, y: 0, width: 24, height: 24) - iconView.tintColor = UIColor(Color("dark")) - iconView.translatesAutoresizingMaskIntoConstraints = false - - return iconView - }() - - lazy var chevron: UIImageView = { - var chevron = UIImageView() - - chevron = UIImageView(image: UIImage(systemName: "chevron.right")) - chevron.tintColor = UIColor(Color("dark")) - - chevron.preferredSymbolConfiguration = UIImage.SymbolConfiguration(font: .preferredFont(for: .footnote, weight: .medium)) - chevron.translatesAutoresizingMaskIntoConstraints = false - - return chevron - - }() - - lazy var timeLabel: UILabel = { - var timeLabel = UILabel() - - timeLabel.text = self.solve?.timeText ?? "" - timeLabel.font = FontManager.fontFor(size: 15, weight: 600) - timeLabel.adjustsFontForContentSizeCategory = true - - return timeLabel - }() - - lazy var dateLabel: UILabel = { - var dateLabel = UILabel() - - if let date = self.solve?.date { - dateLabel.text = getSolveDateFormatter(date).string(from: date) - } else { - dateLabel.text = "Unknown Date" - } - - dateLabel.font = .preferredFont(forTextStyle: .caption1) - dateLabel.textColor = UIColor(Color("grey")) - dateLabel.adjustsFontForContentSizeCategory = true - - return dateLabel - }() - - lazy var infoStack: UIStackView = { - var infoStack = UIStackView(frame: .zero) - - infoStack.axis = .vertical - infoStack.alignment = .leading - infoStack.distribution = .fill - infoStack.spacing = -2 - infoStack.addArrangedSubview(self.timeLabel) - infoStack.addArrangedSubview(self.dateLabel) - infoStack.translatesAutoresizingMaskIntoConstraints = false - - return infoStack - }() - - init(solve: Solve?) { - self.solve = solve - - super.init(frame: .zero) - - self.setupCard() - - - self.addArrangedSubview(self.iconView) - self.addArrangedSubview(self.infoStack) - self.addArrangedSubview(self.chevron) - - - - NSLayoutConstraint.activate([ - self.heightAnchor.constraint(equalToConstant: 44), - - self.iconView.widthAnchor.constraint(equalToConstant: 24), - self.iconView.heightAnchor.constraint(equalTo: self.iconView.widthAnchor), - ]) - } - - required init(coder: NSCoder) { - fatalError("not implemented") - } - - private func setupCard() { - self.layer.cornerRadius = 6 - self.layer.cornerCurve = .continuous - self.backgroundColor = UIColor(Color("overlay0")) - - self.layer.shadowColor = UIColor.black.cgColor - self.layer.shadowOpacity = 0.04 - self.layer.shadowRadius = 6 - self.layer.shadowOffset = CGSize(width: 0.0, height: -2.0) - - self.translatesAutoresizingMaskIntoConstraints = false - - self.distribution = .fill - self.alignment = .center - self.spacing = 10 - - self.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) - self.isLayoutMarginsRelativeArrangement = true - - self.setCustomSpacing(16, after: self.infoStack) - } - - func updateLabel(with solve: Solve) { - self.timeLabel.text = solve.timeText - - if let date = solve.date { - self.dateLabel.text = getSolveDateFormatter(date).string(from: date) - } else { - self.dateLabel.text = "Unknown Date" - } - } -} - - - -class TimeDistViewController: UIViewController, UIScrollViewDelegate { - var points: [LineChartPoint] +class TimeTrendViewController: UIViewController, UIScrollViewDelegate { + var points: [TimeTrendPoint] var interval: Int { didSet { - print("gap delta did set") self.drawYAxisValues() self.scrollView.interval = interval recalculateScrollViewSize() @@ -450,23 +447,21 @@ class TimeDistViewController: UIViewController, UIScrollViewDelegate { let limits: (min: Double, max: Double) -var scrollView: LineChartScroll! - var highlightedPoint: HighlightedPoint! - var highlightedCard: TimeDistributionPointCard! - + var scrollView: TimeTrendLineChartScrollView! + var highlightedPoint: TimeTrendHoverPointView! + var highlightedCard: TimeTrendHoverCardView! + var yAxis: UIView! var imageWidthConstraint: NSLayoutConstraint! var lastSelectedSolve: Solve! - // let crossView: CGPath! // TODO: the crosses for DNFs that is drawn (copy) - private let dotSize: CGFloat = 6 - init(stopwatchManager: StopwatchManager, points: [LineChartPoint], interval: Int, averageValue: Double, limits: (min: Double, max: Double)) { + init(stopwatchManager: StopwatchManager, points: [TimeTrendPoint], interval: Int, averageValue: Double, limits: (min: Double, max: Double)) { self.stopwatchManager = stopwatchManager - self.points = points + self.points = points self.interval = interval self.averageValue = averageValue self.limits = limits @@ -481,7 +476,7 @@ var scrollView: LineChartScroll! override func viewDidLoad() { super.viewDidLoad() - self.scrollView = LineChartScroll(frame: .zero, interval: interval, points: points) + self.scrollView = TimeTrendLineChartScrollView(frame: .zero, interval: interval, points: points) self.scrollView.showsHorizontalScrollIndicator = true self.view.clipsToBounds = true @@ -502,12 +497,9 @@ var scrollView: LineChartScroll! scrollView.addGestureRecognizer(longPressGestureRecognizer) - self.highlightedPoint = HighlightedPoint(at: .zero) + self.highlightedPoint = TimeTrendHoverPointView(at: .zero) self.highlightedPoint.backgroundColor = .clear -// self.highlightedPoint.frame = CGRect(x: self.points[1].point.x - 6, -// y: self.points[1].point.y - 6, -// width: 12, height: 12) self.highlightedPoint.frame = CGRect(x: 0, y: 0, @@ -518,7 +510,7 @@ var scrollView: LineChartScroll! self.scrollView.isUserInteractionEnabled = true - self.highlightedCard = TimeDistributionPointCard(solve: nil) + self.highlightedCard = TimeTrendHoverCardView(solve: nil) let cardTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(solveCardTapped)) self.highlightedCard.addGestureRecognizer(cardTapGestureRecognizer) self.highlightedCard.layer.opacity = 1 @@ -538,19 +530,8 @@ var scrollView: LineChartScroll! self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), ]) - NSLayoutConstraint.activate([ -// self.chartView.topAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.topAnchor, constant: 100), -// self.chartView.bottomAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.bottomAnchor), -// self.chartView.bottomAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.bottomAnchor), -// -// self.chartView.leadingAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.leadingAnchor), -// -// self.chartView.trailingAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.trailingAnchor), -// -// self.chartView.widthAnchor.constraint(equalToConstant: CGFloat((points.count - 1) * interval)) - ]) recalculateScrollViewSize() - + } func recalculateScrollViewSize() { @@ -587,7 +568,7 @@ var scrollView: LineChartScroll! let label = UILabel(frame: .zero) label.translatesAutoresizingMaskIntoConstraints = false label.adjustsFontSizeToFitWidth = true - label.font = axisLabelFont + label.font = AXIS_LABEL_FONT label.textColor = UIColor(Color("grey")) label.text = formatSolveTime(secs: self.limits.min + Double(i) * range / Double(5), dp: 2) @@ -617,7 +598,7 @@ var scrollView: LineChartScroll! return view } - func updateGap(interval: Int, points: [LineChartPoint]) { + func updateGap(interval: Int, points: [TimeTrendPoint]) { self.points = points self.interval = interval @@ -646,25 +627,20 @@ var scrollView: LineChartScroll! var closestCGPoint = closestPoint.getPointFor(interval: interval, imageHeight: self.scrollView.frame.height - CHART_TOP_PADDING - CHART_BOTTOM_PADDING) closestCGPoint.y += CHART_TOP_PADDING - print("CLOSETS: \(closestCGPoint.y), HEIGHT: \(self.scrollView.frame.height)") - self.highlightedCard.updateLabel(with: closestPoint.solve) self.highlightedPoint.isRegular = Penalty(rawValue: closestPoint.solve.penalty) != .dnf -// self.highlightedPoint.frame.origin = chartView.convert(CGPoint(x: closestCGPoint.x - 6, -// y: closestCGPoint.y - 6), to: scrollView) - self.highlightedPoint.frame.origin = CGPoint(x: closestCGPoint.x - 6, y: closestCGPoint.y - 6) - + self.lastSelectedSolve = closestPoint.solve - + self.highlightedCard.frame.origin = CGPoint( x: min(max(self.scrollView.contentOffset.x, - closestCGPoint.x - (self.highlightedCard.frame.width / 2)), - self.scrollView.frame.width - self.highlightedCard.frame.width + self.scrollView.contentOffset.x), - y: closestCGPoint.y - 80) + closestCGPoint.x - (self.highlightedCard.frame.width / 2)), + self.scrollView.frame.width - self.highlightedCard.frame.width + self.scrollView.contentOffset.x), + y: closestCGPoint.y - 80) self.highlightedPoint.layer.opacity = 1 self.highlightedCard.layer.opacity = 1 @@ -679,18 +655,18 @@ var scrollView: LineChartScroll! @objc func solveCardTapped(_ g: UITapGestureRecognizer) { let solveSheet = UIHostingController(rootView: TimeDetailView(for: self.lastSelectedSolve, currentSolve: .constant(self.lastSelectedSolve)).environmentObject(stopwatchManager)) - #warning("BUG: the toolbar doesn't display") +#warning("BUG: the toolbar doesn't display") self.present(solveSheet, animated: true, completion: { self.removeSelectedPoint() }) } } -struct DetailTimeTrendBase: UIViewControllerRepresentable { - typealias UIViewControllerType = TimeDistViewController +struct TimeTrendViewRepresentable: UIViewControllerRepresentable { + typealias UIViewControllerType = TimeTrendViewController @EnvironmentObject var stopwatchManager: StopwatchManager - let points: [LineChartPoint] + let points: [TimeTrendPoint] let interval: Int let averageValue: Double let proxy: GeometryProxy @@ -698,9 +674,8 @@ struct DetailTimeTrendBase: UIViewControllerRepresentable { let limits: (min: Double, max: Double) init(rawDataPoints: [Solve], limits: (min: Double, max: Double), averageValue: Double, interval: Int, proxy: GeometryProxy) { - print("HEIGHT IN INIT: \(proxy.size.height)") self.points = rawDataPoints.enumerated().map({ (i, e) in - return LineChartPoint(solve: e, + return TimeTrendPoint(solve: e, idx: i, min: limits.min, max: limits.max) }) @@ -710,38 +685,13 @@ struct DetailTimeTrendBase: UIViewControllerRepresentable { self.proxy = proxy } - func makeUIViewController(context: Context) -> TimeDistViewController { - print("HEIGHT IN makeUIViewController: \(proxy.size.height)") - let timeDistViewController = TimeDistViewController(stopwatchManager: stopwatchManager, points: points, interval: interval, averageValue: averageValue, limits: limits) + func makeUIViewController(context: Context) -> TimeTrendViewController { + let timeDistViewController = TimeTrendViewController(stopwatchManager: stopwatchManager, points: points, interval: interval, averageValue: averageValue, limits: limits) return timeDistViewController } - func updateUIViewController(_ uiViewController: TimeDistViewController, context: Context) { + func updateUIViewController(_ uiViewController: TimeTrendViewController, context: Context) { uiViewController.updateGap(interval: interval, points: points) } } - - -#Preview { - let session = Session(context: PersistenceController.preview.container.viewContext) - session.name = "temp" - session.lastUsed = Date() - session.pinned = false - session.scrambleType = Int32(0) - session.sessionType = Int16(0) - - for i in 0 ..< 10 { - let solve = Solve(context: PersistenceController.preview.container.viewContext) - solve.time = Double.random(in: 0..<10) - solve.date = Date() - solve.session = session - solve.scrambleType = Int32(0) - solve.penalty = Int16(0) - } - - let stopwatchManager = StopwatchManager(currentSession: session, managedObjectContext: PersistenceController.preview.container.viewContext) - - return TimeTrendDetail().environmentObject(stopwatchManager) - -} diff --git a/CubeTime/TimeList/TimeDetailView.swift b/CubeTime/TimeList/TimeDetailView.swift index db78cde..391c221 100644 --- a/CubeTime/TimeList/TimeDetailView.swift +++ b/CubeTime/TimeList/TimeDetailView.swift @@ -113,11 +113,11 @@ struct TimeDetailView: View { HStack { HStack(alignment: .center, spacing: 4) { - Text(puzzleType.name) - Image(puzzleType.name) .resizable() .frame(width: 16, height: 16) + + Text(puzzleType.name) } Text("|") diff --git a/CubeTime/Timer/TimerHeader.swift b/CubeTime/Timer/TimerHeader.swift index 6729326..1032816 100644 --- a/CubeTime/Timer/TimerHeader.swift +++ b/CubeTime/Timer/TimerHeader.swift @@ -60,7 +60,7 @@ struct TimerHeader: View { } } - Text("PREVIEW") + Text("Preview") .font(.system(size: 17, weight: .medium)) .padding(.trailing) }