Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GraphUI",
module_name = "GraphUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AppBundle:AppBundle",
"//submodules/GraphCore:GraphCore",
],
visibility = [
"//visibility:public",
],
)
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>
@@ -0,0 +1,226 @@
//
// ChartDetailsView.swift
// GraphTest
//
// Created by Andrew Solovey on 14/03/2019.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import GraphCore
import AppBundle
private let cornerRadius: CGFloat = 5
private let verticalMargins: CGFloat = 8
private var labelHeight: CGFloat = 18
private var labelSpacing: CGFloat = 2
private var margin: CGFloat = 10
private var prefixLabelWidth: CGFloat = 29
private var textLabelWidth: CGFloat = 110
private var valueLabelWidth: CGFloat = 70
class ChartDetailsView: UIControl {
let titleLabel = UILabel()
let arrowView = UIImageView()
let activityIndicator = UIActivityIndicatorView()
let arrowButton = UIButton()
var prefixViews: [UILabel] = []
var labelsViews: [UILabel] = []
var valuesViews: [UILabel] = []
private var viewModel: ChartDetailsViewModel?
private var textHeight: CGFloat?
private var theme: ChartTheme = ChartTheme.defaultDayTheme
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = cornerRadius
clipsToBounds = true
addTarget(self, action: #selector(didTapWhole), for: .touchUpInside)
titleLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold)
arrowView.image = UIImage(bundleImageName: "Chart/arrow_right")
arrowView.contentMode = .scaleAspectFill
arrowButton.addTarget(self, action: #selector(didTap), for: .touchUpInside)
addSubview(titleLabel)
addSubview(arrowView)
addSubview(arrowButton)
addSubview(activityIndicator)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(viewModel: ChartDetailsViewModel, animated: Bool) {
self.viewModel = viewModel
titleLabel.setText(viewModel.title, animated: false)
titleLabel.setVisible(!viewModel.title.isEmpty, animated: false)
arrowView.setVisible(viewModel.showArrow && !viewModel.isLoading, animated: false)
arrowButton.isUserInteractionEnabled = viewModel.showArrow && !viewModel.isLoading
self.isEnabled = !viewModel.isLoading
if viewModel.isLoading {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
} else {
activityIndicator.isHidden = true
activityIndicator.stopAnimating()
}
let width: CGFloat = margin * 2 + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + textLabelWidth + valueLabelWidth
var y: CGFloat = verticalMargins
if (!viewModel.title.isEmpty || viewModel.showArrow) {
titleLabel.frame = CGRect(x: margin, y: y, width: width, height: labelHeight)
arrowView.frame = CGRect(x: width - 6 - margin, y: margin + 2, width: 6, height: 10)
activityIndicator.transform = CGAffineTransform(scaleX: 0.65, y: 0.65)
activityIndicator.center = CGPoint(x: width - 3 - margin, y: 16.0)
y += labelHeight
}
let labelsCount: Int = viewModel.values.count + ((viewModel.totalValue == nil) ? 0 : 1)
setLabelsCount(array: &prefixViews,
count: viewModel.showPrefixes ? labelsCount : 0,
font: UIFont.systemFont(ofSize: 12, weight: .bold))
setLabelsCount(array: &labelsViews,
count: labelsCount,
font: UIFont.systemFont(ofSize: 12, weight: .regular),
textAlignment: .left)
setLabelsCount(array: &valuesViews,
count: labelsCount,
font: UIFont.systemFont(ofSize: 12, weight: .bold))
var textHeight: CGFloat = 0.0
UIView.perform(animated: animated, animations: {
for (index, value) in viewModel.values.enumerated() {
var x: CGFloat = margin
if viewModel.showPrefixes {
let prefixLabel = self.prefixViews[index]
prefixLabel.textColor = self.theme.chartDetailsTextColor
prefixLabel.setText(value.prefix, animated: false)
prefixLabel.frame = CGRect(x: x, y: y, width: prefixLabelWidth, height: labelHeight)
x += prefixLabelWidth + margin
prefixLabel.alpha = value.visible ? 1 : 0
}
let titleLabel = self.labelsViews[index]
titleLabel.setTextColor(self.theme.chartDetailsTextColor, animated: false)
titleLabel.setText(value.title, animated: false)
var titleSize = titleLabel.sizeThatFits(CGSize(width: textLabelWidth, height: CGFloat.greatestFiniteMagnitude))
titleSize.height = ceil(titleSize.height)
titleLabel.frame = CGRect(x: x, y: y + labelSpacing, width: titleSize.width, height: titleSize.height)
titleLabel.alpha = value.visible ? 1 : 0
x += textLabelWidth
let valueLabel = self.valuesViews[index]
valueLabel.setTextColor(value.color, animated: false)
valueLabel.setText(value.value, animated: false)
valueLabel.frame = CGRect(x: x, y: y, width: valueLabelWidth, height: labelHeight)
valueLabel.alpha = value.visible ? 1 : 0
if value.visible {
y += titleSize.height + labelSpacing * 2.0
textHeight += titleSize.height + labelSpacing * 2.0
}
}
if let value = viewModel.totalValue {
var x: CGFloat = margin
if viewModel.showPrefixes {
let prefixLabel = self.prefixViews[viewModel.values.count]
prefixLabel.textColor = self.theme.chartDetailsTextColor
prefixLabel.setText(value.prefix, animated: false)
prefixLabel.frame = CGRect(x: x, y: y, width: prefixLabelWidth, height: labelHeight)
prefixLabel.alpha = value.visible ? 1 : 0
x += prefixLabelWidth + margin
}
let titleLabel = self.labelsViews[viewModel.values.count]
titleLabel.setTextColor(self.theme.chartDetailsTextColor, animated: false)
titleLabel.setText(value.title, animated: false)
titleLabel.frame = CGRect(x: x, y: y, width: textLabelWidth, height: labelHeight)
titleLabel.alpha = value.visible ? 1 : 0
x += textLabelWidth
let valueLabel = self.valuesViews[viewModel.values.count]
valueLabel.setTextColor(self.theme.chartDetailsTextColor, animated: false)
valueLabel.setText(value.value, animated: false)
valueLabel.frame = CGRect(x: x, y: y, width: valueLabelWidth, height: labelHeight)
valueLabel.alpha = value.visible ? 1 : 0
}
})
self.textHeight = textHeight
arrowButton.frame = CGRect(x: 0.0, y: 0.0, width: width, height: y)
}
override var intrinsicContentSize: CGSize {
if let viewModel = viewModel {
var height = ((!viewModel.title.isEmpty || viewModel.showArrow) ? labelHeight : 0) +
(viewModel.totalValue?.visible == true ? labelHeight : 0) +
verticalMargins * 2
if let textHeight = textHeight {
height += textHeight
}
let width: CGFloat = margin * 2 +
(viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) +
textLabelWidth +
valueLabelWidth
return CGSize(width: width,
height: height)
} else {
return CGSize(width: 140,
height: labelHeight + verticalMargins)
}
}
@objc private func didTap() {
viewModel?.tapAction?()
}
@objc private func didTapWhole() {
viewModel?.hideAction?()
}
func setLabelsCount(array: inout [UILabel],
count: Int,
font: UIFont,
textAlignment: NSTextAlignment = .right) {
while array.count > count {
let subview = array.removeLast()
subview.removeFromSuperview()
}
while array.count < count {
let label = UILabel()
label.numberOfLines = 2
label.lineBreakMode = .byWordWrapping
label.font = font
label.textAlignment = textAlignment
addSubview(label)
array.append(label)
}
}
}
extension ChartDetailsView: ChartThemeContainer {
func apply(theme: ChartTheme, strings: ChartStrings, animated: Bool) {
self.theme = theme
self.titleLabel.setTextColor(theme.chartDetailsTextColor, animated: animated)
if let viewModel = self.viewModel {
self.setup(viewModel: viewModel, animated: animated)
}
UIView.perform(animated: animated) {
self.arrowView.tintColor = theme.chartDetailsArrowColor
self.activityIndicator.color = theme.chartDetailsArrowColor
self.backgroundColor = theme.chartDetailsViewColor
}
}
}
+205
View File
@@ -0,0 +1,205 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import AsyncDisplayKit
import AppBundle
import GraphCore
import TelegramPresentationData
public enum ChartType {
case lines
case twoAxis
case pie
case area
case bars
case step
case twoAxisStep
case hourlyStep
case twoAxisHourlyStep
case twoAxis5MinStep
case currency
case stars
}
public extension ChartTheme {
convenience init(presentationTheme: PresentationTheme) {
let rangeViewFrameColor = presentationTheme.chart.rangeViewFrameColor
let rangeViewMarkerColor = presentationTheme.chart.rangeViewMarkerColor
let rangeImage = generateImage(CGSize(width: 114.0, height: 42.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(rangeViewFrameColor.cgColor)
var path = UIBezierPath.init(roundedRect: CGRect(x: 0.0, y: 0.0, width: 11.0, height: 42.0), byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: 6.0, height: 6.0))
context.addPath(path.cgPath)
context.fillPath()
path = UIBezierPath.init(roundedRect: CGRect(x: 103.0, y: 0.0, width: 11.0, height: 42.0), byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSize(width: 6.0, height: 6.0))
context.addPath(path.cgPath)
context.fillPath()
context.setFillColor(rangeViewFrameColor.cgColor)
context.fill(CGRect(x: 7.0, y: 0.0, width: 4.0, height: 1.0))
context.fill(CGRect(x: 7.0, y: 41.0, width: 4.0, height: 1.0))
context.fill(CGRect(x: 100.0, y: 0.0, width: 4.0, height: 1.0))
context.fill(CGRect(x: 100.0, y: 41.0, width: 4.0, height: 1.0))
context.fill(CGRect(x: 11.0, y: 0.0, width: 92.0, height: 1.0))
context.fill(CGRect(x: 11.0, y: 41.0, width: 92.0, height: 1.0))
context.setLineCap(.round)
context.setLineWidth(1.5)
context.setStrokeColor(rangeViewMarkerColor.cgColor)
context.move(to: CGPoint(x: 7.0, y: 17.0))
context.addLine(to: CGPoint(x: 4.0, y: 21.0))
context.addLine(to: CGPoint(x: 7.0, y: 25.0))
context.strokePath()
context.move(to: CGPoint(x: 107.0, y: 17.0))
context.addLine(to: CGPoint(x: 110.0, y: 21.0))
context.addLine(to: CGPoint(x: 107.0, y: 25.0))
context.strokePath()
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 15.0, left: 11.0, bottom: 15.0, right: 11.0), resizingMode: .stretch)
self.init(chartTitleColor: presentationTheme.list.itemPrimaryTextColor, actionButtonColor: presentationTheme.list.itemAccentColor, chartBackgroundColor: presentationTheme.list.itemBlocksBackgroundColor, chartLabelsColor: presentationTheme.chart.labelsColor, chartHelperLinesColor: presentationTheme.chart.helperLinesColor, chartStrongLinesColor: presentationTheme.chart.strongLinesColor, barChartStrongLinesColor: presentationTheme.chart.barStrongLinesColor, chartDetailsTextColor: presentationTheme.chart.detailsTextColor, chartDetailsArrowColor: presentationTheme.chart.detailsArrowColor, chartDetailsViewColor: presentationTheme.chart.detailsViewColor, rangeViewFrameColor: rangeViewFrameColor, rangeViewTintColor: presentationTheme.list.blocksBackgroundColor.withAlphaComponent(0.5), rangeViewMarkerColor: rangeViewMarkerColor, rangeCropImage: rangeImage)
}
}
public func createChartController(_ data: String, type: ChartType, rate: Double = 1.0, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) -> BaseChartController? {
var resultController: BaseChartController?
if let data = data.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
let controller: BaseChartController
switch type {
case .lines:
controller = GeneralLinesChartController(chartsCollection: collection)
controller.isZoomable = false
case .twoAxis:
controller = TwoAxisLinesChartController(chartsCollection: collection)
controller.isZoomable = false
case .pie:
controller = PercentPieChartController(chartsCollection: collection, initiallyZoomed: true)
case .area:
controller = PercentPieChartController(chartsCollection: collection, initiallyZoomed: false)
case .bars:
controller = StackedBarsChartController(chartsCollection: collection)
controller.isZoomable = false
case .currency:
var iconCache: [UInt32: UIImage] = [:]
controller = StackedBarsChartController(chartsCollection: collection, currency: .ton, drawCurrency: { context, color, point in
let icon: UIImage?
if let current = iconCache[color.rgb] {
icon = current
} else if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/Ton"), color: color) {
icon = generateImage(image.size, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
}
})
iconCache[color.rgb] = icon
} else {
icon = nil
}
if let icon, let cgImage = icon.cgImage {
context.draw(cgImage, in: CGRect(origin: point.offsetBy(dx: 0.0, dy: -2.0), size: icon.size), byTiling: false)
}
}, rate: rate)
controller.isZoomable = false
case .stars:
var icon: UIImage?
if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") {
icon = generateImage(CGSize(width: floor(image.size.width * 0.82), height: floor(image.size.width * 0.82)), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
}
})
}
controller = StackedBarsChartController(chartsCollection: collection, currency: .xtr, drawCurrency: { context, color, point in
if let icon, let cgImage = icon.cgImage {
context.draw(cgImage, in: CGRect(origin: point.offsetBy(dx: -3.0, dy: -4.0), size: icon.size), byTiling: false)
}
}, rate: rate)
controller.isZoomable = false
case .step:
controller = StepBarsChartController(chartsCollection: collection)
case .twoAxisStep:
controller = TwoAxisStepBarsChartController(chartsCollection: collection)
case .hourlyStep:
controller = StepBarsChartController(chartsCollection: collection, hourly: true)
controller.isZoomable = false
case .twoAxisHourlyStep:
let stepController = TwoAxisStepBarsChartController(chartsCollection: collection)
stepController.hourly = true
controller = stepController
controller.isZoomable = false
case .twoAxis5MinStep:
let stepController = TwoAxisStepBarsChartController(chartsCollection: collection)
stepController.min5 = true
controller = stepController
controller.isZoomable = false
}
controller.getDetailsData = { date, completion in
getDetailsData(date, { detailsData in
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
Queue.mainQueue().async {
completion(collection)
}
}) { error in
completion(nil)
}
} else {
completion(nil)
}
})
}
resultController = controller
}) { error in
}
}
return resultController
}
public final class ChartNode: ASDisplayNode {
private var chartView: ChartStackSection {
return self.view as! ChartStackSection
}
public override init() {
super.init()
self.setViewBlock({
return ChartStackSection()
})
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func setup(theme: ChartTheme, strings: ChartStrings) {
self.chartView.apply(theme: theme, strings: strings, animated: false)
}
public func setup(controller: BaseChartController, noInitialZoom: Bool = false) {
var displayRange = true
var zoomToEnding = true
if let controller = controller as? StepBarsChartController {
displayRange = !controller.hourly
}
if noInitialZoom {
zoomToEnding = false
}
self.chartView.setup(controller: controller, displayRange: displayRange, zoomToEnding: zoomToEnding)
}
public func resetInteraction() {
self.chartView.resetDetailsView()
}
}
@@ -0,0 +1,255 @@
//
// ChartStackSection.swift
// GraphTest
//
// Created by Andrei Salavei on 4/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import GraphCore
import Display
private enum Constants {
static let chartViewHeightFraction: CGFloat = 0.55
}
private class LeftAlignedIconButton: UIButton {
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
var titleRect = super.titleRect(forContentRect: contentRect)
let imageSize = currentImage?.size ?? .zero
titleRect.origin.x = imageSize.width
return titleRect
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
var imageRect = super.imageRect(forContentRect: contentRect)
imageRect.origin.x = 0.0
return imageRect
}
}
class ChartStackSection: UIView, ChartThemeContainer {
var chartView: ChartView
var rangeView: RangeChartView
var visibilityView: ChartVisibilityView
var sectionContainerView: UIView
var titleLabel: UILabel!
var backButton: UIButton!
var controller: BaseChartController?
var theme: ChartTheme?
var strings: ChartStrings?
var displayRange: Bool = true
let hapticFeedback = HapticFeedback()
init() {
sectionContainerView = UIView()
chartView = ChartView()
rangeView = RangeChartView()
visibilityView = ChartVisibilityView()
titleLabel = UILabel()
backButton = LeftAlignedIconButton()
super.init(frame: CGRect())
self.addSubview(sectionContainerView)
sectionContainerView.addSubview(chartView)
sectionContainerView.addSubview(rangeView)
sectionContainerView.addSubview(visibilityView)
sectionContainerView.addSubview(titleLabel)
sectionContainerView.addSubview(backButton)
titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold)
titleLabel.textAlignment = .center
visibilityView.clipsToBounds = true
backButton.isExclusiveTouch = true
backButton.addTarget(self, action: #selector(self.didTapBackButton), for: .touchUpInside)
backButton.setTitle("Zoom Out", for: .normal)
backButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
backButton.setTitleColor(UIColor(rgb: 0x0088ff), for: .normal)
backButton.setImage(UIImage(bundleImageName: "Chart/arrow_left"), for: .normal)
backButton.imageEdgeInsets = UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 3.0)
backButton.imageView?.tintColor = UIColor(rgb: 0x0088ff)
backButton.adjustsImageWhenHighlighted = false
backButton.setVisible(false, animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold)
visibilityView.clipsToBounds = true
backButton.isExclusiveTouch = true
backButton.setVisible(false, animated: false)
}
public func resetDetailsView() {
controller?.cancelChartInteraction()
}
func apply(theme: ChartTheme, strings: ChartStrings, animated: Bool) {
self.theme = theme
self.strings = strings
self.backButton.setTitle(strings.zoomOut, for: .normal)
UIView.perform(animated: animated && self.isVisibleInWindow) {
self.sectionContainerView.backgroundColor = theme.chartBackgroundColor
self.rangeView.backgroundColor = theme.chartBackgroundColor
self.visibilityView.backgroundColor = theme.chartBackgroundColor
self.backButton.tintColor = theme.actionButtonColor
self.backButton.setTitleColor(theme.actionButtonColor, for: .normal)
self.backButton.imageView?.tintColor = theme.actionButtonColor
}
if rangeView.isVisibleInWindow || chartView.isVisibleInWindow {
chartView.loadDetailsViewIfNeeded()
chartView.apply(theme: theme, strings: strings, animated: animated && chartView.isVisibleInWindow)
controller?.apply(theme: theme, strings: strings, animated: animated)
rangeView.apply(theme: theme, strings: strings, animated: animated && rangeView.isVisibleInWindow)
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval.random(in: 0...0.1)) {
self.chartView.loadDetailsViewIfNeeded()
self.controller?.apply(theme: theme, strings: strings, animated: false)
self.chartView.apply(theme: theme, strings: strings, animated: false)
self.rangeView.apply(theme: theme, strings: strings, animated: false)
}
}
self.titleLabel.setTextColor(theme.chartTitleColor, animated: animated && titleLabel.isVisibleInWindow)
}
@objc private func didTapBackButton() {
self.controller?.didTapZoomOut()
}
func setBackButtonVisible(_ visible: Bool, animated: Bool) {
backButton.setVisible(visible, animated: animated)
layoutIfNeeded(animated: animated)
}
func updateToolViews(animated: Bool) {
guard let controller = self.controller else {
return
}
rangeView.setRange(controller.currentChartHorizontalRangeFraction, animated: animated)
rangeView.setRangePaging(enabled: controller.isChartRangePagingEnabled,
minimumSize: controller.minimumSelectedChartRange)
visibilityView.setVisible(controller.drawChartVisibity, animated: animated)
if controller.drawChartVisibity {
visibilityView.isExpanded = true
visibilityView.items = controller.actualChartsCollection.chartValues.map { value in
return ChartVisibilityItem(title: value.name, color: value.color)
}
visibilityView.setItemsSelection(controller.actualChartVisibility)
visibilityView.setNeedsLayout()
visibilityView.layoutIfNeeded()
} else {
visibilityView.isExpanded = false
}
superview?.superview?.layoutIfNeeded(animated: animated)
}
override func layoutSubviews() {
super.layoutSubviews()
let bounds = self.bounds
self.titleLabel.frame = CGRect(origin: CGPoint(x: backButton.alpha > 0.0 ? 36.0 : 0.0, y: 5.0), size: CGSize(width: bounds.width, height: 28.0))
self.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 750.0))
self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 310.0))
self.rangeView.isHidden = !self.displayRange
self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 310.0), size: CGSize(width: bounds.width, height: 42.0))
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.displayRange ? 368.0 : 326.0), size: CGSize(width: bounds.width, height: 350.0))
self.backButton.frame = CGRect(x: 8.0, y: 0.0, width: 96.0, height: 38.0)
self.chartView.setNeedsDisplay()
}
func setup(controller: BaseChartController, displayRange: Bool = true, zoomToEnding: Bool = true) {
self.controller = controller
self.displayRange = displayRange
if let theme = self.theme, let strings = self.strings {
controller.apply(theme: theme, strings: strings, animated: false)
}
self.chartView.renderers = controller.mainChartRenderers
self.chartView.userDidSelectCoordinateClosure = { [unowned self] point in
self.controller?.chartInteractionDidBegin(point: point)
}
self.chartView.userDidDeselectCoordinateClosure = { [unowned self] in
self.controller?.chartInteractionDidEnd()
}
controller.cartViewBounds = { [unowned self] in
return self.chartView.bounds
}
controller.chartFrame = { [unowned self] in
return self.chartView.chartFrame
}
controller.setDetailsViewModel = { [unowned self] viewModel, animated, feedback in
self.chartView.setDetailsViewModel(viewModel: viewModel, animated: animated)
if feedback {
self.hapticFeedback.tap()
}
}
controller.setDetailsChartVisibleClosure = { [unowned self] visible, animated in
self.chartView.setDetailsChartVisible(visible, animated: animated)
}
controller.setDetailsViewPositionClosure = { [unowned self] position in
self.chartView.detailsViewPosition = position
}
controller.setChartTitleClosure = { [unowned self] title, animated in
self.titleLabel.setText(title, animated: animated)
}
controller.setBackButtonVisibilityClosure = { [unowned self] visible, animated in
self.setNeedsLayout()
self.setBackButtonVisible(visible, animated: animated)
}
controller.refreshChartToolsClosure = { [unowned self] animated in
self.updateToolViews(animated: animated)
}
self.rangeView.chartView.renderers = controller.navigationRenderers
self.rangeView.rangeDidChangeClosure = { range in
controller.updateChartRange(range)
}
self.rangeView.touchedOutsideClosure = {
controller.cancelChartInteraction()
}
controller.chartRangeUpdatedClosure = { [unowned self] (range, animated) in
self.rangeView.setRange(range, animated: animated)
}
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
self.rangeView.setRangePaging(enabled: isEnabled, minimumSize: pageSize)
}
self.visibilityView.selectionCallbackClosure = { [unowned self] visibility in
self.controller?.updateChartsVisibility(visibility: visibility, animated: true)
}
controller.initializeChart()
updateToolViews(animated: false)
let range: ClosedRange<CGFloat> = displayRange && zoomToEnding ? 0.8 ... 1.0 : 0.0 ... 1.0
rangeView.setRange(range, animated: false)
controller.updateChartRange(range, animated: false)
self.setNeedsLayout()
}
}
+171
View File
@@ -0,0 +1,171 @@
//
// ChartView.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import GraphCore
class ChartView: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
var chartInsets: UIEdgeInsets = UIEdgeInsets(top: 40, left: 16, bottom: 35, right: 16) {
didSet {
setNeedsDisplay()
}
}
var renderers: [ChartViewRenderer] = [] {
willSet {
renderers.forEach { $0.containerViews.removeAll(where: { $0.value == self || $0.value == nil }) }
}
didSet {
renderers.forEach { $0.containerViews.append(ContainerViewReference(value: self)) }
setNeedsDisplay()
}
}
var chartFrame: CGRect {
let chartBound = self.bounds
return CGRect(x: chartInsets.left,
y: chartInsets.top,
width: max(1, chartBound.width - chartInsets.left - chartInsets.right),
height: max(1, chartBound.height - chartInsets.top - chartInsets.bottom))
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let chartBounds = self.bounds
let chartFrame = self.chartFrame
for renderer in renderers {
renderer.render(context: context, bounds: chartBounds, chartFrame: chartFrame)
}
}
var userDidSelectCoordinateClosure: ((CGPoint) -> Void)?
var userDidDeselectCoordinateClosure: (() -> Void)?
private var _isTracking: Bool = false
private var touchInitialLocation: CGPoint?
override var isTracking: Bool {
return self._isTracking
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let point = touches.first?.location(in: self) {
let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
y: (point.y - chartFrame.origin.y) / chartFrame.height)
userDidSelectCoordinateClosure?(fractionPoint)
self.touchInitialLocation = point
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if var point = touches.first?.location(in: self) {
point.x = max(0.0, min(self.frame.width, point.x))
point.y = max(0.0, min(self.frame.height, point.y))
let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
y: (point.y - chartFrame.origin.y) / chartFrame.height)
userDidSelectCoordinateClosure?(fractionPoint)
if let initialPosition = self.touchInitialLocation, abs(initialPosition.x - point.x) > 3.0 {
self._isTracking = true
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
userDidDeselectCoordinateClosure?()
self.touchInitialLocation = nil
self._isTracking = false
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
userDidDeselectCoordinateClosure?()
self.touchInitialLocation = nil
self._isTracking = false
}
// MARK: Details View
private var detailsView: ChartDetailsView!
private var maxDetailsViewWidth: CGFloat = 0
func loadDetailsViewIfNeeded() {
if detailsView == nil {
let detailsView = ChartDetailsView(frame: bounds)
addSubview(detailsView)
detailsView.alpha = 0
self.detailsView = detailsView
}
}
private var detailsTableTopOffset: CGFloat = 5
private var detailsTableLeftOffset: CGFloat = 8
private var isDetailsViewVisible: Bool = false
var detailsViewPosition: CGFloat = 0 {
didSet {
loadDetailsViewIfNeeded()
let detailsViewSize = detailsView.intrinsicContentSize
maxDetailsViewWidth = max(maxDetailsViewWidth, detailsViewSize.width)
if maxDetailsViewWidth + detailsTableLeftOffset > detailsViewPosition {
detailsView.frame = CGRect(x: max(detailsTableLeftOffset, min(detailsViewPosition + detailsTableLeftOffset, bounds.width - maxDetailsViewWidth - detailsTableLeftOffset)),
y: chartInsets.top + detailsTableTopOffset,
width: maxDetailsViewWidth,
height: detailsViewSize.height)
} else {
detailsView.frame = CGRect(x: max(detailsTableLeftOffset, min(detailsViewPosition - maxDetailsViewWidth - detailsTableLeftOffset, bounds.width - maxDetailsViewWidth - detailsTableLeftOffset)),
y: chartInsets.top + detailsTableTopOffset,
width: maxDetailsViewWidth,
height: detailsViewSize.height)
}
}
}
func setDetailsChartVisible(_ visible: Bool, animated: Bool) {
guard isDetailsViewVisible != visible else {
return
}
isDetailsViewVisible = visible
loadDetailsViewIfNeeded()
detailsView.setVisible(visible, animated: animated)
if !visible {
maxDetailsViewWidth = 0
}
}
func setDetailsViewModel(viewModel: ChartDetailsViewModel, animated: Bool) {
loadDetailsViewIfNeeded()
detailsView.setup(viewModel: viewModel, animated: animated)
UIView.perform(animated: animated, animations: {
let position = self.detailsViewPosition
self.detailsViewPosition = position
})
}
func setupView() {
backgroundColor = .clear
layer.drawsAsynchronously = true
}
}
extension ChartView: ChartThemeContainer {
func apply(theme: ChartTheme, strings: ChartStrings, animated: Bool) {
detailsView?.apply(theme: theme, strings: strings, animated: animated && (detailsView?.isVisibleInWindow ?? false))
}
}
@@ -0,0 +1,96 @@
//
// ChartVisibilityItemCell.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import GraphCore
class ChartVisibilityItemView: UIView {
static let textFont = UIFont.systemFont(ofSize: 14, weight: .medium)
let checkButton: UIButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
setupView()
}
func setupView() {
checkButton.frame = bounds
checkButton.titleLabel?.font = ChartVisibilityItemView.textFont
checkButton.layer.cornerRadius = 15
checkButton.layer.masksToBounds = true
checkButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
let pressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didRecognizedLongPress(recognizer:)))
pressRecognizer.cancelsTouchesInView = true
checkButton.addGestureRecognizer(pressRecognizer)
addSubview(checkButton)
}
var tapClosure: (() -> Void)?
var longTapClosure: (() -> Void)?
private func updateStyle(animated: Bool) {
guard let item = item else {
return
}
UIView.perform(animated: animated, animations: {
if self.isChecked {
self.checkButton.setTitleColor(.white, for: .normal)
self.checkButton.backgroundColor = item.color
self.checkButton.layer.borderColor = nil
self.checkButton.layer.borderWidth = 0
self.checkButton.setTitle("" + item.title, for: .normal)
} else {
self.checkButton.backgroundColor = .clear
self.checkButton.layer.borderColor = item.color.cgColor
self.checkButton.layer.borderWidth = 1
self.checkButton.setTitleColor(item.color, for: .normal)
self.checkButton.setTitle(item.title, for: .normal)
}
})
}
override func layoutSubviews() {
super.layoutSubviews()
checkButton.frame = bounds
}
@objc private func didTapButton() {
tapClosure?()
}
@objc private func didRecognizedLongPress(recognizer: UIGestureRecognizer) {
if recognizer.state == .began {
longTapClosure?()
}
}
var item: ChartVisibilityItem? = nil {
didSet {
updateStyle(animated: false)
}
}
private(set) var isChecked: Bool = true
func setChecked(isChecked: Bool, animated: Bool) {
self.isChecked = isChecked
updateStyle(animated: true)
}
}
@@ -0,0 +1,157 @@
//
// ChartVisibilityView.swift
// GraphTest
//
// Created by Andrei Salavei on 4/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import GraphCore
import Display
private enum Constants {
static let itemHeight: CGFloat = 30
static let itemSpacing: CGFloat = 8
static let labelTextApproxInsets: CGFloat = 40
static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
}
public func calculateVisiblityHeight(width: CGFloat, items: [ChartVisibilityItem]) -> CGFloat {
let frames = generateItemsFrames(frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)), items: items)
guard let lastFrame = frames.last else { return .zero }
return lastFrame.maxY + Constants.insets.bottom
}
private func generateItemsFrames(frame: CGRect, items: [ChartVisibilityItem]) -> [CGRect] {
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
var frames: [CGRect] = []
for item in items {
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
} else if previousPoint.x <= Constants.insets.left {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
} else {
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
previousPoint.x = Constants.insets.left
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
}
previousPoint.x += width + Constants.itemSpacing
}
return frames
}
class ChartVisibilityView: UIView {
var items: [ChartVisibilityItem] = [] {
didSet {
selectedItems = items.map { _ in true }
while selectionViews.count > selectedItems.count {
selectionViews.last?.removeFromSuperview()
selectionViews.removeLast()
}
while selectionViews.count < selectedItems.count {
let view = ChartVisibilityItemView(frame: bounds)
addSubview(view)
selectionViews.append(view)
}
for (index, item) in items.enumerated() {
let view = selectionViews[index]
view.item = item
view.tapClosure = { [weak self, weak view] in
guard let self = self else { return }
let selected = !self.selectedItems[index]
let selectedItemsCount = self.selectedItems.filter { $0 }.count
if selectedItemsCount == 1 && !selected {
view?.layer.addShakeAnimation()
} else {
self.setItemSelected(selected, at: index, animated: true)
self.notifyItemSelection()
}
}
view.longTapClosure = { [weak self] in
guard let self = self else { return }
let hasSelectedItem = self.selectedItems.enumerated().contains(where: { $0.element && $0.offset != index })
if hasSelectedItem {
for (itemIndex, _) in self.items.enumerated() {
self.setItemSelected(itemIndex == index, at: itemIndex, animated: true)
}
} else {
for (itemIndex, _) in self.items.enumerated() {
self.setItemSelected(true, at: itemIndex, animated: true)
}
}
self.notifyItemSelection()
}
}
}
}
private(set) var selectedItems: [Bool] = []
var isExpanded: Bool = true {
didSet {
invalidateIntrinsicContentSize()
setNeedsUpdateConstraints()
}
}
private var selectionViews: [ChartVisibilityItemView] = []
var selectionCallbackClosure: (([Bool]) -> Void)?
func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) {
self.selectedItems[index] = selected
self.selectionViews[index].setChecked(isChecked: selected, animated: animated)
}
func setItemsSelection(_ selection: [Bool]) {
assert(selection.count == items.count)
self.selectedItems = selection
for (index, selected) in self.selectedItems.enumerated() {
selectionViews[index].setChecked(isChecked: selected, animated: false)
}
}
private func notifyItemSelection() {
selectionCallbackClosure?(selectedItems)
}
override func layoutSubviews() {
super.layoutSubviews()
updateFrames()
}
private func updateFrames() {
for (index, frame) in generateItemsFrames(frame: bounds, items: self.items).enumerated() {
selectionViews[index].frame = frame
}
}
override var intrinsicContentSize: CGSize {
guard isExpanded else {
var size = self.bounds.size
size.height = 0
return size
}
let frames = generateItemsFrames(frame: UIScreen.main.bounds, items: self.items)
guard let lastFrame = frames.last else { return .zero }
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
return size
}
}
extension ChartVisibilityView: ChartThemeContainer {
func apply(theme: ChartTheme, strings: ChartStrings, animated: Bool) {
UIView.perform(animated: animated) {
self.backgroundColor = theme.chartBackgroundColor
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//
// GraphUI.h
// GraphUI
//
// Created by Peter on 8/13/19.
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for GraphUI.
FOUNDATION_EXPORT double GraphUIVersionNumber;
//! Project version string for GraphUI.
FOUNDATION_EXPORT const unsigned char GraphUIVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <GraphUI/PublicHeader.h>
@@ -0,0 +1,327 @@
//
// RangeChartView.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import GraphCore
import AppBundle
private enum Constants {
static let cropIndicatorLineWidth: CGFloat = 1
static let markerSelectionRange: CGFloat = 25
static let defaultMinimumRangeDistance: CGFloat = 0.1
static let titntAreaWidth: CGFloat = 10
static let horizontalContentMargin: CGFloat = 16
static let cornerRadius: CGFloat = 5
}
class RangeChartView: UIControl {
private enum Marker {
case lower
case upper
case center
}
public var lowerBound: CGFloat = 0 {
didSet {
setNeedsLayout()
}
}
public var upperBound: CGFloat = 1 {
didSet {
setNeedsLayout()
}
}
public var selectionColor: UIColor = .blue
public var defaultColor: UIColor = .lightGray
public var minimumRangeDistance: CGFloat = Constants.defaultMinimumRangeDistance
private let lowerBoundTintView = UIView()
private let upperBoundTintView = UIView()
private let cropFrameView = UIImageView()
private var selectedMarker: Marker?
private var selectedMarkerHorizontalOffset: CGFloat = 0
private var selectedMarkerInitialLocation: CGPoint?
private var isBoundCropHighlighted: Bool = false
private var isRangePagingEnabled: Bool = false
private var targetTapLocation: CGPoint?
public let chartView = ChartView()
override init(frame: CGRect) {
super.init(frame: frame)
layoutMargins = UIEdgeInsets(top: Constants.cropIndicatorLineWidth,
left: Constants.horizontalContentMargin,
bottom: Constants.cropIndicatorLineWidth,
right: Constants.horizontalContentMargin)
self.setup()
}
func setup() {
isMultipleTouchEnabled = false
chartView.chartInsets = .zero
chartView.backgroundColor = .clear
addSubview(chartView)
addSubview(lowerBoundTintView)
addSubview(upperBoundTintView)
addSubview(cropFrameView)
cropFrameView.isUserInteractionEnabled = false
chartView.isUserInteractionEnabled = false
lowerBoundTintView.isUserInteractionEnabled = false
upperBoundTintView.isUserInteractionEnabled = false
chartView.layer.cornerRadius = 5
upperBoundTintView.layer.cornerRadius = 5
lowerBoundTintView.layer.cornerRadius = 5
chartView.layer.masksToBounds = true
upperBoundTintView.layer.masksToBounds = true
lowerBoundTintView.layer.masksToBounds = true
layoutViews()
}
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
public var rangeDidChangeClosure: ((ClosedRange<CGFloat>) -> Void)?
public var touchedOutsideClosure: (() -> Void)?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setRangePaging(enabled: Bool, minimumSize: CGFloat) {
isRangePagingEnabled = enabled
minimumRangeDistance = minimumSize
}
func setRange(_ range: ClosedRange<CGFloat>, animated: Bool) {
UIView.perform(animated: animated) {
self.lowerBound = range.lowerBound
self.upperBound = range.upperBound
self.layoutIfNeeded()
}
}
override func layoutSubviews() {
super.layoutSubviews()
layoutViews()
}
override var isEnabled: Bool {
get {
return super.isEnabled
}
set {
if newValue == false {
selectedMarker = nil
}
super.isEnabled = newValue
}
}
// MARK: - Touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled else { return }
guard let point = touches.first?.location(in: self) else { return }
if abs(locationInView(for: upperBound) - point.x + Constants.markerSelectionRange / 2) < Constants.markerSelectionRange {
selectedMarker = .upper
selectedMarkerHorizontalOffset = point.x - locationInView(for: upperBound)
selectedMarkerInitialLocation = point
isBoundCropHighlighted = true
} else if abs(locationInView(for: lowerBound) - point.x - Constants.markerSelectionRange / 2) < Constants.markerSelectionRange {
selectedMarker = .lower
selectedMarkerHorizontalOffset = point.x - locationInView(for: lowerBound)
selectedMarkerInitialLocation = point
isBoundCropHighlighted = true
} else if point.x > locationInView(for: lowerBound) && point.x < locationInView(for: upperBound) {
selectedMarker = .center
selectedMarkerHorizontalOffset = point.x - locationInView(for: lowerBound)
selectedMarkerInitialLocation = point
isBoundCropHighlighted = true
} else {
targetTapLocation = point
selectedMarkerHorizontalOffset = cropFrameView.frame.width / 2.0
selectedMarker = nil
selectedMarkerInitialLocation = nil
return
}
sendActions(for: .touchDown)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled else { return }
guard let selectedMarker = selectedMarker else { return }
guard let point = touches.first?.location(in: self) else { return }
let horizontalPosition = point.x - selectedMarkerHorizontalOffset
let fraction = fractionFor(offsetX: horizontalPosition)
updateMarkerOffset(selectedMarker, fraction: fraction)
if let initialPosition = selectedMarkerInitialLocation, abs(initialPosition.x - point.x) > 3.0 {
self._isTracking = true
}
targetTapLocation = nil
sendActions(for: .valueChanged)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled else { return }
if let point = targetTapLocation {
let horizontalPosition = point.x - selectedMarkerHorizontalOffset
let fraction = fractionFor(offsetX: horizontalPosition)
updateMarkerOffset(.center, fraction: fraction)
sendActions(for: .touchUpInside)
self.targetTapLocation = nil
return
}
guard let selectedMarker = selectedMarker else {
touchedOutsideClosure?()
return
}
guard let point = touches.first?.location(in: self) else { return }
let horizontalPosition = point.x - selectedMarkerHorizontalOffset
let fraction = fractionFor(offsetX: horizontalPosition)
updateMarkerOffset(selectedMarker, fraction: fraction)
self.selectedMarker = nil
self.selectedMarkerInitialLocation = nil
self.isBoundCropHighlighted = false
if bounds.contains(point) {
sendActions(for: .touchUpInside)
} else {
sendActions(for: .touchUpOutside)
}
rangeDidChangeClosure?(lowerBound...upperBound)
self._isTracking = false
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
self.targetTapLocation = nil
self.selectedMarker = nil
self.selectedMarkerInitialLocation = nil
self.isBoundCropHighlighted = false
self._isTracking = false
sendActions(for: .touchCancel)
}
private var _isTracking: Bool = false
override var isTracking: Bool {
return self._isTracking
}
}
private extension RangeChartView {
var contentFrame: CGRect {
return CGRect(x: layoutMargins.right,
y: layoutMargins.top,
width: (bounds.width - layoutMargins.right - layoutMargins.left),
height: bounds.height - layoutMargins.top - layoutMargins.bottom)
}
func locationInView(for fraction: CGFloat) -> CGFloat {
return contentFrame.minX + contentFrame.width * fraction
}
func locationInView(for fraction: Double) -> CGFloat {
return locationInView(for: CGFloat(fraction))
}
func fractionFor(offsetX: CGFloat) -> CGFloat {
guard contentFrame.width > 0 else {
return 0
}
return crop(0, CGFloat((offsetX - contentFrame.minX ) / contentFrame.width), 1)
}
private func updateMarkerOffset(_ marker: Marker, fraction: CGFloat, notifyDelegate: Bool = true) {
let fractionToCount: CGFloat
if isRangePagingEnabled {
guard let minValue = stride(from: CGFloat(0.0), through: CGFloat(1.0), by: minimumRangeDistance).min(by: { abs($0 - fraction) < abs($1 - fraction) }) else { return }
fractionToCount = minValue
} else {
fractionToCount = fraction
}
switch marker {
case .lower:
lowerBound = min(fractionToCount, upperBound - minimumRangeDistance)
case .upper:
upperBound = max(fractionToCount, lowerBound + minimumRangeDistance)
case .center:
let distance = upperBound - lowerBound
lowerBound = max(0, min(fractionToCount, 1 - distance))
upperBound = lowerBound + distance
}
if notifyDelegate {
rangeDidChangeClosure?(lowerBound...upperBound)
}
UIView.animate(withDuration: isRangePagingEnabled ? 0.1 : 0) {
self.layoutIfNeeded()
}
}
// MARK: - Layout
func layoutViews() {
cropFrameView.frame = CGRect(x: locationInView(for: lowerBound),
y: contentFrame.minY - Constants.cropIndicatorLineWidth,
width: locationInView(for: upperBound) - locationInView(for: lowerBound),
height: contentFrame.height + Constants.cropIndicatorLineWidth * 2)
if chartView.frame != contentFrame {
chartView.frame = contentFrame
}
lowerBoundTintView.frame = CGRect(x: contentFrame.minX,
y: contentFrame.minY,
width: max(0, locationInView(for: lowerBound) - contentFrame.minX + Constants.titntAreaWidth),
height: contentFrame.height)
upperBoundTintView.frame = CGRect(x: locationInView(for: upperBound) - Constants.titntAreaWidth,
y: contentFrame.minY,
width: max(0, contentFrame.maxX - locationInView(for: upperBound) + Constants.titntAreaWidth),
height: contentFrame.height)
}
}
extension RangeChartView: ChartThemeContainer {
func apply(theme: ChartTheme, strings: ChartStrings, animated: Bool) {
let closure = {
self.lowerBoundTintView.backgroundColor = theme.rangeViewTintColor
self.upperBoundTintView.backgroundColor = theme.rangeViewTintColor
}
self.cropFrameView.setImage(theme.rangeCropImage, animated: animated)
if animated {
UIView.animate(withDuration: .defaultDuration, animations: closure)
} else {
closure()
}
}
}
@@ -0,0 +1,24 @@
//
// UIImageView+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 4/9/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension UIImageView {
func setImage(_ image: UIImage?, animated: Bool) {
if self.image != image {
if animated {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
animation.type = .fade
animation.duration = .defaultDuration
self.layer.add(animation, forKey: "kCATransitionImageFade")
}
self.image = image
}
}
}
@@ -0,0 +1,37 @@
//
// UILabel+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 4/9/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension UILabel {
func setTextColor(_ color: UIColor, animated: Bool) {
if self.textColor != color {
if animated {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
animation.type = .fade
animation.duration = .defaultDuration
self.layer.add(animation, forKey: "kCATransitionColorFade")
}
self.textColor = color
}
}
func setText(_ title: String?, animated: Bool) {
if self.text != title {
if animated {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
animation.type = .fade
animation.duration = .defaultDuration
self.layer.add(animation, forKey: "kCATransitionTextFade")
}
self.text = title
}
}
}
@@ -0,0 +1,57 @@
//
// UIView+Extensions.swift
// GraphTest
//
// Created by Andrei Salavei on 4/10/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension UIView {
static let oneDevicePixel: CGFloat = (1.0 / max(2, min(1, UIScreen.main.scale)))
}
// MARK: UIView+Animation
public extension UIView {
func bringToFront() {
superview?.bringSubviewToFront(self)
}
func layoutIfNeeded(animated: Bool) {
UIView.perform(animated: animated) {
self.layoutIfNeeded()
}
}
func setVisible(_ visible: Bool, animated: Bool) {
let updatedAlpha: CGFloat = visible ? 1 : 0
if self.alpha != updatedAlpha {
UIView.perform(animated: animated) {
self.alpha = updatedAlpha
}
}
}
static func perform(animated: Bool, animations: @escaping () -> Void) {
perform(animated: animated, animations: animations, completion: { _ in })
}
static func perform(animated: Bool, animations: @escaping () -> Void, completion: @escaping (Bool) -> Void) {
if animated {
UIView.animate(withDuration: .defaultDuration, delay: 0, animations: animations, completion: completion)
} else {
animations()
completion(true)
}
}
var isVisibleInWindow: Bool {
guard let windowBounds = window?.bounds else {
return false
}
let frame = convert(bounds, to: nil)
return frame.intersects(windowBounds)
}
}