mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '97fd1d10f0'
This commit is contained in:
commit
63b85e07a4
173 changed files with 16061 additions and 4252 deletions
BIN
Telegram-iOS/Resources/Compass.tgs
Normal file
BIN
Telegram-iOS/Resources/Compass.tgs
Normal file
Binary file not shown.
BIN
Telegram-iOS/Resources/Dice_1.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_1.tgs
Normal file
Binary file not shown.
BIN
Telegram-iOS/Resources/Dice_2.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_2.tgs
Normal file
Binary file not shown.
BIN
Telegram-iOS/Resources/Dice_3.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_3.tgs
Normal file
Binary file not shown.
BIN
Telegram-iOS/Resources/Dice_4.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_4.tgs
Normal file
Binary file not shown.
BIN
Telegram-iOS/Resources/Dice_5.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_5.tgs
Normal file
Binary file not shown.
BIN
Telegram-iOS/Resources/Dice_6.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_6.tgs
Normal file
Binary file not shown.
|
|
@ -5303,3 +5303,22 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"Appearance.BubbleCorners.Apply" = "Set";
|
||||
|
||||
"Conversation.LiveLocationYouAndOther" = "**You** and %@";
|
||||
|
||||
"PeopleNearby.MakeVisible" = "Make Myself Visible";
|
||||
"PeopleNearby.MakeInvisible" = "Stop Showing Me";
|
||||
|
||||
"PeopleNearby.ShowMorePeople_0" = "Show %@ More People";
|
||||
"PeopleNearby.ShowMorePeople_1" = "Show %@ More People";
|
||||
"PeopleNearby.ShowMorePeople_2" = "Show %@ More People";
|
||||
"PeopleNearby.ShowMorePeople_3_10" = "Show %@ More People";
|
||||
"PeopleNearby.ShowMorePeople_many" = "Show %@ More People";
|
||||
"PeopleNearby.ShowMorePeople_any" = "Show %@ More People";
|
||||
|
||||
"PeopleNearby.VisibleUntil" = "visible until %@";
|
||||
|
||||
"PeopleNearby.MakeVisibleTitle" = "Make Myself Visible";
|
||||
"PeopleNearby.MakeVisibleDescription" = "Allow people nearby to view your profile for 24 hours?\n\nYour phone number will remain hidden.";
|
||||
|
||||
"PeopleNearby.DiscoverDescription" = "Exchange contact info with people nearby\nand find new friends.";
|
||||
|
||||
"Time.TomorrowAt" = "tomorrow at %@";
|
||||
|
|
|
|||
|
|
@ -42,9 +42,15 @@ public enum AnimatedStickerMode {
|
|||
case direct
|
||||
}
|
||||
|
||||
public enum AnimatedStickerPlaybackPosition {
|
||||
case start
|
||||
case end
|
||||
}
|
||||
|
||||
public enum AnimatedStickerPlaybackMode {
|
||||
case once
|
||||
case loop
|
||||
case still(AnimatedStickerPlaybackPosition)
|
||||
}
|
||||
|
||||
private final class AnimatedStickerFrame {
|
||||
|
|
@ -72,6 +78,7 @@ private protocol AnimatedStickerFrameSource: class {
|
|||
var frameCount: Int { get }
|
||||
|
||||
func takeFrame() -> AnimatedStickerFrame?
|
||||
func skipToEnd()
|
||||
}
|
||||
|
||||
private final class AnimatedStickerFrameSourceWrapper {
|
||||
|
|
@ -244,6 +251,9 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
self.data = data
|
||||
self.dataComplete = complete
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
|
|
@ -289,6 +299,10 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
self.currentFrame = self.frameCount - 1
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerFrameQueue {
|
||||
|
|
@ -383,7 +397,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
|
||||
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
||||
|
||||
private var isPlaying: Bool = false
|
||||
public var isPlaying: Bool = false
|
||||
private var canDisplayFirstFrame: Bool = false
|
||||
private var playbackMode: AnimatedStickerPlaybackMode = .loop
|
||||
|
||||
|
|
@ -456,7 +470,9 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
strongSelf.directData = (directData, path, width, height)
|
||||
}
|
||||
if strongSelf.isPlaying {
|
||||
if case let .still(position) = playbackMode {
|
||||
strongSelf.seekTo(position)
|
||||
} else if strongSelf.isPlaying {
|
||||
strongSelf.play()
|
||||
} else if strongSelf.canDisplayFirstFrame {
|
||||
strongSelf.play(firstFrame: true)
|
||||
|
|
@ -597,11 +613,11 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
self.reportedStarted = false
|
||||
self.timer.swap(nil)?.invalidate()
|
||||
if self.playToCompletionOnStop {
|
||||
self.seekToStart()
|
||||
self.seekTo(.start)
|
||||
}
|
||||
}
|
||||
|
||||
public func seekToStart() {
|
||||
public func seekTo(_ position: AnimatedStickerPlaybackPosition) {
|
||||
self.isPlaying = false
|
||||
|
||||
let directData = self.directData
|
||||
|
|
@ -612,6 +628,9 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
if position == .end {
|
||||
maybeFrameSource?.skipToEnd()
|
||||
}
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
|
||||
|
|
|
|||
|
|
@ -59,7 +59,12 @@ public final class CallListController: ViewController {
|
|||
if case .tab = self.mode {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
|
||||
|
||||
let icon = UIImage(bundleImageName: "Chat List/Tabs/IconCalls")
|
||||
let icon: UIImage?
|
||||
if useSpecialTabBarIcons() {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconCalls")
|
||||
} else {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/IconCalls")
|
||||
}
|
||||
self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle
|
||||
self.tabBarItem.image = icon
|
||||
self.tabBarItem.selectedImage = icon
|
||||
|
|
|
|||
31
submodules/Charts/BUCK
Normal file
31
submodules/Charts/BUCK
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "Charts",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/Postbox:Postbox#shared",
|
||||
"//submodules/TelegramCore:TelegramCore#shared",
|
||||
"//submodules/SyncCore:SyncCore#shared",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/AlertUI:AlertUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
],
|
||||
)
|
||||
22
submodules/Charts/Info.plist
Normal file
22
submodules/Charts/Info.plist
Normal 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>
|
||||
258
submodules/Charts/Sources/Chart Screen/ChartDetailsView.swift
Normal file
258
submodules/Charts/Sources/Chart Screen/ChartDetailsView.swift
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
//
|
||||
// ChartDetailsView.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrew Solovey on 14/03/2019.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private let cornerRadius: CGFloat = 5
|
||||
private let verticalMargins: CGFloat = 8
|
||||
private var labelHeight: CGFloat = 18
|
||||
private var margin: CGFloat = 10
|
||||
private var prefixLabelWidth: CGFloat = 27
|
||||
private var textLabelWidth: CGFloat = 60
|
||||
private var valueLabelWidth: CGFloat = 65
|
||||
|
||||
struct ChartDetailsViewModel {
|
||||
struct Value {
|
||||
let prefix: String?
|
||||
let title: String
|
||||
let value: String
|
||||
let color: UIColor
|
||||
let visible: Bool
|
||||
}
|
||||
|
||||
var title: String
|
||||
var showArrow: Bool
|
||||
var showPrefixes: Bool
|
||||
var values: [Value]
|
||||
var totalValue: Value?
|
||||
var tapAction: (() -> Void)?
|
||||
|
||||
static let blank = ChartDetailsViewModel(title: "", showArrow: false, showPrefixes: false, values: [], totalValue: nil, tapAction: nil)
|
||||
}
|
||||
|
||||
class ChartDetailsView: UIControl {
|
||||
let titleLabel = UILabel()
|
||||
let arrowView = UIImageView()
|
||||
|
||||
var prefixViews: [UILabel] = []
|
||||
var labelsViews: [UILabel] = []
|
||||
var valuesViews: [UILabel] = []
|
||||
|
||||
private var viewModel: ChartDetailsViewModel?
|
||||
private var colorMode: ColorMode = .day
|
||||
|
||||
static func fromNib() -> ChartDetailsView {
|
||||
return Bundle.main.loadNibNamed("ChartDetailsView", owner: nil, options: nil)?.first as! ChartDetailsView
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
layer.cornerRadius = cornerRadius
|
||||
clipsToBounds = true
|
||||
|
||||
addTarget(self, action: #selector(didTap), for: .touchUpInside)
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold)
|
||||
arrowView.image = UIImage.arrowRight
|
||||
arrowView.contentMode = .scaleAspectFill
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(arrowView)
|
||||
}
|
||||
|
||||
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: animated)
|
||||
titleLabel.setVisible(!viewModel.title.isEmpty, animated: animated)
|
||||
arrowView.setVisible(viewModel.showArrow, animated: animated)
|
||||
|
||||
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, width: 6, height: 10)
|
||||
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))
|
||||
|
||||
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.colorMode.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.colorMode.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[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 += labelHeight
|
||||
}
|
||||
}
|
||||
if let value = viewModel.totalValue {
|
||||
var x: CGFloat = margin
|
||||
if viewModel.showPrefixes {
|
||||
let prefixLabel = self.prefixViews[viewModel.values.count]
|
||||
prefixLabel.textColor = self.colorMode.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.colorMode.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.colorMode.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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
if let viewModel = viewModel {
|
||||
let height = ((!viewModel.title.isEmpty || viewModel.showArrow) ? labelHeight : 0) +
|
||||
(CGFloat(viewModel.values.filter({ $0.visible }).count) * labelHeight) +
|
||||
(viewModel.totalValue?.visible == true ? labelHeight : 0) +
|
||||
verticalMargins * 2
|
||||
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?()
|
||||
}
|
||||
|
||||
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.font = font
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.textAlignment = textAlignment
|
||||
addSubview(label)
|
||||
array.append(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ChartDetailsView: ColorModeContainer {
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
self.colorMode = colorMode
|
||||
self.titleLabel.setTextColor(colorMode.chartDetailsTextColor, animated: animated)
|
||||
if let viewModel = self.viewModel {
|
||||
self.setup(viewModel: viewModel, animated: animated)
|
||||
}
|
||||
UIView.perform(animated: animated) {
|
||||
self.arrowView.tintColor = colorMode.chartDetailsArrowColor
|
||||
self.backgroundColor = colorMode.chartDetailsViewColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIStackView+removeAllArrangedSubviews
|
||||
public extension UIStackView {
|
||||
func setLabelsCount(_ count: Int,
|
||||
font: UIFont,
|
||||
huggingPriority: UILayoutPriority,
|
||||
textAlignment: NSTextAlignment = .right) {
|
||||
while arrangedSubviews.count > count {
|
||||
let subview = arrangedSubviews.last!
|
||||
removeArrangedSubview(subview)
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
while arrangedSubviews.count < count {
|
||||
let label = UILabel()
|
||||
label.font = font
|
||||
label.textAlignment = textAlignment
|
||||
label.setContentHuggingPriority(huggingPriority, for: .horizontal)
|
||||
label.setContentHuggingPriority(huggingPriority, for: .vertical)
|
||||
label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 999), for: .horizontal)
|
||||
label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 999), for: .vertical)
|
||||
addArrangedSubview(label)
|
||||
}
|
||||
}
|
||||
|
||||
func label(at index: Int) -> UILabel {
|
||||
return arrangedSubviews[index] as! UILabel
|
||||
}
|
||||
|
||||
func removeAllArrangedSubviews() {
|
||||
for subview in arrangedSubviews {
|
||||
removeArrangedSubview(subview)
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIStackView+addArrangedSubviews
|
||||
public extension UIStackView {
|
||||
func addArrangedSubviews(_ views: [UIView]) {
|
||||
views.forEach({ addArrangedSubview($0) })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIStackView+insertArrangedSubviews
|
||||
public extension UIStackView {
|
||||
func insertArrangedSubviews(_ views: [UIView], at index: Int) {
|
||||
views.reversed().forEach({ insertArrangedSubview($0, at: index) })
|
||||
}
|
||||
}
|
||||
199
submodules/Charts/Sources/Chart Screen/ChartStackSection.swift
Normal file
199
submodules/Charts/Sources/Chart Screen/ChartStackSection.swift
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
//
|
||||
// ChartStackSection.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private enum Constants {
|
||||
static let chartViewHeightFraction: CGFloat = 0.55
|
||||
}
|
||||
|
||||
class ChartStackSection: UIView, ColorModeContainer {
|
||||
var chartView: ChartView
|
||||
var rangeView: RangeChartView
|
||||
var visibilityView: ChartVisibilityView
|
||||
var sectionContainerView: UIView
|
||||
var separators: [UIView] = []
|
||||
|
||||
var headerLabel: UILabel!
|
||||
var titleLabel: UILabel!
|
||||
var backButton: UIButton!
|
||||
|
||||
var controller: BaseChartController!
|
||||
|
||||
init() {
|
||||
sectionContainerView = UIView()
|
||||
chartView = ChartView()
|
||||
rangeView = RangeChartView()
|
||||
visibilityView = ChartVisibilityView()
|
||||
headerLabel = UILabel()
|
||||
titleLabel = UILabel()
|
||||
backButton = UIButton()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(sectionContainerView)
|
||||
sectionContainerView.addSubview(chartView)
|
||||
sectionContainerView.addSubview(rangeView)
|
||||
sectionContainerView.addSubview(visibilityView)
|
||||
|
||||
headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold)
|
||||
visibilityView.clipsToBounds = true
|
||||
backButton.isExclusiveTouch = true
|
||||
|
||||
backButton.setVisible(false, animated: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold)
|
||||
visibilityView.clipsToBounds = true
|
||||
backButton.isExclusiveTouch = true
|
||||
|
||||
backButton.setVisible(false, animated: false)
|
||||
}
|
||||
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
UIView.perform(animated: animated && self.isVisibleInWindow) {
|
||||
self.backgroundColor = colorMode.tableBackgroundColor
|
||||
|
||||
self.sectionContainerView.backgroundColor = colorMode.chartBackgroundColor
|
||||
self.rangeView.backgroundColor = colorMode.chartBackgroundColor
|
||||
self.visibilityView.backgroundColor = colorMode.chartBackgroundColor
|
||||
|
||||
self.backButton.tintColor = colorMode.actionButtonColor
|
||||
self.backButton.setTitleColor(colorMode.actionButtonColor, for: .normal)
|
||||
|
||||
for separator in self.separators {
|
||||
separator.backgroundColor = colorMode.tableSeparatorColor
|
||||
}
|
||||
}
|
||||
|
||||
if rangeView.isVisibleInWindow || chartView.isVisibleInWindow {
|
||||
chartView.loadDetailsViewIfNeeded()
|
||||
chartView.apply(colorMode: colorMode, animated: animated && chartView.isVisibleInWindow)
|
||||
controller.apply(colorMode: colorMode, animated: animated)
|
||||
rangeView.apply(colorMode: colorMode, animated: animated && rangeView.isVisibleInWindow)
|
||||
} else {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval.random(in: 0...0.1)) {
|
||||
self.chartView.loadDetailsViewIfNeeded()
|
||||
self.controller.apply(colorMode: colorMode, animated: false)
|
||||
self.chartView.apply(colorMode: colorMode, animated: false)
|
||||
self.rangeView.apply(colorMode: colorMode, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.titleLabel.setTextColor(colorMode.chartTitleColor, animated: animated && titleLabel.isVisibleInWindow)
|
||||
self.headerLabel.setTextColor(colorMode.sectionTitleColor, animated: animated && headerLabel.isVisibleInWindow)
|
||||
}
|
||||
|
||||
@IBAction func didTapBackButton() {
|
||||
controller.didTapZoomOut()
|
||||
}
|
||||
|
||||
func setBackButtonVisible(_ visible: Bool, animated: Bool) {
|
||||
backButton.setVisible(visible, animated: animated)
|
||||
layoutIfNeeded(animated: animated)
|
||||
}
|
||||
|
||||
func updateToolViews(animated: Bool) {
|
||||
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.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 350.0))
|
||||
self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 250.0))
|
||||
self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 250.0), size: CGSize(width: bounds.width, height: 48.0))
|
||||
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: 308.0), size: CGSize(width: bounds.width, height: 122.0))
|
||||
}
|
||||
|
||||
func setup(controller: BaseChartController, title: String) {
|
||||
self.controller = controller
|
||||
self.headerLabel.text = title
|
||||
|
||||
// Chart
|
||||
chartView.renderers = controller.mainChartRenderers
|
||||
chartView.userDidSelectCoordinateClosure = { [unowned self] point in
|
||||
self.controller.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
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 in
|
||||
self.chartView.setDetailsViewModel(viewModel: viewModel, animated: animated)
|
||||
}
|
||||
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.setBackButtonVisible(visible, animated: animated)
|
||||
}
|
||||
controller.refreshChartToolsClosure = { [unowned self] animated in
|
||||
self.updateToolViews(animated: animated)
|
||||
}
|
||||
|
||||
// Range view
|
||||
rangeView.chartView.renderers = controller.navigationRenderers
|
||||
rangeView.rangeDidChangeClosure = { range in
|
||||
controller.updateChartRange(range)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Visibility view
|
||||
visibilityView.selectionCallbackClosure = { [unowned self] visibility in
|
||||
self.controller.updateChartsVisibility(visibility: visibility, animated: true)
|
||||
}
|
||||
|
||||
controller.initializeChart()
|
||||
updateToolViews(animated: false)
|
||||
}
|
||||
}
|
||||
149
submodules/Charts/Sources/Chart Screen/ChartStackSection.xib
Normal file
149
submodules/Charts/Sources/Chart Screen/ChartStackSection.xib
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ChartStackSection" customModule="GraphTest" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="421" height="485"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Pd-im-6vD">
|
||||
<rect key="frame" x="0.0" y="54" width="421" height="430"/>
|
||||
<subviews>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Gs1-ng-qdf" customClass="ChartView" customModule="GraphTest" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="421" height="250"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="910" verticalHuggingPriority="910" horizontalCompressionResistancePriority="960" verticalCompressionResistancePriority="960" contentHorizontalAlignment="left" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gze-ly-E3P">
|
||||
<rect key="frame" x="16" y="0.0" width="100" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="100" id="0fS-5e-eL0"/>
|
||||
<constraint firstAttribute="height" priority="937" constant="38" id="Qll-Xf-A2W"/>
|
||||
<constraint firstAttribute="width" secondItem="Gze-ly-E3P" secondAttribute="height" multiplier="20:8" id="eWA-Ka-ghV"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<inset key="contentEdgeInsets" minX="0.0" minY="0.0" maxX="2" maxY="0.0"/>
|
||||
<state key="normal" title="Zoom Out" image="arrow_left">
|
||||
<color key="titleColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<state key="selected">
|
||||
<color key="titleColor" red="0.0" green="0.47843137250000001" blue="1" alpha="0.38792913732394368" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="didTapBackButton" destination="iN0-l3-epB" eventType="touchUpInside" id="zsN-Ne-pKH"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="800" verticalHuggingPriority="800" horizontalCompressionResistancePriority="850" verticalCompressionResistancePriority="850" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="kGq-zn-SjL">
|
||||
<rect key="frame" x="189.5" y="0.0" width="42" height="38"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="38" id="Ngt-jk-4ri"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Gze-ly-E3P" firstAttribute="leading" secondItem="Gs1-ng-qdf" secondAttribute="leading" constant="16" id="4qc-36-ziS"/>
|
||||
<constraint firstItem="kGq-zn-SjL" firstAttribute="top" secondItem="Gs1-ng-qdf" secondAttribute="top" id="HG2-7K-e3g"/>
|
||||
<constraint firstAttribute="height" constant="250" id="PH2-EY-kJT"/>
|
||||
<constraint firstItem="kGq-zn-SjL" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Gze-ly-E3P" secondAttribute="trailing" constant="4" id="V4X-jJ-C0F"/>
|
||||
<constraint firstItem="kGq-zn-SjL" firstAttribute="centerX" secondItem="Gs1-ng-qdf" secondAttribute="centerX" priority="500" id="g1F-0x-Gyq"/>
|
||||
<constraint firstItem="Gze-ly-E3P" firstAttribute="centerY" secondItem="Gs1-ng-qdf" secondAttribute="top" constant="20" id="rbC-Ih-Hxc"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kGq-zn-SjL" secondAttribute="trailing" priority="999" constant="16" id="wyb-Ls-dPG"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="900" verticalHuggingPriority="900" horizontalCompressionResistancePriority="950" verticalCompressionResistancePriority="950" translatesAutoresizingMaskIntoConstraints="NO" id="7Oh-hd-0e7" customClass="RangeChartView" customModule="GraphTest" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="250" width="421" height="58"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="58" id="XGF-mc-YZQ"/>
|
||||
</constraints>
|
||||
<edgeInsets key="layoutMargins" top="1" left="16" bottom="17" right="16"/>
|
||||
</view>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="900" verticalHuggingPriority="900" horizontalCompressionResistancePriority="950" verticalCompressionResistancePriority="950" translatesAutoresizingMaskIntoConstraints="NO" id="uuw-Jy-472" customClass="ChartVisibilityView" customModule="GraphTest" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="308" width="421" height="122"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="1" left="16" bottom="1" right="16"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="uuw-Jy-472" secondAttribute="bottom" id="0DO-9R-O0l"/>
|
||||
<constraint firstItem="Gs1-ng-qdf" firstAttribute="leading" secondItem="6Pd-im-6vD" secondAttribute="leading" id="8wc-1j-dh7"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Gs1-ng-qdf" secondAttribute="trailing" id="Ax0-cp-SF2"/>
|
||||
<constraint firstItem="7Oh-hd-0e7" firstAttribute="top" secondItem="Gs1-ng-qdf" secondAttribute="bottom" id="I8A-x2-oZG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="uuw-Jy-472" secondAttribute="trailing" id="Z1f-t1-4GJ"/>
|
||||
<constraint firstItem="7Oh-hd-0e7" firstAttribute="leading" secondItem="6Pd-im-6vD" secondAttribute="leading" id="dCz-H4-an3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7Oh-hd-0e7" secondAttribute="trailing" id="hQM-uW-x9q"/>
|
||||
<constraint firstItem="uuw-Jy-472" firstAttribute="leading" secondItem="6Pd-im-6vD" secondAttribute="leading" id="htA-IO-x7c"/>
|
||||
<constraint firstItem="uuw-Jy-472" firstAttribute="top" secondItem="7Oh-hd-0e7" secondAttribute="bottom" id="kx7-e3-2UX"/>
|
||||
<constraint firstItem="Gs1-ng-qdf" firstAttribute="top" secondItem="6Pd-im-6vD" secondAttribute="top" id="zN5-uX-gl4"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="900" verticalHuggingPriority="900" horizontalCompressionResistancePriority="910" verticalCompressionResistancePriority="910" text="_Header" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TAY-o7-MHD">
|
||||
<rect key="frame" x="16" y="27" width="405" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4QI-3Q-rtc">
|
||||
<rect key="frame" x="0.0" y="53" width="421" height="1"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="TSt-sG-IxH" customClass="OnePixelConstrain" customModule="GraphTest" customModuleProvider="target"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AEQ-wS-3p4">
|
||||
<rect key="frame" x="0.0" y="484" width="421" height="1"/>
|
||||
<color key="backgroundColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="q3W-Ii-wV7" customClass="OnePixelConstrain" customModule="GraphTest" customModuleProvider="target"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="4QI-3Q-rtc" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="1oT-KU-FRt"/>
|
||||
<constraint firstAttribute="trailing" secondItem="4QI-3Q-rtc" secondAttribute="trailing" id="3Z5-bY-93t"/>
|
||||
<constraint firstAttribute="bottom" secondItem="AEQ-wS-3p4" secondAttribute="bottom" priority="999" id="4cm-gK-MnK"/>
|
||||
<constraint firstItem="AEQ-wS-3p4" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="Bah-zR-bfE"/>
|
||||
<constraint firstItem="TAY-o7-MHD" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="27" id="GIj-mI-5dl"/>
|
||||
<constraint firstItem="AEQ-wS-3p4" firstAttribute="top" secondItem="6Pd-im-6vD" secondAttribute="bottom" id="GRz-lz-VD5"/>
|
||||
<constraint firstItem="4QI-3Q-rtc" firstAttribute="top" secondItem="TAY-o7-MHD" secondAttribute="bottom" constant="5" id="T77-y7-mHo"/>
|
||||
<constraint firstItem="6Pd-im-6vD" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="XLV-JP-MAz"/>
|
||||
<constraint firstItem="TAY-o7-MHD" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="caV-uR-OCt"/>
|
||||
<constraint firstItem="6Pd-im-6vD" firstAttribute="top" secondItem="4QI-3Q-rtc" secondAttribute="bottom" id="fcf-nY-X5q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TAY-o7-MHD" secondAttribute="trailing" id="fyT-jz-qX4"/>
|
||||
<constraint firstAttribute="trailing" secondItem="AEQ-wS-3p4" secondAttribute="trailing" id="xSX-Xx-73t"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6Pd-im-6vD" secondAttribute="trailing" id="yOk-xT-or5"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="backButton" destination="Gze-ly-E3P" id="tjz-dk-9Re"/>
|
||||
<outlet property="backButtonWidthConstraint" destination="0fS-5e-eL0" id="CkZ-8G-YjF"/>
|
||||
<outlet property="chartHeightConstraint" destination="PH2-EY-kJT" id="W0R-Ms-q48"/>
|
||||
<outlet property="chartView" destination="Gs1-ng-qdf" id="fSW-zn-WuU"/>
|
||||
<outlet property="headerLabel" destination="TAY-o7-MHD" id="aNR-Oo-dMR"/>
|
||||
<outlet property="rangeView" destination="7Oh-hd-0e7" id="LaC-Mf-kKQ"/>
|
||||
<outlet property="sectionContainerView" destination="6Pd-im-6vD" id="lNz-Oe-uVZ"/>
|
||||
<outlet property="titleLabel" destination="kGq-zn-SjL" id="lhp-Hk-PQi"/>
|
||||
<outlet property="visibilityView" destination="uuw-Jy-472" id="rZn-nk-wqx"/>
|
||||
<outletCollection property="separators" destination="4QI-3Q-rtc" collectionClass="NSMutableArray" id="keK-ua-xv6"/>
|
||||
<outletCollection property="separators" destination="AEQ-wS-3p4" collectionClass="NSMutableArray" id="7We-sB-fTh"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-217" y="61"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="arrow_left" width="15" height="15"/>
|
||||
</resources>
|
||||
</document>
|
||||
158
submodules/Charts/Sources/Chart Screen/ChartView.swift
Normal file
158
submodules/Charts/Sources/Chart Screen/ChartView.swift
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// ChartView.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol ChartViewRenderer: class {
|
||||
var containerViews: [UIView] { get set }
|
||||
func render(context: CGContext, bounds: CGRect, chartFrame: CGRect)
|
||||
}
|
||||
|
||||
class ChartView: UIView {
|
||||
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 == self }) }
|
||||
}
|
||||
didSet {
|
||||
renderers.forEach { $0.containerViews.append(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)?
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ 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)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
userDidDeselectCoordinateClosure?()
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
userDidDeselectCoordinateClosure?()
|
||||
}
|
||||
|
||||
// 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: min(detailsViewPosition + detailsTableLeftOffset, bounds.width - maxDetailsViewWidth),
|
||||
y: chartInsets.top + detailsTableTopOffset,
|
||||
width: maxDetailsViewWidth,
|
||||
height: detailsViewSize.height)
|
||||
} else {
|
||||
detailsView.frame = CGRect(x: detailsViewPosition - 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: ColorModeContainer {
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
detailsView?.apply(colorMode: colorMode, animated: animated && (detailsView?.isVisibleInWindow ?? false))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// ChartVisibilityItemCell.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
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 = 6
|
||||
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)
|
||||
}
|
||||
}
|
||||
147
submodules/Charts/Sources/Chart Screen/ChartVisibilityView.swift
Normal file
147
submodules/Charts/Sources/Chart Screen/ChartVisibilityView.swift
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
//
|
||||
// ChartVisibilityView.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
struct ChartVisibilityItem {
|
||||
var title: String
|
||||
var color: UIColor
|
||||
}
|
||||
|
||||
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] in
|
||||
guard let self = self else { return }
|
||||
self.setItemSelected(!self.selectedItems[index], 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] = []
|
||||
|
||||
private func generateItemsFrames(frame: CGRect) -> [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
|
||||
}
|
||||
|
||||
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).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)
|
||||
guard let lastFrame = frames.last else { return .zero }
|
||||
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
extension ChartVisibilityView: ColorModeContainer {
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
UIView.perform(animated: animated) {
|
||||
self.backgroundColor = colorMode.chartBackgroundColor
|
||||
self.tintColor = colorMode.descriptionActionColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// ChartsDataLoader.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/8/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ChartsDataType: String {
|
||||
case generalLines = "1"
|
||||
case twoAxisLines = "2"
|
||||
case stackedBars = "3"
|
||||
case dailyBars = "4"
|
||||
case percentPie = "5"
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let overviewFilename = "overview.json"
|
||||
static let dataDir = "data"
|
||||
}
|
||||
|
||||
class ChartsDataLoader {
|
||||
static var documentDirectoryURL: URL {
|
||||
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||
let documentsDirectory = paths[0]
|
||||
return documentsDirectory
|
||||
}
|
||||
|
||||
static func overviewData(type: ChartsDataType, extraCopiesCount: Int = 0, sync: Bool = false, success: @escaping (ChartsCollection) -> Void) {
|
||||
let path = Bundle.main.bundleURL
|
||||
.appendingPathComponent(Constants.dataDir)
|
||||
.appendingPathComponent(type.rawValue)
|
||||
.appendingPathComponent(Constants.overviewFilename)
|
||||
ChartsDataManager().readChart(file: path, extraCopiesCount: extraCopiesCount, sync: sync, success: success, failure: { _ in })
|
||||
}
|
||||
|
||||
static func detaildData(type: ChartsDataType, extraCopiesCount: Int = 0, date: Date, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
|
||||
let dateComponents = Calendar.utc.dateComponents([.day, .month, .year], from: date)
|
||||
let yearMonth = String(format: "%04d-%02d", dateComponents.year ?? 0, dateComponents.month ?? 0)
|
||||
let day = String(format: "%02d.json", dateComponents.day ?? 0)
|
||||
|
||||
let path = Bundle.main.bundleURL
|
||||
.appendingPathComponent(Constants.dataDir)
|
||||
.appendingPathComponent(type.rawValue)
|
||||
.appendingPathComponent(yearMonth)
|
||||
.appendingPathComponent(day)
|
||||
ChartsDataManager().readChart(file: path, extraCopiesCount: extraCopiesCount, sync: false, success: success, failure: failure)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
//
|
||||
// ChartsStackViewController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ChartsStackViewController: UIViewController {
|
||||
@IBOutlet private var stackView: UIStackView!
|
||||
@IBOutlet private var scrollView: UIScrollView!
|
||||
@IBOutlet private var psLabel: UILabel!
|
||||
@IBOutlet private var ppsLabel: UILabel!
|
||||
@IBOutlet private var animationButton: ChartVisibilityItemView!
|
||||
|
||||
private var sections: [ChartStackSection] = []
|
||||
|
||||
private var colorMode: ColorMode = .night
|
||||
private var colorModeButton: UIBarButtonItem!
|
||||
private var performFastAnimation: Bool = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Statistics"
|
||||
colorModeButton = UIBarButtonItem(title: colorMode.switchTitle, style: .plain, target: self, action: #selector(didTapSwitchColorMode))
|
||||
navigationItem.rightBarButtonItem = colorModeButton
|
||||
|
||||
apply(colorMode: colorMode, animated: false)
|
||||
|
||||
self.navigationController?.navigationBar.barStyle = .black
|
||||
self.navigationController?.navigationBar.isTranslucent = false
|
||||
|
||||
self.view.isUserInteractionEnabled = false
|
||||
animationButton.backgroundColor = .clear
|
||||
animationButton.tapClosure = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.setSlowAnimationEnabled(!self.animationButton.isChecked)
|
||||
}
|
||||
self.setSlowAnimationEnabled(false)
|
||||
|
||||
loadChart1()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.view.setNeedsUpdateConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func loadChart1() {
|
||||
ChartsDataLoader.overviewData(type: .generalLines, sync: true, success: { collection in
|
||||
let generalLinesChartController = GeneralLinesChartController(chartsCollection: collection)
|
||||
self.addSection(controller: generalLinesChartController, title: "FOLLOWERS")
|
||||
generalLinesChartController.getDetailsData = { date, completion in
|
||||
ChartsDataLoader.detaildData(type: .generalLines, date: date, success: { collection in
|
||||
completion(collection)
|
||||
}, failure: { error in
|
||||
completion(nil)
|
||||
})
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.loadChart2()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func loadChart2() {
|
||||
ChartsDataLoader.overviewData(type: .twoAxisLines, success: { collection in
|
||||
let twoAxisLinesChartController = TwoAxisLinesChartController(chartsCollection: collection)
|
||||
self.addSection(controller: twoAxisLinesChartController, title: "INTERACTIONS")
|
||||
twoAxisLinesChartController.getDetailsData = { date, completion in
|
||||
ChartsDataLoader.detaildData(type: .twoAxisLines, date: date, success: { collection in
|
||||
completion(collection)
|
||||
}, failure: { error in
|
||||
completion(nil)
|
||||
})
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.loadChart3()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func loadChart3() {
|
||||
ChartsDataLoader.overviewData(type: .stackedBars, success: { collection in
|
||||
let stackedBarsChartController = StackedBarsChartController(chartsCollection: collection)
|
||||
self.addSection(controller: stackedBarsChartController, title: "FRUITS")
|
||||
stackedBarsChartController.getDetailsData = { date, completion in
|
||||
ChartsDataLoader.detaildData(type: .stackedBars, date: date, success: { collection in
|
||||
completion(collection)
|
||||
}, failure: { error in
|
||||
completion(nil)
|
||||
})
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.loadChart4()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func loadChart4() {
|
||||
ChartsDataLoader.overviewData(type: .dailyBars, success: { collection in
|
||||
let dailyBarsChartController = DailyBarsChartController(chartsCollection: collection)
|
||||
self.addSection(controller: dailyBarsChartController, title: "VIEWS")
|
||||
dailyBarsChartController.getDetailsData = { date, completion in
|
||||
ChartsDataLoader.detaildData(type: .dailyBars, date: date, success: { collection in
|
||||
completion(collection)
|
||||
}, failure: { error in
|
||||
completion(nil)
|
||||
})
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.loadChart5()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func loadChart5() {
|
||||
ChartsDataLoader.overviewData(type: .percentPie, success: { collection in
|
||||
let percentPieChartController = PercentPieChartController(chartsCollection: collection)
|
||||
self.addSection(controller: percentPieChartController, title: "MORE FRUITS")
|
||||
self.finalizeChartsLoading()
|
||||
})
|
||||
}
|
||||
|
||||
func setSlowAnimationEnabled(_ isEnabled: Bool) {
|
||||
animationButton.setChecked(isChecked: isEnabled, animated: true)
|
||||
if isEnabled {
|
||||
TimeInterval.animationDurationMultipler = 5
|
||||
} else {
|
||||
TimeInterval.animationDurationMultipler = 1
|
||||
}
|
||||
}
|
||||
|
||||
func finalizeChartsLoading() {
|
||||
self.view.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
func addSection(controller: BaseChartController, title: String) {
|
||||
let section = Bundle.main.loadNibNamed("ChartStackSection", owner: nil, options: nil)?.first as! ChartStackSection
|
||||
section.frame = UIScreen.main.bounds
|
||||
section.layoutIfNeeded()
|
||||
section.setup(controller: controller, title: title)
|
||||
section.apply(colorMode: colorMode, animated: false)
|
||||
stackView.addArrangedSubview(section)
|
||||
sections.append(section)
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return (colorMode == .day) ? .default : .lightContent
|
||||
}
|
||||
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
return .fade
|
||||
}
|
||||
|
||||
@objc private func didTapSwitchColorMode() {
|
||||
self.colorMode = self.colorMode == .day ? .night : .day
|
||||
apply(colorMode: self.colorMode, animated: !performFastAnimation)
|
||||
colorModeButton.title = colorMode.switchTitle
|
||||
}
|
||||
}
|
||||
|
||||
extension ChartsStackViewController: UIScrollViewDelegate {
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
performFastAnimation = decelerate
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
performFastAnimation = false
|
||||
}
|
||||
}
|
||||
|
||||
extension ChartsStackViewController: ColorModeContainer {
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
|
||||
UIView.perform(animated: animated) {
|
||||
self.psLabel.setTextColor(colorMode.sectionTitleColor, animated: animated && self.psLabel.isVisibleInWindow)
|
||||
self.ppsLabel.setTextColor(colorMode.sectionTitleColor, animated: animated && self.ppsLabel.isVisibleInWindow)
|
||||
self.animationButton.item = ChartVisibilityItem(title: "Enable slow animations",
|
||||
color: colorMode.sectionTitleColor)
|
||||
|
||||
self.view.backgroundColor = colorMode.tableBackgroundColor
|
||||
|
||||
if (animated) {
|
||||
let animation = CATransition()
|
||||
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
|
||||
animation.type = .fade
|
||||
animation.duration = .defaultDuration
|
||||
self.navigationController?.navigationBar.layer.add(animation, forKey: "kCATransitionColorFade")
|
||||
}
|
||||
|
||||
self.navigationController?.navigationBar.tintColor = colorMode.actionButtonColor
|
||||
self.navigationController?.navigationBar.barTintColor = colorMode.chartBackgroundColor
|
||||
self.navigationController?.navigationBar.titleTextAttributes = [.font: UIFont.systemFont(ofSize: 17, weight: .medium),
|
||||
.foregroundColor: colorMode.viewTintColor]
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
self.setNeedsStatusBarAppearanceUpdate()
|
||||
|
||||
for section in sections {
|
||||
section.apply(colorMode: colorMode, animated: animated && section.isVisibleInWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorMode {
|
||||
var switchTitle: String {
|
||||
switch self {
|
||||
case .day:
|
||||
return "Night Mode"
|
||||
case .night:
|
||||
return "Day Mode"
|
||||
}
|
||||
}
|
||||
}
|
||||
291
submodules/Charts/Sources/Chart Screen/RangeChartView.swift
Normal file
291
submodules/Charts/Sources/Chart Screen/RangeChartView.swift
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
//
|
||||
// RangeChartView.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
import UIKit
|
||||
|
||||
private enum Constants {
|
||||
static let cropIndocatorLineWidth: CGFloat = 1
|
||||
static let markerSelectionRange: CGFloat = 25
|
||||
static let defaultMinimumRangeDistance: CGFloat = 0.05
|
||||
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 selectedMarkerHorizontalOffet: CGFloat = 0
|
||||
private var isBoundCropHighlighted: Bool = false
|
||||
private var isRangePagingEnabled: Bool = false
|
||||
|
||||
public let chartView = ChartView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
layoutMargins = UIEdgeInsets(top: Constants.cropIndocatorLineWidth,
|
||||
left: Constants.horizontalContentMargin,
|
||||
bottom: Constants.cropIndocatorLineWidth,
|
||||
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
|
||||
selectedMarkerHorizontalOffet = point.x - locationInView(for: upperBound)
|
||||
isBoundCropHighlighted = true
|
||||
} else if abs(locationInView(for: lowerBound) - point.x - Constants.markerSelectionRange / 2) < Constants.markerSelectionRange {
|
||||
selectedMarker = .lower
|
||||
selectedMarkerHorizontalOffet = point.x - locationInView(for: lowerBound)
|
||||
isBoundCropHighlighted = true
|
||||
} else if point.x > locationInView(for: lowerBound) && point.x < locationInView(for: upperBound) {
|
||||
selectedMarker = .center
|
||||
selectedMarkerHorizontalOffet = point.x - locationInView(for: lowerBound)
|
||||
isBoundCropHighlighted = true
|
||||
} else {
|
||||
selectedMarker = 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 - selectedMarkerHorizontalOffet
|
||||
let fraction = fractionFor(offsetX: horizontalPosition)
|
||||
updateMarkerOffset(selectedMarker, fraction: fraction)
|
||||
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard isEnabled else { return }
|
||||
guard let selectedMarker = selectedMarker else {
|
||||
touchedOutsideClosure?()
|
||||
return
|
||||
}
|
||||
guard let point = touches.first?.location(in: self) else { return }
|
||||
|
||||
let horizontalPosition = point.x - selectedMarkerHorizontalOffet
|
||||
let fraction = fractionFor(offsetX: horizontalPosition)
|
||||
updateMarkerOffset(selectedMarker, fraction: fraction)
|
||||
|
||||
self.selectedMarker = nil
|
||||
self.isBoundCropHighlighted = false
|
||||
if bounds.contains(point) {
|
||||
sendActions(for: .touchUpInside)
|
||||
} else {
|
||||
sendActions(for: .touchUpOutside)
|
||||
}
|
||||
rangeDidChangeClosure?(lowerBound...upperBound)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.selectedMarker = nil
|
||||
self.isBoundCropHighlighted = false
|
||||
sendActions(for: .touchCancel)
|
||||
}
|
||||
}
|
||||
|
||||
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.cropIndocatorLineWidth,
|
||||
width: locationInView(for: upperBound) - locationInView(for: lowerBound),
|
||||
height: contentFrame.height + Constants.cropIndocatorLineWidth * 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: ColorModeContainer {
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
let colusre = {
|
||||
self.lowerBoundTintView.backgroundColor = colorMode.rangeViewTintColor
|
||||
self.upperBoundTintView.backgroundColor = colorMode.rangeViewTintColor
|
||||
}
|
||||
|
||||
self.cropFrameView.setImage(colorMode.rangeCropImage, animated: animated)
|
||||
|
||||
// self.chartView.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: .defaultDuration, animations: colusre)
|
||||
} else {
|
||||
colusre()
|
||||
}
|
||||
}
|
||||
}
|
||||
48
submodules/Charts/Sources/ChartNode.swift
Normal file
48
submodules/Charts/Sources/ChartNode.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import AppBundle
|
||||
|
||||
public final class ChartNode: ASDisplayNode {
|
||||
private var chartView: ChartStackSection {
|
||||
return self.view as! ChartStackSection
|
||||
}
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return ChartStackSection()
|
||||
})
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
public func setup(_ data: String, bar: Bool = false) {
|
||||
if let data = data.data(using: .utf8) {
|
||||
ChartsDataManager().readChart(data: data, extraCopiesCount: 0, sync: true, success: { [weak self] collection in
|
||||
let controller: BaseChartController
|
||||
if bar {
|
||||
controller = DailyBarsChartController(chartsCollection: collection)
|
||||
} else {
|
||||
controller = GeneralLinesChartController(chartsCollection: collection)
|
||||
}
|
||||
if let strongSelf = self {
|
||||
strongSelf.chartView.setup(controller: controller, title: "")
|
||||
strongSelf.chartView.apply(colorMode: .day, animated: false)
|
||||
}
|
||||
}) { error in
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// ChardData.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct ChartsCollection {
|
||||
struct Chart {
|
||||
var color: UIColor
|
||||
var name: String
|
||||
var values: [Double]
|
||||
}
|
||||
|
||||
var axisValues: [Date]
|
||||
var chartValues: [Chart]
|
||||
|
||||
static let blank = ChartsCollection(axisValues: [], chartValues: [])
|
||||
var isBlank: Bool {
|
||||
return axisValues.isEmpty || chartValues.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
extension ChartsCollection {
|
||||
public init(from decodedData: [String: Any]) throws {
|
||||
guard let columns = decodedData["columns"] as? [[Any]] else {
|
||||
throw ChartsError.generalConversion("Unable to get columns from: \(decodedData)")
|
||||
}
|
||||
guard let types = decodedData["types"] as? [String: String] else {
|
||||
throw ChartsError.generalConversion("Unable to get types from: \(decodedData)")
|
||||
}
|
||||
guard let names = decodedData["names"] as? [String: String] else {
|
||||
throw ChartsError.generalConversion("Unable to get names from: \(decodedData)")
|
||||
}
|
||||
guard let colors = decodedData["colors"] as? [String: String] else {
|
||||
throw ChartsError.generalConversion("Unable to get colors from: \(decodedData)")
|
||||
}
|
||||
|
||||
// chart.colors – Color for each variable in 6-hex-digit format (e.g. "#AAAAAA").
|
||||
// chart.names – Name for each variable.
|
||||
// chart.percentage – true for percentage based values.
|
||||
// chart.stacked – true for values stacking on top of each other.
|
||||
// chart.y_scaled – true for charts with 2 Y axes.
|
||||
|
||||
var axixValuesToSetup: [Date] = []
|
||||
var chartToSetup: [Chart] = []
|
||||
for column in columns {
|
||||
guard let columnId = column.first as? String else {
|
||||
throw ChartsError.generalConversion("Unable to get column name from: \(column)")
|
||||
}
|
||||
guard let typeString = types[columnId], let type = ColumnType(rawValue: typeString) else {
|
||||
throw ChartsError.generalConversion("Unable to get column type from: \(types) - \(columnId)")
|
||||
}
|
||||
switch type {
|
||||
case .axix:
|
||||
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
|
||||
case .chart, .bar, .area:
|
||||
guard let colorString = colors[columnId],
|
||||
let color = UIColor(hexString: colorString) else {
|
||||
throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)")
|
||||
}
|
||||
guard let name = names[columnId] else {
|
||||
throw ChartsError.generalConversion("Unable to get column name from: \(names) - \(columnId)")
|
||||
}
|
||||
let values = try column.dropFirst().map { try Convert.doubleFrom($0) }
|
||||
chartToSetup.append(Chart(color: color,
|
||||
name: name,
|
||||
values: values))
|
||||
}
|
||||
}
|
||||
|
||||
guard axixValuesToSetup.isEmpty == false,
|
||||
chartToSetup.isEmpty == false,
|
||||
chartToSetup.firstIndex(where: { $0.values.count != axixValuesToSetup.count }) == nil else {
|
||||
throw ChartsError.generalConversion("Saniazing: Invalid number of items: \(axixValuesToSetup), \(chartToSetup)")
|
||||
}
|
||||
self.axisValues = axixValuesToSetup
|
||||
self.chartValues = chartToSetup
|
||||
}
|
||||
}
|
||||
|
||||
private enum ColumnType: String {
|
||||
case axix = "x"
|
||||
case chart = "line"
|
||||
case area = "area"
|
||||
case bar = "bar"
|
||||
}
|
||||
191
submodules/Charts/Sources/Charts Reader/ChartsDataManager.swift
Normal file
191
submodules/Charts/Sources/Charts Reader/ChartsDataManager.swift
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
//
|
||||
// ChartsDataManager.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ChartsDataManager {
|
||||
func readChart(item: [String: Any], extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
|
||||
let workItem: (() -> Void) = {
|
||||
do {
|
||||
var collection = try ChartsCollection(from: item)
|
||||
for _ in 0..<extraCopiesCount {
|
||||
for valueIndex in collection.chartValues.indices {
|
||||
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
|
||||
}
|
||||
guard let firstValue = collection.axisValues.first,
|
||||
let lastValule = collection.axisValues.last else {
|
||||
throw ChartsError.invalidJson
|
||||
}
|
||||
let startItem = lastValule.addingTimeInterval(.day)
|
||||
for valueIndex in collection.axisValues.indices {
|
||||
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
|
||||
let newDate = startItem.addingTimeInterval(intervalToAdd)
|
||||
collection.axisValues.append(newDate)
|
||||
}
|
||||
}
|
||||
if sync {
|
||||
success(collection)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
success(collection)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
assertionFailure("Error occure: \(error)")
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sync {
|
||||
workItem()
|
||||
} else {
|
||||
DispatchQueue.global().async(execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func readChart(data: Data, extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
|
||||
let workItem: (() -> Void) = {
|
||||
do {
|
||||
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
guard let item = decoded as? [String: Any] else {
|
||||
throw ChartsError.invalidJson
|
||||
}
|
||||
var collection = try ChartsCollection(from: item)
|
||||
for _ in 0..<extraCopiesCount {
|
||||
for valueIndex in collection.chartValues.indices {
|
||||
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
|
||||
}
|
||||
guard let firstValue = collection.axisValues.first,
|
||||
let lastValule = collection.axisValues.last else {
|
||||
throw ChartsError.invalidJson
|
||||
}
|
||||
let startItem = lastValule.addingTimeInterval(.day)
|
||||
for valueIndex in collection.axisValues.indices {
|
||||
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
|
||||
let newDate = startItem.addingTimeInterval(intervalToAdd)
|
||||
collection.axisValues.append(newDate)
|
||||
}
|
||||
}
|
||||
if sync {
|
||||
success(collection)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
success(collection)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
assertionFailure("Error occure: \(error)")
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sync {
|
||||
workItem()
|
||||
} else {
|
||||
DispatchQueue.global().async(execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func readChart(file: URL, extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
|
||||
let workItem: (() -> Void) = {
|
||||
do {
|
||||
let data = try Data(contentsOf: file)
|
||||
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
guard let item = decoded as? [String: Any] else {
|
||||
throw ChartsError.invalidJson
|
||||
}
|
||||
var collection = try ChartsCollection(from: item)
|
||||
for _ in 0..<extraCopiesCount {
|
||||
for valueIndex in collection.chartValues.indices {
|
||||
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
|
||||
}
|
||||
guard let firstValue = collection.axisValues.first,
|
||||
let lastValule = collection.axisValues.last else {
|
||||
throw ChartsError.invalidJson
|
||||
}
|
||||
let startItem = lastValule.addingTimeInterval(.day)
|
||||
for valueIndex in collection.axisValues.indices {
|
||||
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
|
||||
let newDate = startItem.addingTimeInterval(intervalToAdd)
|
||||
collection.axisValues.append(newDate)
|
||||
}
|
||||
}
|
||||
if sync {
|
||||
success(collection)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
success(collection)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
assertionFailure("Error occure: \(error)")
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sync {
|
||||
workItem()
|
||||
} else {
|
||||
DispatchQueue.global().async(execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func readCharts(file: URL, extraCopiesCount: Int = 0, sync: Bool, success: @escaping ([ChartsCollection]) -> Void, failure: @escaping (Error) -> Void) {
|
||||
let workItem: (() -> Void) = {
|
||||
do {
|
||||
let data = try Data(contentsOf: file)
|
||||
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
guard let items = decoded as? [[String: Any]] else {
|
||||
throw ChartsError.invalidJson
|
||||
}
|
||||
var collections = try items.map { try ChartsCollection(from: $0) }
|
||||
for _ in 0..<extraCopiesCount {
|
||||
for collrctionIndex in collections.indices {
|
||||
for valueIndex in collections[collrctionIndex].chartValues.indices {
|
||||
collections[collrctionIndex].chartValues[valueIndex] .values += collections[collrctionIndex].chartValues[valueIndex].values
|
||||
}
|
||||
guard let firstValue = collections[collrctionIndex].axisValues.first,
|
||||
let lastValule = collections[collrctionIndex].axisValues.last else {
|
||||
return
|
||||
}
|
||||
let startItem = lastValule.addingTimeInterval(.day)
|
||||
for valueIndex in collections[collrctionIndex].axisValues.indices {
|
||||
let intervalToAdd = collections[collrctionIndex].axisValues[valueIndex].timeIntervalSince(firstValue)
|
||||
let newDate = startItem.addingTimeInterval(intervalToAdd)
|
||||
collections[collrctionIndex].axisValues.append(newDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
if sync {
|
||||
success(collections)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
success(collections)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
assertionFailure("Error occure: \(error)")
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
if sync {
|
||||
workItem()
|
||||
} else {
|
||||
DispatchQueue.global().async(execute: workItem)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
14
submodules/Charts/Sources/Charts Reader/ChartsError.swift
Normal file
14
submodules/Charts/Sources/Charts Reader/ChartsError.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// ChartsError.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ChartsError: Error {
|
||||
case invalidJson
|
||||
case generalConversion(String)
|
||||
}
|
||||
42
submodules/Charts/Sources/Charts Reader/Convert.swift
Normal file
42
submodules/Charts/Sources/Charts Reader/Convert.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Convert.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Convert {
|
||||
public static func doubleFrom(_ value: Any?) throws -> Double {
|
||||
guard let double = try doubleFrom(value, lenientCast: false) else {
|
||||
throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)")
|
||||
}
|
||||
return double
|
||||
}
|
||||
|
||||
public static func doubleFrom(_ value: Any?, lenientCast: Bool = false) throws -> Double? {
|
||||
guard let value = value else {
|
||||
return nil
|
||||
}
|
||||
if let intValue = value as? Int {
|
||||
return Double(intValue)
|
||||
} else if let floatValue = value as? Float {
|
||||
return Double(floatValue)
|
||||
} else if let int64Value = value as? Int64 {
|
||||
return Double(int64Value)
|
||||
} else if let intValue = value as? Int {
|
||||
return Double(intValue)
|
||||
} else if let stringValue = value as? String {
|
||||
if let doubleValue = Double(stringValue) {
|
||||
return doubleValue
|
||||
}
|
||||
}
|
||||
if lenientCast {
|
||||
return nil
|
||||
} else {
|
||||
throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)")
|
||||
}
|
||||
}
|
||||
}
|
||||
19
submodules/Charts/Sources/Charts.h
Normal file
19
submodules/Charts/Sources/Charts.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// StatisticsUI.h
|
||||
// StatisticsUI
|
||||
//
|
||||
// Created by Peter on 8/13/19.
|
||||
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for StatisticsUI.
|
||||
FOUNDATION_EXPORT double StatisticsUIVersionNumber;
|
||||
|
||||
//! Project version string for StatisticsUI.
|
||||
FOUNDATION_EXPORT const unsigned char StatisticsUIVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <StatisticsUI/PublicHeader.h>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
//
|
||||
// BaseChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum BaseConstants {
|
||||
static let defaultRange: ClosedRange<CGFloat> = 0...1
|
||||
static let minimumAxisYLabelsDistance: CGFloat = 90
|
||||
static let monthDayDateFormatter = DateFormatter.utc(format: "MMM d")
|
||||
static let timeDateFormatter = DateFormatter.utc(format: "HH:mm")
|
||||
static let headerFullRangeFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter.utc()
|
||||
formatter.calendar = Calendar.utc
|
||||
formatter.dateStyle = .long
|
||||
return formatter
|
||||
}()
|
||||
static let headerMediumRangeFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter.utc()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
static let headerFullZoomedFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter.utc()
|
||||
formatter.dateStyle = .full
|
||||
return formatter
|
||||
}()
|
||||
|
||||
static let verticalBaseAnchors: [CGFloat] = [8, 5, 2.5, 2, 1]
|
||||
static let defaultVerticalBaseAnchor: CGFloat = 1
|
||||
|
||||
static let mainChartLineWidth: CGFloat = 2
|
||||
static let previewChartLineWidth: CGFloat = 1
|
||||
|
||||
static let previewLinesChartOptimizationLevel: CGFloat = 1.5
|
||||
static let linesChartOptimizationLevel: CGFloat = 1.0
|
||||
static let barsChartOptimizationLevel: CGFloat = 0.75
|
||||
|
||||
static let defaultRangePresetLength = TimeInterval.day * 60
|
||||
|
||||
static let chartNumberFormatter: ScalesNumberFormatter = {
|
||||
let numberFormatter = ScalesNumberFormatter()
|
||||
numberFormatter.allowsFloats = true
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.usesGroupingSeparator = true
|
||||
numberFormatter.groupingSeparator = " "
|
||||
numberFormatter.minimumIntegerDigits = 1
|
||||
numberFormatter.minimumFractionDigits = 0
|
||||
numberFormatter.maximumFractionDigits = 2
|
||||
return numberFormatter
|
||||
}()
|
||||
|
||||
static let detailsNumberFormatter: NumberFormatter = {
|
||||
let detailsNumberFormatter = NumberFormatter()
|
||||
detailsNumberFormatter.allowsFloats = false
|
||||
detailsNumberFormatter.numberStyle = .decimal
|
||||
detailsNumberFormatter.usesGroupingSeparator = true
|
||||
detailsNumberFormatter.groupingSeparator = " "
|
||||
return detailsNumberFormatter
|
||||
}()
|
||||
}
|
||||
|
||||
class BaseChartController: ColorModeContainer {
|
||||
//let performanceRenderer = PerformanceRenderer()
|
||||
var initialChartsCollection: ChartsCollection
|
||||
var isZoomed: Bool = false
|
||||
|
||||
var chartTitle: String = ""
|
||||
|
||||
init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartsCollection = chartsCollection
|
||||
}
|
||||
|
||||
var mainChartRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var navigationRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var cartViewBounds: (() -> CGRect) = { fatalError() }
|
||||
var chartFrame: (() -> CGRect) = { fatalError() }
|
||||
|
||||
func initializeChart() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func chartInteractionDidBegin(point: CGPoint) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func chartInteractionDidEnd() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func cancelChartInteraction() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func didTapZoomOut() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var isChartRangePagingEnabled: Bool = false
|
||||
var minimumSelectedChartRange: CGFloat = 0.05
|
||||
var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize
|
||||
func setChartRangePagingEnabled(isEnabled: Bool, minimumSelectionSize: CGFloat) {
|
||||
isChartRangePagingEnabled = isEnabled
|
||||
minimumSelectedChartRange = minimumSelectionSize
|
||||
chartRangePagingClosure?(isChartRangePagingEnabled, minimumSelectedChartRange)
|
||||
}
|
||||
|
||||
var chartRangeUpdatedClosure: ((ClosedRange<CGFloat>, Bool) -> Void)?
|
||||
var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var actualChartVisibility: [Bool] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var actualChartsCollection: ChartsCollection {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var drawChartNavigation: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var setDetailsViewPositionClosure: ((CGFloat) -> Void)?
|
||||
var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)?
|
||||
var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)?
|
||||
var getDetailsData: ((Date, @escaping (ChartsCollection?) -> Void) -> Void)?
|
||||
var setChartTitleClosure: ((String, Bool) -> Void)?
|
||||
var setBackButtonVisibilityClosure: ((Bool, Bool) -> Void)?
|
||||
var refreshChartToolsClosure: ((Bool) -> Void)?
|
||||
|
||||
func didTapZoomIn(date: Date) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
//
|
||||
// GeneralChartComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum GeneralChartComponentConstants {
|
||||
static let defaultInitialRangeLength = CGFloat(TimeInterval.day * 60)
|
||||
static let defaultZoomedRangeLength = CGFloat(TimeInterval.day)
|
||||
}
|
||||
|
||||
class GeneralChartComponentController: ColorModeContainer {
|
||||
var chartsCollection: ChartsCollection = ChartsCollection.blank
|
||||
var chartVisibility: [Bool] = []
|
||||
var lastChartInteractionPoint: CGPoint = .zero
|
||||
var isChartInteractionBegun: Bool = false
|
||||
var isChartInteracting: Bool = false
|
||||
let isZoomed: Bool
|
||||
|
||||
var colorMode: ColorMode = .day
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var initialHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var initialVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
var cartViewBounds: (() -> CGRect) = { fatalError() }
|
||||
var chartFrame: (() -> CGRect) = { fatalError() }
|
||||
|
||||
init(isZoomed: Bool) {
|
||||
self.isZoomed = isZoomed
|
||||
}
|
||||
|
||||
func initialize(chartsCollection: ChartsCollection,
|
||||
initialDate: Date,
|
||||
totalHorizontalRange: ClosedRange<CGFloat>,
|
||||
totalVerticalRange: ClosedRange<CGFloat>) {
|
||||
self.chartsCollection = chartsCollection
|
||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
||||
self.totalHorizontalRange = totalHorizontalRange
|
||||
self.totalVerticalRange = totalVerticalRange
|
||||
self.initialHorizontalRange = totalHorizontalRange
|
||||
self.initialVerticalRange = totalVerticalRange
|
||||
|
||||
didLoad()
|
||||
setupInitialChartRange(initialDate: initialDate)
|
||||
}
|
||||
|
||||
func didLoad() {
|
||||
hideDetailsView(animated: false)
|
||||
}
|
||||
func willAppear(animated: Bool) {
|
||||
updateChartRangeTitle(animated: animated)
|
||||
setupChartRangePaging()
|
||||
}
|
||||
func willDisappear(animated: Bool) {
|
||||
}
|
||||
|
||||
func setupInitialChartRange(initialDate: Date) {
|
||||
guard let first = chartsCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = chartsCollection.axisValues.last?.timeIntervalSince1970 else { return }
|
||||
|
||||
let rangeStart = CGFloat(first)
|
||||
let rangeEnd = CGFloat(last)
|
||||
|
||||
if isZoomed {
|
||||
let initalDate = CGFloat(initialDate.timeIntervalSince1970)
|
||||
|
||||
initialHorizontalRange = max(initalDate, rangeStart)...min(initalDate + GeneralChartComponentConstants.defaultZoomedRangeLength, rangeEnd)
|
||||
initialVerticalRange = totalVerticalRange
|
||||
} else {
|
||||
initialHorizontalRange = max(rangeStart, rangeEnd - GeneralChartComponentConstants.defaultInitialRangeLength)...rangeEnd
|
||||
initialVerticalRange = totalVerticalRange
|
||||
}
|
||||
}
|
||||
func setupChartRangePaging() {
|
||||
chartRangePagingClosure?(false, 0.05)
|
||||
}
|
||||
|
||||
var visibleHorizontalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.verticalRange.current
|
||||
}
|
||||
var visibleVerticalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.verticalRange.current
|
||||
}
|
||||
var currentHorizontalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.horizontalRange.end
|
||||
}
|
||||
var currentVerticalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.verticalRange.end
|
||||
}
|
||||
var currentMainRangeRenderer: BaseChartRenderer {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var visiblePreviewHorizontalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.verticalRange.current
|
||||
}
|
||||
var visiblePreviewVerticalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.verticalRange.current
|
||||
}
|
||||
var currentPreviewHorizontalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.horizontalRange.end
|
||||
}
|
||||
var currentPreviewVerticalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.verticalRange.end
|
||||
}
|
||||
var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var mainChartRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
var previewRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
self.chartVisibility = visibility
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalMainChartRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalMainChartRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
func chartRangeFractionDidUpdated(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
chartRangeDidUpdated(horizontalRange)
|
||||
updateChartRangeTitle(animated: true)
|
||||
}
|
||||
|
||||
func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
hideDetailsView(animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Details & Interaction
|
||||
func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
||||
guard chartsCollection.axisValues.count > 0 else { return nil }
|
||||
var closestDate = chartsCollection.axisValues[0]
|
||||
var minIndex = 0
|
||||
for (index, date) in chartsCollection.axisValues.enumerated() {
|
||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
||||
closestDate = date
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
return (closestDate, minIndex)
|
||||
}
|
||||
|
||||
func chartInteractionDidBegin(point: CGPoint) {
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
let horizontalRange = currentHorizontalMainChartRange
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartWasInteracting = isChartInteractionBegun
|
||||
lastChartInteractionPoint = point
|
||||
isChartInteractionBegun = true
|
||||
isChartInteracting = true
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
showDetailsView(at: chartValue, detailsViewPosition: detailsViewPosition, dataIndex: minIndex, date: closestDate, animted: chartWasInteracting)
|
||||
}
|
||||
|
||||
func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
setDetailsViewModel?(chartDetailsViewModel(closestDate: date, pointIndex: dataIndex), animted)
|
||||
setDetailsChartVisibleClosure?(true, true)
|
||||
setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
}
|
||||
|
||||
func chartInteractionDidEnd() {
|
||||
isChartInteracting = false
|
||||
}
|
||||
|
||||
func hideDetailsView(animated: Bool) {
|
||||
isChartInteractionBegun = false
|
||||
setDetailsChartVisibleClosure?(false, animated)
|
||||
}
|
||||
|
||||
var visibleDetailsChartValues: [ChartsCollection.Chart] {
|
||||
let visibleCharts: [ChartsCollection.Chart] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartsCollection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
var updatePreviewRangeClosure: ((ClosedRange<CGFloat>, Bool) -> Void)?
|
||||
var zoomInOnDateClosure: ((Date) -> Void)?
|
||||
var setChartTitleClosure: ((String, Bool) -> Void)?
|
||||
var setDetailsViewPositionClosure: ((CGFloat) -> Void)?
|
||||
var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)?
|
||||
var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)?
|
||||
var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize
|
||||
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
self.colorMode = colorMode
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
var prevoiusHorizontalStrideInterval: Int = -1
|
||||
func updateHorizontalLimitLabels(horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
horizontalRange: ClosedRange<CGFloat>,
|
||||
scaleType: ChartScaleType,
|
||||
forceUpdate: Bool,
|
||||
animated: Bool) {
|
||||
let scaleTimeInterval: TimeInterval
|
||||
if chartsCollection.axisValues.count >= 1 {
|
||||
scaleTimeInterval = chartsCollection.axisValues[1].timeIntervalSince1970 - chartsCollection.axisValues[0].timeIntervalSince1970
|
||||
} else {
|
||||
scaleTimeInterval = scaleType.timeInterval
|
||||
}
|
||||
|
||||
let numberOfItems = horizontalRange.distance / CGFloat(scaleTimeInterval)
|
||||
let maximumNumberOfItems = chartFrame().width / scaleType.minimumAxisXDistance
|
||||
let tempStride = max(1, Int((numberOfItems / maximumNumberOfItems).rounded(.up)))
|
||||
var strideInterval = 1
|
||||
while strideInterval < tempStride {
|
||||
strideInterval *= 2
|
||||
}
|
||||
|
||||
if forceUpdate || (strideInterval != prevoiusHorizontalStrideInterval && strideInterval > 0) {
|
||||
var labels: [LinesChartLabel] = []
|
||||
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||
let date = chartsCollection.axisValues[index]
|
||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
}
|
||||
prevoiusHorizontalStrideInterval = strideInterval
|
||||
horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
||||
let ditance = verticalRange.distance
|
||||
let chartHeight = chartFrame().height
|
||||
|
||||
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
var base: CGFloat = BaseConstants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
base = base * multiplier / dividor
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let lowerBound = verticalValue
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
verticalValue += base
|
||||
}
|
||||
let updatedRange = lowerBound...verticalValue
|
||||
|
||||
return (updatedRange, verticalLabels)
|
||||
}
|
||||
|
||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "",
|
||||
color: component.color,
|
||||
visible: chartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: false,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in
|
||||
self?.zoomInOnDateClosure?(closestDate) })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound) + 1)
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
//
|
||||
// BaseLinesChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BaseLinesChartController: BaseChartController {
|
||||
var chartVisibility: [Bool]
|
||||
var zoomChartVisibility: [Bool]
|
||||
var lastChartInteractionPoint: CGPoint = .zero
|
||||
var isChartInteractionBegun: Bool = false
|
||||
|
||||
var initialChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var zoomedChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
override init(chartsCollection: ChartsCollection) {
|
||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
||||
self.zoomChartVisibility = []
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
}
|
||||
|
||||
func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
self.initialChartsCollection = chartsCollection
|
||||
self.isZoomed = isZoomed
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
|
||||
updateChartRangeTitle(animated: animated)
|
||||
}
|
||||
|
||||
func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(zoomedChartRange.lowerBound) + .hour)
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(zoomedChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
lastChartInteractionPoint = point
|
||||
isChartInteractionBegun = true
|
||||
}
|
||||
|
||||
override func chartInteractionDidEnd() {
|
||||
|
||||
}
|
||||
|
||||
override func cancelChartInteraction() {
|
||||
isChartInteractionBegun = false
|
||||
}
|
||||
|
||||
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
|
||||
}
|
||||
|
||||
override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? zoomChartVisibility : chartVisibility
|
||||
}
|
||||
|
||||
override var actualChartsCollection: ChartsCollection {
|
||||
return initialChartsCollection
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibleCharts: [ChartsCollection.Chart] = actualChartVisibility.enumerated().compactMap { args in
|
||||
args.element ? initialChartsCollection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
|
||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let values: [ChartDetailsViewModel.Value] = actualChartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
||||
color: component.color,
|
||||
visible: actualChartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: false,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in self?.didTapZoomIn(date: closestDate) })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
cancelChartInteraction()
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.initialChartRange = self.currentHorizontalRange
|
||||
if let startDate = updatedCollection.axisValues.first,
|
||||
let endDate = updatedCollection.axisValues.last {
|
||||
self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970))
|
||||
} else {
|
||||
self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1)
|
||||
}
|
||||
self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func horizontalLimitsLabels(horizontalRange: ClosedRange<CGFloat>,
|
||||
scaleType: ChartScaleType,
|
||||
prevoiusHorizontalStrideInterval: Int) -> (Int, [LinesChartLabel])? {
|
||||
let numberOfItems = horizontalRange.distance / CGFloat(scaleType.timeInterval)
|
||||
let maximumNumberOfItems = chartFrame().width / scaleType.minimumAxisXDistance
|
||||
let tempStride = max(1, Int((numberOfItems / maximumNumberOfItems).rounded(.up)))
|
||||
var strideInterval = 1
|
||||
while strideInterval < tempStride {
|
||||
strideInterval *= 2
|
||||
}
|
||||
|
||||
if strideInterval != prevoiusHorizontalStrideInterval && strideInterval > 0 {
|
||||
var labels: [LinesChartLabel] = []
|
||||
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||
let date = initialChartsCollection.axisValues[index]
|
||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
}
|
||||
return (strideInterval, labels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
||||
guard initialChartsCollection.axisValues.count > 0 else { return nil }
|
||||
var closestDate = initialChartsCollection.axisValues[0]
|
||||
var minIndex = 0
|
||||
for (index, date) in initialChartsCollection.axisValues.enumerated() {
|
||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
||||
closestDate = date
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
return (closestDate, minIndex)
|
||||
}
|
||||
|
||||
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
||||
let ditance = verticalRange.distance
|
||||
let chartHeight = chartFrame().height
|
||||
|
||||
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
var base: CGFloat = BaseConstants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
base = base * multiplier / dividor
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let lowerBound = verticalValue
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
verticalValue += base
|
||||
}
|
||||
let updatedRange = lowerBound...verticalValue
|
||||
|
||||
return (updatedRange, verticalLabels)
|
||||
}
|
||||
}
|
||||
|
||||
enum ChartScaleType {
|
||||
case day
|
||||
case hour
|
||||
case minutes5
|
||||
}
|
||||
|
||||
extension ChartScaleType {
|
||||
var timeInterval: TimeInterval {
|
||||
switch self {
|
||||
case .day: return .day
|
||||
case .hour: return .hour
|
||||
case .minutes5: return .minute * 5
|
||||
}
|
||||
}
|
||||
|
||||
var minimumAxisXDistance: CGFloat {
|
||||
switch self {
|
||||
case .day: return 50
|
||||
case .hour: return 40
|
||||
case .minutes5: return 40
|
||||
}
|
||||
}
|
||||
var dateFormatter: DateFormatter {
|
||||
switch self {
|
||||
case .day: return BaseConstants.monthDayDateFormatter
|
||||
case .hour: return BaseConstants.timeDateFormatter
|
||||
case .minutes5: return BaseConstants.timeDateFormatter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
//
|
||||
// LinesChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private enum Constants {
|
||||
static let defaultRange: ClosedRange<CGFloat> = 0...1
|
||||
}
|
||||
|
||||
class GeneralLinesChartController: BaseLinesChartController {
|
||||
private let initialChartCollection: ChartsCollection
|
||||
|
||||
private let mainLinesRenderer = LinesChartRenderer()
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
private let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
private let lineBulletsRenerer = LineBulletsRenerer()
|
||||
|
||||
private let previewLinesRenderer = LinesChartRenderer()
|
||||
|
||||
private var totalVerticalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
private var totalHorizontalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
|
||||
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||
|
||||
private (set) var chartLines: [LinesChartRenderer.LineData] = []
|
||||
|
||||
override init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
self.mainLinesRenderer.lineWidth = 2
|
||||
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
|
||||
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
|
||||
|
||||
self.lineBulletsRenerer.isEnabled = false
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
self.zoomChartVisibility = chartVisibility
|
||||
}
|
||||
|
||||
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
|
||||
|
||||
self.chartLines = chartsCollection.chartValues.map { chart in
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
return LinesChartRenderer.LineData(color: chart.color, points: points)
|
||||
}
|
||||
|
||||
self.prevoiusHorizontalStrideInterval = -1
|
||||
self.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
self.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero,
|
||||
color: $0.color)}
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
self.previewLinesRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
self.previewLinesRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
|
||||
self.mainLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
self.previewLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
|
||||
updateHorizontalLimists(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [//performanceRenderer,
|
||||
mainLinesRenderer,
|
||||
horizontalScalesRenderer,
|
||||
verticalScalesRenderer,
|
||||
verticalLineRenderer,
|
||||
lineBulletsRenerer
|
||||
]
|
||||
}
|
||||
|
||||
override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [previewLinesRenderer]
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
chartVisibility = visibility
|
||||
zoomChartVisibility = visibility
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
previewLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
|
||||
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = mainLinesRenderer.horizontalRange.current
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
self.lineBulletsRenerer.bullets = chartLines.compactMap { chart in
|
||||
return LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
|
||||
}
|
||||
self.lineBulletsRenerer.isEnabled = true
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
|
||||
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return mainLinesRenderer.horizontalRange.end
|
||||
}
|
||||
|
||||
override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
self.lineBulletsRenerer.isEnabled = false
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
var visibleCharts: [LinesChartRenderer.LineData] {
|
||||
let visibleCharts: [LinesChartRenderer.LineData] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartLines[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
updateHorizontalLimists(horizontalRange: horizontalRange, animated: true)
|
||||
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
lineBulletsRenerer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(verticalRange: range, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(verticalRange: range, animated: animated)
|
||||
lineBulletsRenerer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateHorizontalLimists(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
self.prevoiusHorizontalStrideInterval = stride
|
||||
}
|
||||
}
|
||||
|
||||
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: visibleCharts,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
|
||||
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: verticalRange)
|
||||
|
||||
if verticalScalesRenderer.verticalRange.end != range {
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated)
|
||||
updateMainChartVerticalRange(range: range, animated: animated)
|
||||
}
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
guard let previewVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: visibleCharts) else { return }
|
||||
|
||||
if previewLinesRenderer.verticalRange.end != previewVerticalRange {
|
||||
previewLinesRenderer.setup(verticalRange: previewVerticalRange, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
//
|
||||
// TwoAxisLinesChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private enum Constants {
|
||||
static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1]
|
||||
static let defaultRange: ClosedRange<CGFloat> = 0...1
|
||||
}
|
||||
|
||||
class TwoAxisLinesChartController: BaseLinesChartController {
|
||||
class GraphController {
|
||||
let mainLinesRenderer = LinesChartRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
let lineBulletsRenerer = LineBulletsRenerer()
|
||||
let previewLinesRenderer = LinesChartRenderer()
|
||||
|
||||
var chartLines: [LinesChartRenderer.LineData] = []
|
||||
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
|
||||
init() {
|
||||
self.mainLinesRenderer.lineWidth = 2
|
||||
self.previewLinesRenderer.lineWidth = 1
|
||||
self.lineBulletsRenerer.isEnabled = false
|
||||
|
||||
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
|
||||
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
lineBulletsRenerer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private var graphControllers: [GraphController] = []
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
|
||||
private let initialChartCollection: ChartsCollection
|
||||
|
||||
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||
|
||||
override init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
self.zoomChartVisibility = chartVisibility
|
||||
}
|
||||
|
||||
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
|
||||
|
||||
for (index, controller) in self.graphControllers.enumerated() {
|
||||
let chart = chartsCollection.chartValues[index]
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
let chartLines = [LinesChartRenderer.LineData(color: chart.color, points: points)]
|
||||
controller.chartLines = [LinesChartRenderer.LineData(color: chart.color, points: points)]
|
||||
controller.verticalScalesRenderer.labelsColor = chart.color
|
||||
controller.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
controller.lineBulletsRenerer.bullets = chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero,
|
||||
color: $0.color) }
|
||||
controller.previewLinesRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
||||
controller.previewLinesRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
||||
controller.mainLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
controller.previewLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
|
||||
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
||||
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
||||
}
|
||||
|
||||
self.prevoiusHorizontalStrideInterval = -1
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
updateHorizontalLimists(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.mainLinesRenderer } +
|
||||
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenerer] } +
|
||||
[horizontalScalesRenderer, verticalLineRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
override var navigationRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.previewLinesRenderer }
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
chartVisibility = visibility
|
||||
zoomChartVisibility = visibility
|
||||
let firstIndex = visibility.firstIndex(where: { $0 })
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
let graph = graphControllers[index]
|
||||
for graphIndex in graph.chartLines.indices {
|
||||
graph.mainLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.previewLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.lineBulletsRenerer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
}
|
||||
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
||||
if let firstIndex = firstIndex {
|
||||
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = currentHorizontalRange
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
for graphController in graphControllers {
|
||||
graphController.lineBulletsRenerer.bullets = graphController.chartLines.map { chart in
|
||||
LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
|
||||
}
|
||||
graphController.lineBulletsRenerer.isEnabled = true
|
||||
}
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return graphControllers.first?.mainLinesRenderer.horizontalRange.end ?? Constants.defaultRange
|
||||
}
|
||||
|
||||
override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
for graphController in graphControllers {
|
||||
graphController.lineBulletsRenerer.isEnabled = false
|
||||
}
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
updateHorizontalLimists(horizontalRange: horizontalRange, animated: true)
|
||||
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
for controller in graphControllers {
|
||||
controller.mainLinesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.lineBulletsRenerer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateHorizontalLimists(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
self.prevoiusHorizontalStrideInterval = stride
|
||||
}
|
||||
}
|
||||
|
||||
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
let chartHeight = chartFrame().height
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||
let (index, controller) = arg
|
||||
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) ?? controller.totalVerticalRange
|
||||
|
||||
var numberOfOffsetsPerItem = verticalRange.distance / approximateNumberOfChartValues
|
||||
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
let generalBase = Constants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
let base = generalBase * multiplier / dividor
|
||||
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let startValue = verticalValue
|
||||
var count = 0
|
||||
if chartVisibility[index] {
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
count += 1
|
||||
verticalValue += base
|
||||
}
|
||||
}
|
||||
return (startValue: startValue, base: base, count: count, maximumNumberOfDecimals: maximumNumberOfDecimals)
|
||||
}
|
||||
|
||||
let totalCount = dividorsAndMultiplers.map { $0.count }.max() ?? 0
|
||||
guard totalCount > 0 else { return }
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
for (index, controller) in graphControllers.enumerated() {
|
||||
|
||||
let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index]
|
||||
|
||||
let updatedRange = startValue...(startValue + base * CGFloat(totalCount))
|
||||
if controller.verticalScalesRenderer.verticalRange.end != updatedRange {
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
for multipler in 0...(totalCount - 1) {
|
||||
let verticalValue = startValue + base * CGFloat(multipler)
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
}
|
||||
|
||||
controller.verticalScalesRenderer.setup(verticalLimitsLabels: verticalLabels, animated: animated)
|
||||
controller.updateMainChartVerticalRange(range: updatedRange, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
|
||||
for controller in graphControllers {
|
||||
controller.verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
controller.lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
controller.verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// PercentChartComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PercentChartComponentController: GeneralChartComponentController {
|
||||
let mainPecentChartRenderer: PecentChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
let verticalLineRenderer: VerticalLinesRenderer
|
||||
let previewPercentChartRenderer: PecentChartRenderer
|
||||
var percentageData: PecentChartRenderer.PercentageData = .blank
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainPecentChartRenderer: PecentChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
verticalLineRenderer: VerticalLinesRenderer,
|
||||
previewPercentChartRenderer: PecentChartRenderer) {
|
||||
self.mainPecentChartRenderer = mainPecentChartRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.verticalLineRenderer = verticalLineRenderer
|
||||
self.previewPercentChartRenderer = previewPercentChartRenderer
|
||||
|
||||
super.init(isZoomed: isZoomed)
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let components = chartsCollection.chartValues.map { PecentChartRenderer.PercentageData.Component(color: $0.color,
|
||||
values: $0.values.map { CGFloat($0) }) }
|
||||
self.percentageData = PecentChartRenderer.PercentageData(locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
|
||||
components: components)
|
||||
let totalHorizontalRange = PecentChartRenderer.PercentageData.horizontalRange(data: self.percentageData) ?? BaseConstants.defaultRange
|
||||
let totalVerticalRange = BaseConstants.defaultRange
|
||||
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: totalVerticalRange)
|
||||
|
||||
mainPecentChartRenderer.percentageData = self.percentageData
|
||||
previewPercentChartRenderer.percentageData = self.percentageData
|
||||
|
||||
let axisValues: [CGFloat] = [0, 25, 50, 75, 100]
|
||||
let labels: [LinesChartLabel] = axisValues.map { value in
|
||||
return LinesChartLabel(value: value / 100, text: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: Double(value))) ?? "")
|
||||
}
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: false)
|
||||
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: false)
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: false)
|
||||
previewPercentChartRenderer.setup(verticalRange: totalVerticalRange, animated: false)
|
||||
previewPercentChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: false)
|
||||
}
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
previewPercentChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewPercentChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setConponentsVisible(visible: true, animated: true)
|
||||
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
super.chartRangeDidUpdated(updatedRange)
|
||||
|
||||
initialHorizontalRange = updatedRange
|
||||
setupMainChart(horizontalRange: updatedRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: true)
|
||||
}
|
||||
|
||||
func updateHorizontalLimitLabels(animated: Bool) {
|
||||
updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
horizontalRange: initialHorizontalRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
forceUpdate: false,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func prepareAppearanceAnimation(horizontalRnage: ClosedRange<CGFloat>) {
|
||||
setupMainChart(horizontalRange: horizontalRnage, animated: false)
|
||||
setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
func setConponentsVisible(visible: Bool, animated: Bool) {
|
||||
mainPecentChartRenderer.setVisible(visible, animated: animated)
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalLineRenderer.setVisible(visible, animated: animated)
|
||||
previewPercentChartRenderer.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainPecentChartRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainPecentChartRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainPecentChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
previewPercentChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
verticalScalesRenderer.setVisible(visibility.contains(true), animated: animated)
|
||||
}
|
||||
|
||||
override func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let visibleValues = visibleDetailsChartValues
|
||||
|
||||
let total = visibleValues.map { $0.values[pointIndex] }.reduce(0, +)
|
||||
|
||||
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100),
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
||||
color: component.color,
|
||||
visible: chartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: true,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in
|
||||
self?.hideDetailsView(animated: true)
|
||||
self?.zoomInOnDateClosure?(closestDate) })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
var currentlyVisiblePercentageData: PecentChartRenderer.PercentageData {
|
||||
var currentPercentageData = percentageData
|
||||
currentPercentageData.components = chartVisibility.enumerated().compactMap { $0.element ? currentPercentageData.components[$0.offset] : nil }
|
||||
return currentPercentageData
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return mainPecentChartRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewPercentChartRenderer
|
||||
}
|
||||
|
||||
override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
super.showDetailsView(at: chartPosition, detailsViewPosition: detailsViewPosition, dataIndex: dataIndex, date: date, animted: animted)
|
||||
verticalLineRenderer.values = [chartPosition]
|
||||
verticalLineRenderer.isEnabled = true
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
super.hideDetailsView(animated: animated)
|
||||
|
||||
verticalLineRenderer.values = []
|
||||
verticalLineRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.barChartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.barChartStrongLinesColor
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
//
|
||||
// PercentPieChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum PercentConstants {
|
||||
static let percentValueFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.positiveSuffix = "%"
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let zoomedRange = 7
|
||||
}
|
||||
|
||||
class PercentPieChartController: BaseChartController {
|
||||
let percentController: PercentChartComponentController
|
||||
let pieController: PieChartComponentController
|
||||
let transitionRenderer: PercentPieAnimationRenderer
|
||||
|
||||
override init(chartsCollection: ChartsCollection) {
|
||||
transitionRenderer = PercentPieAnimationRenderer()
|
||||
percentController = PercentChartComponentController(isZoomed: false,
|
||||
mainPecentChartRenderer: PecentChartRenderer(),
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer(),
|
||||
verticalScalesRenderer: VerticalScalesRenderer(),
|
||||
verticalLineRenderer: VerticalLinesRenderer(),
|
||||
previewPercentChartRenderer: PecentChartRenderer())
|
||||
pieController = PieChartComponentController(isZoomed: true,
|
||||
pieChartRenderer: PieChartRenderer(),
|
||||
previewBarChartRenderer: BarChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[percentController, pieController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
transitionRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [percentController.mainPecentChartRenderer,
|
||||
transitionRenderer,
|
||||
percentController.horizontalScalesRenderer,
|
||||
percentController.verticalScalesRenderer,
|
||||
percentController.verticalLineRenderer,
|
||||
pieController.pieChartRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [percentController.previewPercentChartRenderer,
|
||||
pieController.previewBarChartRenderer]
|
||||
}
|
||||
|
||||
override func initializeChart() {
|
||||
percentController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = pieController.initialHorizontalRange
|
||||
|
||||
pieController.updateChartsVisibility(visibility: percentController.chartVisibility, animated: false)
|
||||
pieController.pieChartRenderer.setup(horizontalRange: percentController.currentHorizontalMainChartRange, animated: false)
|
||||
pieController.previewBarChartRenderer.setup(horizontalRange: percentController.currentPreviewHorizontalRange, animated: false)
|
||||
pieController.pieChartRenderer.setVisible(false, animated: false)
|
||||
pieController.previewBarChartRenderer.setVisible(true, animated: false)
|
||||
|
||||
pieController.willAppear(animated: animated)
|
||||
percentController.willDisappear(animated: animated)
|
||||
|
||||
pieController.pieChartRenderer.drawPie = false
|
||||
percentController.mainPecentChartRenderer.isEnabled = false
|
||||
|
||||
setupTransitionRenderer()
|
||||
|
||||
percentController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
percentController.previewPercentChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated)
|
||||
percentController.setConponentsVisible(visible: false, animated: animated)
|
||||
|
||||
transitionRenderer.animate(fromDataToPie: true, animated: animated) { [weak self] in
|
||||
self?.pieController.pieChartRenderer.drawPie = true
|
||||
self?.percentController.mainPecentChartRenderer.isEnabled = true
|
||||
}
|
||||
} else {
|
||||
if !pieController.chartsCollection.isBlank {
|
||||
let fromHorizontalRange = pieController.currentHorizontalMainChartRange
|
||||
let toHorizontalRange = percentController.initialHorizontalRange
|
||||
|
||||
pieController.pieChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated)
|
||||
pieController.previewBarChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated)
|
||||
pieController.pieChartRenderer.setVisible(false, animated: animated)
|
||||
pieController.previewBarChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
percentController.updateChartsVisibility(visibility: pieController.chartVisibility, animated: false)
|
||||
percentController.setupMainChart(horizontalRange: fromHorizontalRange, animated: false)
|
||||
percentController.previewPercentChartRenderer.setup(horizontalRange: fromHorizontalRange, animated: false)
|
||||
percentController.setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
percentController.willAppear(animated: animated)
|
||||
pieController.willDisappear(animated: animated)
|
||||
|
||||
if animated {
|
||||
pieController.pieChartRenderer.drawPie = false
|
||||
percentController.mainPecentChartRenderer.isEnabled = false
|
||||
|
||||
setupTransitionRenderer()
|
||||
|
||||
transitionRenderer.animate(fromDataToPie: false, animated: true) {
|
||||
self.pieController.pieChartRenderer.drawPie = true
|
||||
self.percentController.mainPecentChartRenderer.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
}
|
||||
|
||||
func setupTransitionRenderer() {
|
||||
transitionRenderer.setup(verticalRange: percentController.currentVerticalMainChartRange, animated: false)
|
||||
transitionRenderer.setup(horizontalRange: percentController.currentHorizontalMainChartRange, animated: false)
|
||||
transitionRenderer.visiblePieComponents = pieController.visiblePieDataWithCurrentPreviewRange
|
||||
transitionRenderer.visiblePercentageData = percentController.currentlyVisiblePercentageData
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
pieController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
percentController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? pieController.chartVisibility : percentController.chartVisibility
|
||||
let collection = isZoomed ? pieController.chartsCollection : percentController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? pieController.chartVisibility : percentController.chartVisibility
|
||||
}
|
||||
|
||||
override var actualChartsCollection: ChartsCollection {
|
||||
return isZoomed ? pieController.chartsCollection : percentController.chartsCollection
|
||||
}
|
||||
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
pieController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
percentController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
pieController.chartInteractionDidEnd()
|
||||
} else {
|
||||
percentController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return pieController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return percentController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return pieController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return percentController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
cancelChartInteraction()
|
||||
let currentCollection = percentController.chartsCollection
|
||||
let range: Int = Constants.zoomedRange
|
||||
guard let (foundDate, index) = percentController.findClosestDateTo(dateToFind: date) else { return }
|
||||
var lowIndex = max(0, index - range / 2)
|
||||
var highIndex = min(currentCollection.axisValues.count - 1, index + range / 2)
|
||||
if lowIndex == 0 {
|
||||
highIndex = lowIndex + (range - 1)
|
||||
} else if highIndex == currentCollection.axisValues.count - 1 {
|
||||
lowIndex = highIndex - (range - 1)
|
||||
}
|
||||
|
||||
let newValues = currentCollection.chartValues.map { chart in
|
||||
return ChartsCollection.Chart(color: chart.color,
|
||||
name: chart.name,
|
||||
values: Array(chart.values[(lowIndex...highIndex)]))
|
||||
}
|
||||
let newCollection = ChartsCollection(axisValues: Array(currentCollection.axisValues[(lowIndex...highIndex)]),
|
||||
chartValues: newValues)
|
||||
let selectedRange = CGFloat(foundDate.timeIntervalSince1970 - .day)...CGFloat(foundDate.timeIntervalSince1970)
|
||||
pieController.initialize(chartsCollection: newCollection, initialDate: date, totalHorizontalRange: 0...1, totalVerticalRange: 0...1)
|
||||
pieController.initialHorizontalRange = selectedRange
|
||||
|
||||
switchToChart(chartsCollection: newCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
|
||||
override func didTapZoomOut() {
|
||||
self.pieController.deselectSegment(completion: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.switchToChart(chartsCollection: self.percentController.chartsCollection, isZoomed: false, animated: true)
|
||||
})
|
||||
}
|
||||
|
||||
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return pieController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return percentController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
pieController.apply(colorMode: colorMode, animated: animated)
|
||||
percentController.apply(colorMode: colorMode, animated: animated)
|
||||
transitionRenderer.backgroundColor = colorMode.chartBackgroundColor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
//
|
||||
// PieChartComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PieChartComponentController: GeneralChartComponentController {
|
||||
let pieChartRenderer: PieChartRenderer
|
||||
let previewBarChartRenderer: BarChartRenderer
|
||||
var barWidth: CGFloat = 1
|
||||
|
||||
var chartBars: BarChartRenderer.BarsData = .blank
|
||||
|
||||
init(isZoomed: Bool,
|
||||
pieChartRenderer: PieChartRenderer,
|
||||
previewBarChartRenderer: BarChartRenderer) {
|
||||
self.pieChartRenderer = pieChartRenderer
|
||||
self.previewBarChartRenderer = previewBarChartRenderer
|
||||
super.init(isZoomed: isZoomed)
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (width, chartBars, totalHorizontalRange, _) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
self.barWidth = width
|
||||
self.chartBars = chartBars
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
|
||||
self.previewBarChartRenderer.bars = chartBars
|
||||
self.previewBarChartRenderer.fillToTop = true
|
||||
|
||||
pieChartRenderer.valuesFormatter = PercentConstants.percentValueFormatter
|
||||
pieChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: false)
|
||||
previewBarChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
previewBarChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: false)
|
||||
|
||||
pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: false)
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: false)
|
||||
}
|
||||
|
||||
private var pieDataWithCurrentPreviewRange: [PieChartRenderer.PieComponent] {
|
||||
let range = currentHorizontalMainChartRange
|
||||
var pieComponents = chartsCollection.chartValues.map { PieChartRenderer.PieComponent(color: $0.color,
|
||||
value: 0) }
|
||||
guard var valueIndex = chartsCollection.axisValues.firstIndex(where: { CGFloat($0.timeIntervalSince1970) > (range.lowerBound + 1)}) else {
|
||||
return pieComponents
|
||||
}
|
||||
var count = 0
|
||||
while valueIndex < chartsCollection.axisValues.count, CGFloat(chartsCollection.axisValues[valueIndex].timeIntervalSince1970) <= range.upperBound {
|
||||
count += 1
|
||||
for pieIndex in pieComponents.indices {
|
||||
pieComponents[pieIndex].value += CGFloat(chartsCollection.chartValues[pieIndex].values[valueIndex])
|
||||
}
|
||||
valueIndex += 1
|
||||
}
|
||||
return pieComponents
|
||||
}
|
||||
|
||||
var visiblePieDataWithCurrentPreviewRange: [PieChartRenderer.PieComponent] {
|
||||
let currentData = pieDataWithCurrentPreviewRange
|
||||
return chartVisibility.enumerated().compactMap { $0.element ? currentData[$0.offset] : nil }
|
||||
}
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
pieChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
pieChartRenderer.setVisible(true, animated: animated)
|
||||
|
||||
previewBarChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewBarChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
previewBarChartRenderer.setVisible(true, animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: false)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
}
|
||||
|
||||
override func setupChartRangePaging() {
|
||||
let valuesCount = chartsCollection.axisValues.count
|
||||
guard valuesCount > 0 else { return }
|
||||
chartRangePagingClosure?(true, 1.0 / CGFloat(valuesCount))
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
initialHorizontalRange = updatedRange
|
||||
|
||||
setupMainChart(horizontalRange: updatedRange, animated: true)
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
pieChartRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: animated)
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
pieChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
previewBarChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
if let segment = pieChartRenderer.selectedSegment {
|
||||
if !visibility[segment] {
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: true)
|
||||
}
|
||||
}
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
|
||||
func deselectSegment(completion: @escaping () -> Void) {
|
||||
if pieChartRenderer.hasSelectedSegments {
|
||||
hideDetailsView(animated: true)
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration / 2) {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectedDataLabelIfNeeded() {
|
||||
if let segment = pieChartRenderer.selectedSegment {
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(segmentInde: segment), false)
|
||||
self.setDetailsViewPositionClosure?(chartFrame().width / 4)
|
||||
} else {
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
}
|
||||
}
|
||||
|
||||
func chartDetailsViewModel(segmentInde: Int) -> ChartDetailsViewModel {
|
||||
let pieItem = pieDataWithCurrentPreviewRange[segmentInde]
|
||||
let title = chartsCollection.chartValues[segmentInde].name
|
||||
let valueString = BaseConstants.detailsNumberFormatter.string(from: pieItem.value)
|
||||
let viewModel = ChartDetailsViewModel(title: "",
|
||||
showArrow: false,
|
||||
showPrefixes: false,
|
||||
values: [ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: title,
|
||||
value: valueString,
|
||||
color: pieItem.color,
|
||||
visible: true)],
|
||||
totalValue: nil,
|
||||
tapAction: nil)
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return pieChartRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewBarChartRenderer
|
||||
}
|
||||
|
||||
var lastInteractionPoint: CGPoint = .zero
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
lastInteractionPoint = point
|
||||
}
|
||||
|
||||
override func chartInteractionDidEnd() {
|
||||
if let segment = pieChartRenderer.selectedItemIndex(at: lastInteractionPoint) {
|
||||
if pieChartRenderer.selectedSegment == segment {
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: true)
|
||||
} else {
|
||||
pieChartRenderer.selectSegmentAt(at: segment, animated: true)
|
||||
}
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: animated)
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
|
||||
override func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound) + .day + 1)
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
//
|
||||
// BarsComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BarsComponentController: GeneralChartComponentController {
|
||||
let mainBarsRenderer: BarChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
|
||||
let previewBarsChartRenderer: BarChartRenderer
|
||||
private(set) var barsWidth: CGFloat = 1
|
||||
|
||||
private (set) var chartBars: BarChartRenderer.BarsData = .blank
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainBarsRenderer: BarChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer) {
|
||||
self.mainBarsRenderer = mainBarsRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.previewBarsChartRenderer = previewBarsChartRenderer
|
||||
|
||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
|
||||
super.init(isZoomed: isZoomed)
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
self.chartBars = chartBars
|
||||
self.barsWidth = width
|
||||
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: totalVerticalRange)
|
||||
}
|
||||
|
||||
override func setupInitialChartRange(initialDate: Date) {
|
||||
guard let first = chartsCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = chartsCollection.axisValues.last?.timeIntervalSince1970 else { return }
|
||||
|
||||
let rangeStart = CGFloat(first)
|
||||
let rangeEnd = CGFloat(last)
|
||||
|
||||
if isZoomed {
|
||||
let initalDate = CGFloat(initialDate.timeIntervalSince1970)
|
||||
|
||||
initialHorizontalRange = max(initalDate - barsWidth, rangeStart)...min(initalDate + GeneralChartComponentConstants.defaultZoomedRangeLength - barsWidth, rangeEnd)
|
||||
initialVerticalRange = totalVerticalRange
|
||||
} else {
|
||||
super.setupInitialChartRange(initialDate: initialDate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
mainBarsRenderer.bars = self.chartBars
|
||||
previewBarsChartRenderer.bars = self.chartBars
|
||||
|
||||
previewBarsChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
setConponentsVisible(visible: true, animated: animated)
|
||||
updateHorizontalLimitLabels(animated: animated, forceUpdate: true)
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
super.chartRangeDidUpdated(updatedRange)
|
||||
if !isZoomed {
|
||||
initialHorizontalRange = updatedRange
|
||||
}
|
||||
setupMainChart(horizontalRange: updatedRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: true, forceUpdate: false)
|
||||
updateChartVerticalRanges(horizontalRange: updatedRange, animated: true)
|
||||
}
|
||||
|
||||
func updateHorizontalLimitLabels(animated: Bool, forceUpdate: Bool) {
|
||||
updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
horizontalRange: currentHorizontalMainChartRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
forceUpdate: forceUpdate,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func prepareAppearanceAnimation(horizontalRnage: ClosedRange<CGFloat>) {
|
||||
setupMainChart(horizontalRange: horizontalRnage, animated: false)
|
||||
setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
func setConponentsVisible(visible: Bool, animated: Bool) {
|
||||
mainBarsRenderer.setVisible(visible, animated: animated)
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
previewBarsChartRenderer.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
var visibleBars: BarChartRenderer.BarsData {
|
||||
let visibleComponents: [BarChartRenderer.BarsData.Component] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartBars.components[args.offset] : nil
|
||||
}
|
||||
return BarChartRenderer.BarsData(barWidth: chartBars.barWidth,
|
||||
locations: chartBars.locations,
|
||||
components: visibleComponents)
|
||||
}
|
||||
|
||||
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
||||
if verticalScalesRenderer.verticalRange.end != range {
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated)
|
||||
}
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: range, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars) {
|
||||
previewBarsChartRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainBarsRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
previewBarsChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true)
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibleCharts: [ChartsCollection.Chart] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartsCollection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
override func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
||||
let visibleChartValues = self.visibleChartValues
|
||||
let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
||||
|
||||
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: "Total",
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||
color: .white,
|
||||
visible: visibleChartValues.count > 1)
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return mainBarsRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewBarsChartRenderer
|
||||
}
|
||||
|
||||
override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
let rangeWithOffset = detailsViewPosition - barsWidth / currentHorizontalMainChartRange.distance * chartFrame().width / 2
|
||||
super.showDetailsView(at: chartPosition, detailsViewPosition: rangeWithOffset, dataIndex: dataIndex, date: date, animted: animted)
|
||||
mainBarsRenderer.setSelectedIndex(dataIndex, animated: true)
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
super.hideDetailsView(animated: animated)
|
||||
|
||||
mainBarsRenderer.setSelectedIndex(nil, animated: animated)
|
||||
}
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.barChartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.barChartStrongLinesColor
|
||||
mainBarsRenderer.update(backgroundColor: colorMode.chartBackgroundColor, animated: false)
|
||||
previewBarsChartRenderer.update(backgroundColor: colorMode.chartBackgroundColor, animated: false)
|
||||
}
|
||||
|
||||
override func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound + barsWidth))
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
//
|
||||
// DailyBarsChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class DailyBarsChartController: BaseChartController {
|
||||
let barsController: BarsComponentController
|
||||
let linesController: LinesComponentController
|
||||
|
||||
override init(chartsCollection: ChartsCollection) {
|
||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
barsController = BarsComponentController(isZoomed: false,
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
linesController = LinesComponentController(isZoomed: true,
|
||||
userLinesTransitionAnimation: false,
|
||||
mainLinesRenderer: LinesChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
verticalLineRenderer: VerticalLinesRenderer(),
|
||||
lineBulletsRenerer: LineBulletsRenerer(),
|
||||
previewLinesChartRenderer: LinesChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[barsController, linesController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [barsController.mainBarsRenderer,
|
||||
linesController.mainLinesRenderer,
|
||||
barsController.horizontalScalesRenderer,
|
||||
barsController.verticalScalesRenderer,
|
||||
linesController.verticalLineRenderer,
|
||||
linesController.lineBulletsRenerer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [barsController.previewBarsChartRenderer,
|
||||
linesController.previewLinesChartRenderer]
|
||||
}
|
||||
|
||||
override func initializeChart() {
|
||||
barsController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false)
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = linesController.initialHorizontalRange
|
||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||
let initialChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
linesController.mainLinesRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
linesController.previewLinesChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||
linesController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
linesController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
linesController.mainLinesRenderer.setVisible(false, animated: false)
|
||||
linesController.previewLinesChartRenderer.setVisible(false, animated: false)
|
||||
|
||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setup(horizontalRange: linesController.totalHorizontalRange, animated: animated)
|
||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
linesController.willAppear(animated: animated)
|
||||
barsController.willDisappear(animated: animated)
|
||||
|
||||
linesController.updateChartsVisibility(visibility: linesController.chartLines.map { _ in true }, animated: false)
|
||||
} else {
|
||||
if !linesController.chartsCollection.isBlank {
|
||||
barsController.hideDetailsView(animated: false)
|
||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||
|
||||
let toHorizontalRange = barsController.initialHorizontalRange
|
||||
let destinationChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
linesController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
linesController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
linesController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
linesController.previewLinesChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||
linesController.mainLinesRenderer.setVisible(false, animated: animated)
|
||||
linesController.previewLinesChartRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
barsController.willAppear(animated: animated)
|
||||
linesController.willDisappear(animated: animated)
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
self.refreshChartToolsClosure?(animated)
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
linesController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
barsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? linesController.chartVisibility : barsController.chartVisibility
|
||||
let collection = isZoomed ? linesController.chartsCollection : barsController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? linesController.chartVisibility : barsController.chartVisibility
|
||||
}
|
||||
|
||||
override var actualChartsCollection: ChartsCollection {
|
||||
return isZoomed ? linesController.chartsCollection : barsController.chartsCollection
|
||||
}
|
||||
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
linesController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
barsController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
linesController.chartInteractionDidEnd()
|
||||
} else {
|
||||
barsController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return linesController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return barsController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return linesController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return barsController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
if isZoomed {
|
||||
return linesController.hideDetailsView(animated: true)
|
||||
}
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.linesController.initialize(chartsCollection: updatedCollection,
|
||||
initialDate: date,
|
||||
totalHorizontalRange: 0...1,
|
||||
totalVerticalRange: 0...1)
|
||||
self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func lineProportionAnimationRange() -> ClosedRange<CGFloat> {
|
||||
let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.linesController.chartLines[$0.offset] : nil }
|
||||
let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange
|
||||
let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars,
|
||||
calculatingRange: self.linesController.totalHorizontalRange) ?? BaseConstants.defaultRange
|
||||
let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance)
|
||||
return range
|
||||
}
|
||||
|
||||
override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||
}
|
||||
|
||||
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return linesController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
linesController.apply(colorMode: colorMode, animated: animated)
|
||||
barsController.apply(colorMode: colorMode, animated: animated)
|
||||
}
|
||||
|
||||
override var drawChartVisibity: Bool {
|
||||
return isZoomed
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
||||
//TODO: Добавить ховеры на кнопки
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
//
|
||||
// LinesComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class LinesComponentController: GeneralChartComponentController {
|
||||
let mainLinesRenderer: LinesChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
let verticalLineRenderer: VerticalLinesRenderer
|
||||
let lineBulletsRenerer: LineBulletsRenerer
|
||||
|
||||
let previewLinesChartRenderer: LinesChartRenderer
|
||||
|
||||
private let zoomedLinesRenderer = LinesChartRenderer()
|
||||
private let zoomedPreviewLinesRenderer = LinesChartRenderer()
|
||||
|
||||
private let userLinesTransitionAnimation: Bool
|
||||
|
||||
private(set) var chartLines: [LinesChartRenderer.LineData] = []
|
||||
|
||||
init(isZoomed: Bool,
|
||||
userLinesTransitionAnimation: Bool,
|
||||
mainLinesRenderer: LinesChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
verticalLineRenderer: VerticalLinesRenderer,
|
||||
lineBulletsRenerer: LineBulletsRenerer,
|
||||
previewLinesChartRenderer: LinesChartRenderer) {
|
||||
self.mainLinesRenderer = mainLinesRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.verticalLineRenderer = verticalLineRenderer
|
||||
self.lineBulletsRenerer = lineBulletsRenerer
|
||||
self.previewLinesChartRenderer = previewLinesChartRenderer
|
||||
self.userLinesTransitionAnimation = userLinesTransitionAnimation
|
||||
|
||||
super.init(isZoomed: isZoomed)
|
||||
|
||||
self.mainLinesRenderer.lineWidth = BaseConstants.mainChartLineWidth
|
||||
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
|
||||
self.previewLinesChartRenderer.lineWidth = BaseConstants.previewChartLineWidth
|
||||
self.previewLinesChartRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
|
||||
|
||||
self.lineBulletsRenerer.isEnabled = false
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection,
|
||||
initialDate: Date,
|
||||
totalHorizontalRange _: ClosedRange<CGFloat>,
|
||||
totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (chartLines, totalHorizontalRange, totalVerticalRange) = LinesChartRenderer.LineData.initialComponents(chartsCollection: chartsCollection)
|
||||
self.chartLines = chartLines
|
||||
|
||||
self.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero,
|
||||
color: $0.color)}
|
||||
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: totalVerticalRange)
|
||||
|
||||
self.mainLinesRenderer.setup(verticalRange: totalVerticalRange, animated: true)
|
||||
}
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
mainLinesRenderer.setLines(lines: self.chartLines, animated: animated && userLinesTransitionAnimation)
|
||||
previewLinesChartRenderer.setLines(lines: self.chartLines, animated: animated && userLinesTransitionAnimation)
|
||||
|
||||
previewLinesChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewLinesChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
setConponentsVisible(visible: true, animated: animated)
|
||||
updateHorizontalLimitLabels(animated: animated, forceUpdate: true)
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
super.chartRangeDidUpdated(updatedRange)
|
||||
if !isZoomed {
|
||||
initialHorizontalRange = updatedRange
|
||||
}
|
||||
setupMainChart(horizontalRange: updatedRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: true, forceUpdate: false)
|
||||
updateChartVerticalRanges(horizontalRange: updatedRange, animated: true)
|
||||
}
|
||||
|
||||
func updateHorizontalLimitLabels(animated: Bool, forceUpdate: Bool) {
|
||||
updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
horizontalRange: currentHorizontalMainChartRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
forceUpdate: forceUpdate,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func prepareAppearanceAnimation(horizontalRnage: ClosedRange<CGFloat>) {
|
||||
setupMainChart(horizontalRange: horizontalRnage, animated: false)
|
||||
setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
func setConponentsVisible(visible: Bool, animated: Bool) {
|
||||
mainLinesRenderer.setVisible(visible, animated: animated)
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalLineRenderer.setVisible(visible, animated: animated)
|
||||
previewLinesChartRenderer.setVisible(visible, animated: animated)
|
||||
lineBulletsRenerer.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
lineBulletsRenerer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
var visibleLines: [LinesChartRenderer.LineData] {
|
||||
return chartVisibility.enumerated().compactMap { $0.element ? chartLines[$0.offset] : nil }
|
||||
}
|
||||
|
||||
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let range = LinesChartRenderer.LineData.verticalRange(lines: visibleLines,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
||||
if verticalScalesRenderer.verticalRange.end != range {
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated)
|
||||
}
|
||||
|
||||
setupMainChart(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
if let range = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) {
|
||||
previewLinesChartRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
lineBulletsRenerer.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
previewLinesChartRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true)
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return mainLinesRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewLinesChartRenderer
|
||||
}
|
||||
|
||||
override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
super.showDetailsView(at: chartPosition, detailsViewPosition: detailsViewPosition, dataIndex: dataIndex, date: date, animted: animted)
|
||||
verticalLineRenderer.values = [chartPosition]
|
||||
verticalLineRenderer.isEnabled = true
|
||||
|
||||
lineBulletsRenerer.isEnabled = true
|
||||
lineBulletsRenerer.setVisible(true, animated: animted)
|
||||
lineBulletsRenerer.bullets = chartLines.compactMap { chart in
|
||||
return LineBulletsRenerer.Bullet(coordinate: chart.points[dataIndex], color: chart.color)
|
||||
}
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
super.hideDetailsView(animated: animated)
|
||||
|
||||
verticalLineRenderer.values = []
|
||||
verticalLineRenderer.isEnabled = false
|
||||
lineBulletsRenerer.isEnabled = false
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
//
|
||||
// StackedBarsChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class StackedBarsChartController: BaseChartController {
|
||||
let barsController: BarsComponentController
|
||||
let zoomedBarsController: BarsComponentController
|
||||
|
||||
override init(chartsCollection: ChartsCollection) {
|
||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
barsController = BarsComponentController(isZoomed: false,
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
zoomedBarsController = BarsComponentController(isZoomed: true,
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[barsController, zoomedBarsController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [barsController.mainBarsRenderer,
|
||||
zoomedBarsController.mainBarsRenderer,
|
||||
barsController.horizontalScalesRenderer,
|
||||
barsController.verticalScalesRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [barsController.previewBarsChartRenderer,
|
||||
zoomedBarsController.previewBarsChartRenderer]
|
||||
}
|
||||
|
||||
override func initializeChart() {
|
||||
barsController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false)
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
|
||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||
let verticalVisibleRange = barsController.currentVerticalMainChartRange
|
||||
let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||
|
||||
zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false)
|
||||
|
||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
|
||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
zoomedBarsController.willAppear(animated: animated)
|
||||
barsController.willDisappear(animated: animated)
|
||||
|
||||
zoomedBarsController.updateChartsVisibility(visibility: barsController.chartVisibility, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut)
|
||||
} else {
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.hideDetailsView(animated: false)
|
||||
barsController.chartVisibility = zoomedBarsController.chartVisibility
|
||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||
|
||||
let toHorizontalRange = barsController.initialHorizontalRange
|
||||
|
||||
let verticalVisibleRange = barsController.initialVerticalRange
|
||||
let targetVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||
|
||||
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in
|
||||
self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
||||
self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
barsController.willAppear(animated: animated)
|
||||
zoomedBarsController.willDisappear(animated: animated)
|
||||
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
}
|
||||
|
||||
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
barsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
|
||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
|
||||
}
|
||||
|
||||
override var actualChartsCollection: ChartsCollection {
|
||||
return isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
}
|
||||
|
||||
override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
barsController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidEnd()
|
||||
} else {
|
||||
barsController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return barsController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return barsController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
if isZoomed {
|
||||
return zoomedBarsController.hideDetailsView(animated: true)
|
||||
}
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.zoomedBarsController.initialize(chartsCollection: updatedCollection,
|
||||
initialDate: date,
|
||||
totalHorizontalRange: 0...1,
|
||||
totalVerticalRange: 0...1)
|
||||
self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||
}
|
||||
|
||||
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
override func apply(colorMode: ColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
zoomedBarsController.apply(colorMode: colorMode, animated: animated)
|
||||
barsController.apply(colorMode: colorMode, animated: animated)
|
||||
}
|
||||
}
|
||||
293
submodules/Charts/Sources/Charts/Renderes/BarChartRenderer.swift
Normal file
293
submodules/Charts/Sources/Charts/Renderes/BarChartRenderer.swift
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
//
|
||||
// BarChartRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BarChartRenderer: BaseChartRenderer {
|
||||
struct BarsData {
|
||||
static let blank = BarsData(barWidth: 1, locations: [], components: [])
|
||||
var barWidth: CGFloat
|
||||
var locations: [CGFloat]
|
||||
var components: [Component]
|
||||
|
||||
struct Component {
|
||||
var color: UIColor
|
||||
var values: [CGFloat]
|
||||
}
|
||||
}
|
||||
|
||||
var fillToTop: Bool = false
|
||||
private(set) lazy var selectedIndexAnimator: AnimationController<CGFloat> = {
|
||||
return AnimationController(current: 0, refreshClosure: self.refreshClosure)
|
||||
}()
|
||||
func setSelectedIndex(_ index: Int?, animated: Bool) {
|
||||
let destinationValue: CGFloat = (index == nil) ? 0 : 1
|
||||
if animated {
|
||||
if index != nil {
|
||||
selectedBarIndex = index
|
||||
}
|
||||
self.selectedIndexAnimator.completionClosure = {
|
||||
self.selectedBarIndex = index
|
||||
}
|
||||
guard self.selectedIndexAnimator.end != destinationValue else { return }
|
||||
self.selectedIndexAnimator.animate(to: destinationValue, duration: .defaultDuration)
|
||||
} else {
|
||||
self.selectedIndexAnimator.set(current: destinationValue)
|
||||
self.selectedBarIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedBarIndex: Int? {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var generalUnselectedAlpha: CGFloat = 0.5
|
||||
|
||||
private var componentsAnimators: [AnimationController<CGFloat>] = []
|
||||
var bars: BarsData = BarsData(barWidth: 1, locations: [], components: []) {
|
||||
willSet {
|
||||
if bars.components.count != newValue.components.count {
|
||||
componentsAnimators = newValue.components.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
|
||||
}
|
||||
}
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
private lazy var backgroundColorAnimator = AnimationController(current: UIColorContainer(color: .white), refreshClosure: refreshClosure)
|
||||
func update(backgroundColor: UIColor, animated: Bool) {
|
||||
if animated {
|
||||
backgroundColorAnimator.animate(to: UIColorContainer(color: backgroundColor), duration: .defaultDuration)
|
||||
} else {
|
||||
backgroundColorAnimator.set(current: UIColorContainer(color: backgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let chartsAlpha = chartAlphaAnimator.current
|
||||
if chartsAlpha == 0 { return }
|
||||
|
||||
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
|
||||
|
||||
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
|
||||
var unselectedPaths: [[CGRect]] = bars.components.map { _ in [] }
|
||||
|
||||
if var barIndex = bars.locations.firstIndex(where: { $0 >= range.lowerBound }) {
|
||||
if fillToTop {
|
||||
barIndex = max(0, barIndex - 1)
|
||||
|
||||
while barIndex < bars.locations.count {
|
||||
let currentLocation = bars.locations[barIndex]
|
||||
let right = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid()
|
||||
let left = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame).roundedUpToPixelGrid()
|
||||
|
||||
var summ: CGFloat = 0
|
||||
for (index, component) in bars.components.enumerated() {
|
||||
summ += componentsAnimators[index].current * component.values[barIndex]
|
||||
}
|
||||
guard summ > 0 else {
|
||||
barIndex += 1
|
||||
continue
|
||||
}
|
||||
|
||||
var stackedValue: CGFloat = 0
|
||||
for (index, component) in bars.components.enumerated() {
|
||||
let visibilityPercent = componentsAnimators[index].current
|
||||
if visibilityPercent == 0 { continue }
|
||||
|
||||
let bottomFraction = stackedValue
|
||||
let topFraction = stackedValue + ((component.values[barIndex] * visibilityPercent) / summ)
|
||||
|
||||
let rect = CGRect(x: left,
|
||||
y: chartFrame.maxY - chartFrame.height * topFraction,
|
||||
width: right - left,
|
||||
height: chartFrame.height * (topFraction - bottomFraction))
|
||||
if selectedBarIndex == barIndex {
|
||||
selectedPaths[index].append(rect)
|
||||
} else {
|
||||
unselectedPaths[index].append(rect)
|
||||
}
|
||||
stackedValue = topFraction
|
||||
}
|
||||
if currentLocation > range.upperBound {
|
||||
break
|
||||
}
|
||||
barIndex += 1
|
||||
}
|
||||
|
||||
for (index, component) in bars.components.enumerated() {
|
||||
context.saveGState()
|
||||
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor)
|
||||
context.fill(selectedPaths[index])
|
||||
let resultAlpha: CGFloat = 1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current
|
||||
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue * resultAlpha).cgColor)
|
||||
context.fill(unselectedPaths[index])
|
||||
context.restoreGState()
|
||||
}
|
||||
} else {
|
||||
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
|
||||
barIndex = max(0, barIndex - 1)
|
||||
|
||||
var currentLocation = bars.locations[barIndex]
|
||||
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
|
||||
var rightX: CGFloat = 0
|
||||
|
||||
let startPoint = CGPoint(x: leftX,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
|
||||
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
||||
for path in backgourndPaths.indices {
|
||||
backgourndPaths[path].reserveCapacity(itemsCount)
|
||||
backgourndPaths[path].append(startPoint)
|
||||
}
|
||||
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
||||
while barIndex < bars.locations.count {
|
||||
currentLocation = bars.locations[barIndex]
|
||||
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
|
||||
|
||||
var stackedValue: CGFloat = 0
|
||||
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
|
||||
for (index, component) in bars.components.enumerated() {
|
||||
let visibilityPercent = componentsAnimators[index].current
|
||||
if visibilityPercent == 0 { continue }
|
||||
|
||||
let height = component.values[barIndex] * visibilityPercent
|
||||
stackedValue += height
|
||||
let topY = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
|
||||
let componentHeight = (bottomY - topY)
|
||||
maxValues[index] = max(maxValues[index], componentHeight)
|
||||
if selectedBarIndex == barIndex {
|
||||
let rect = CGRect(x: leftX,
|
||||
y: topY,
|
||||
width: rightX - leftX,
|
||||
height: componentHeight)
|
||||
selectedPaths[index].append(rect)
|
||||
}
|
||||
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||
bottomY = topY
|
||||
}
|
||||
if currentLocation > range.upperBound {
|
||||
break
|
||||
}
|
||||
leftX = rightX
|
||||
barIndex += 1
|
||||
}
|
||||
|
||||
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
|
||||
|
||||
for (index, component) in bars.components.enumerated().reversed() {
|
||||
if maxValues[index] < optimizationLevel {
|
||||
continue
|
||||
}
|
||||
context.saveGState()
|
||||
backgourndPaths[index].append(endPoint)
|
||||
|
||||
context.setFillColor(UIColor.valueBetween(start: backgroundColorAnimator.current.color,
|
||||
end: component.color,
|
||||
offset: colorOffset).cgColor)
|
||||
context.beginPath()
|
||||
context.addLines(between: backgourndPaths[index])
|
||||
context.closePath()
|
||||
context.fillPath()
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
for (index, component) in bars.components.enumerated().reversed() {
|
||||
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor)
|
||||
context.fill(selectedPaths[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BarChartRenderer.BarsData {
|
||||
static func initialComponents(chartsCollection: ChartsCollection) ->
|
||||
(width: CGFloat,
|
||||
chartBars: BarChartRenderer.BarsData,
|
||||
totalHorizontalRange: ClosedRange<CGFloat>,
|
||||
totalVerticalRange: ClosedRange<CGFloat>) {
|
||||
let width: CGFloat
|
||||
if chartsCollection.axisValues.count > 1 {
|
||||
width = CGFloat(abs(chartsCollection.axisValues[1].timeIntervalSince1970 - chartsCollection.axisValues[0].timeIntervalSince1970))
|
||||
} else {
|
||||
width = 1
|
||||
}
|
||||
let components = chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
|
||||
values: $0.values.map { CGFloat($0) }) }
|
||||
let chartBars = BarChartRenderer.BarsData(barWidth: width,
|
||||
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
|
||||
components: components)
|
||||
|
||||
|
||||
|
||||
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1
|
||||
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
|
||||
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
|
||||
}
|
||||
|
||||
static func visibleHorizontalRange(bars: BarChartRenderer.BarsData, width: CGFloat) -> ClosedRange<CGFloat>? {
|
||||
guard let firstPoint = bars.locations.first,
|
||||
let lastPoint = bars.locations.last,
|
||||
firstPoint <= lastPoint else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (firstPoint - width)...lastPoint
|
||||
}
|
||||
|
||||
static func verticalRange(bars: BarChartRenderer.BarsData, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
||||
guard bars.components.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
if let calculatingRange = calculatingRange {
|
||||
guard var index = bars.locations.firstIndex(where: { $0 >= calculatingRange.lowerBound && $0 <= calculatingRange.upperBound }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var vMax: CGFloat = bars.components[0].values[index]
|
||||
while index < bars.locations.count {
|
||||
var summ: CGFloat = 0
|
||||
for component in bars.components {
|
||||
summ += component.values[index]
|
||||
}
|
||||
vMax = max(vMax, summ)
|
||||
|
||||
if bars.locations[index] > calculatingRange.upperBound {
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return 0...vMax
|
||||
} else {
|
||||
var index = 0
|
||||
|
||||
var vMax: CGFloat = bars.components[0].values[index]
|
||||
while index < bars.locations.count {
|
||||
var summ: CGFloat = 0
|
||||
for component in bars.components {
|
||||
summ += component.values[index]
|
||||
}
|
||||
vMax = max(vMax, summ)
|
||||
index += 1
|
||||
}
|
||||
return 0...vMax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// BaseChartRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private let exponentialAnimationTrashold: CGFloat = 100
|
||||
|
||||
class BaseChartRenderer: ChartViewRenderer {
|
||||
var containerViews: [UIView] = []
|
||||
|
||||
var optimizationLevel: CGFloat = 1 {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var isEnabled: Bool = true {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) lazy var chartAlphaAnimator: AnimationController<CGFloat> = {
|
||||
return AnimationController(current: 1, refreshClosure: self.refreshClosure)
|
||||
}()
|
||||
func setVisible(_ visible: Bool, animated: Bool) {
|
||||
let destinationValue: CGFloat = visible ? 1 : 0
|
||||
guard self.chartAlphaAnimator.end != destinationValue else { return }
|
||||
if animated {
|
||||
self.chartAlphaAnimator.animate(to: destinationValue, duration: .defaultDuration)
|
||||
} else {
|
||||
self.chartAlphaAnimator.set(current: destinationValue)
|
||||
}
|
||||
}
|
||||
|
||||
lazy var horizontalRange = AnimationController<ClosedRange<CGFloat>>(current: 0...1, refreshClosure: refreshClosure)
|
||||
lazy var verticalRange = AnimationController<ClosedRange<CGFloat>>(current: 0...1, refreshClosure: refreshClosure)
|
||||
|
||||
func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
|
||||
guard self.verticalRange.end != verticalRange else {
|
||||
self.verticalRange.timeFunction = timeFunction ?? .linear
|
||||
return
|
||||
}
|
||||
if animated {
|
||||
let function: TimeFunction
|
||||
if let timeFunction = timeFunction {
|
||||
function = timeFunction
|
||||
} else if self.verticalRange.current.distance > 0 && verticalRange.distance > 0 {
|
||||
if self.verticalRange.current.distance / verticalRange.distance > exponentialAnimationTrashold {
|
||||
function = .easeIn
|
||||
} else if verticalRange.distance / self.verticalRange.current.distance > exponentialAnimationTrashold {
|
||||
function = .easeOut
|
||||
} else {
|
||||
function = .linear
|
||||
}
|
||||
} else {
|
||||
function = .linear
|
||||
}
|
||||
|
||||
self.verticalRange.animate(to: verticalRange, duration: .defaultDuration, timeFunction: function)
|
||||
} else {
|
||||
self.verticalRange.set(current: verticalRange)
|
||||
}
|
||||
}
|
||||
|
||||
func setup(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
guard self.horizontalRange.end != horizontalRange else { return }
|
||||
if animated {
|
||||
let animationCurve: TimeFunction = self.horizontalRange.current.distance > horizontalRange.distance ? .easeOut : .easeIn
|
||||
self.horizontalRange.animate(to: horizontalRange, duration: .defaultDuration, timeFunction: animationCurve)
|
||||
} else {
|
||||
self.horizontalRange.set(current: horizontalRange)
|
||||
}
|
||||
}
|
||||
|
||||
func transform(toChartCoordinateHorizontal x: CGFloat, chartFrame: CGRect) -> CGFloat {
|
||||
return chartFrame.origin.x + (x - horizontalRange.current.lowerBound) / horizontalRange.current.distance * chartFrame.width
|
||||
}
|
||||
|
||||
func transform(toChartCoordinateVertical y: CGFloat, chartFrame: CGRect) -> CGFloat {
|
||||
return chartFrame.height + chartFrame.origin.y - (y - verticalRange.current.lowerBound) / verticalRange.current.distance * chartFrame.height
|
||||
}
|
||||
|
||||
func transform(toChartCoordinate point: CGPoint, chartFrame: CGRect) -> CGPoint {
|
||||
return CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame),
|
||||
y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame))
|
||||
}
|
||||
|
||||
func renderRange(bounds: CGRect, chartFrame: CGRect) -> ClosedRange<CGFloat> {
|
||||
let lowerBound = horizontalRange.current.lowerBound - chartFrame.origin.x / chartFrame.width * horizontalRange.current.distance
|
||||
let upperBound = horizontalRange.current.upperBound + (bounds.width - chartFrame.width - chartFrame.origin.x) / chartFrame.width * horizontalRange.current.distance
|
||||
guard lowerBound <= upperBound else {
|
||||
print("Error: Unexpecated bounds range!")
|
||||
return 0...1
|
||||
}
|
||||
return lowerBound...upperBound
|
||||
}
|
||||
|
||||
func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
fatalError("abstract")
|
||||
}
|
||||
|
||||
func setNeedsDisplay() {
|
||||
containerViews.forEach { $0.setNeedsDisplay() }
|
||||
}
|
||||
|
||||
var refreshClosure: () -> Void {
|
||||
return { [weak self] in
|
||||
self?.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
//
|
||||
// ChartDetailsRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ChartDetailsRenderer: BaseChartRenderer, ColorModeContainer {
|
||||
private lazy var colorAnimator = AnimationController<CGFloat>(current: 1, refreshClosure: refreshClosure)
|
||||
private var fromColorMode: ColorMode = .day
|
||||
private var currentColorMode: ColorMode = .day
|
||||
func apply(colorMode: ColorMode, animated: Bool) {
|
||||
if currentColorMode != colorMode {
|
||||
fromColorMode = currentColorMode
|
||||
currentColorMode = colorMode
|
||||
if animated {
|
||||
colorAnimator.set(current: 0)
|
||||
colorAnimator.animate(to: 1, duration: .defaultDuration)
|
||||
} else {
|
||||
colorAnimator.set(current: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var valuesAnimators: [AnimationController<CGFloat>] = []
|
||||
func setValueVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
valuesAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
var detailsViewModel: ChartDetailsViewModel = .blank {
|
||||
didSet {
|
||||
if detailsViewModel.values.count != valuesAnimators.count {
|
||||
valuesAnimators = detailsViewModel.values.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: refreshClosure) }
|
||||
}
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
var detailsViewPosition: CGFloat = 0 {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var detailViewPositionOffset: CGFloat = 10
|
||||
var detailViewTopOffset: CGFloat = 10
|
||||
private var iconWidth: CGFloat = 10
|
||||
private var margins: CGFloat = 10
|
||||
private let cornerRadius: CGFloat = 5
|
||||
private var rowHeight: CGFloat = 20
|
||||
private let titleFont = UIFont.systemFont(ofSize: 14, weight: .bold)
|
||||
private let prefixFont = UIFont.systemFont(ofSize: 14, weight: .bold)
|
||||
private let labelsFont = UIFont.systemFont(ofSize: 14, weight: .medium)
|
||||
private let valuesFont = UIFont.systemFont(ofSize: 14, weight: .bold)
|
||||
private let labelsColor: UIColor = .black
|
||||
|
||||
private(set) var previousRenderBannerFrame: CGRect = .zero
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
previousRenderBannerFrame = .zero
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let generalAlpha = chartAlphaAnimator.current
|
||||
if generalAlpha == 0 { return }
|
||||
|
||||
let widths: [(prefix: CGFloat, label: CGFloat, value: CGFloat)] = detailsViewModel.values.map { value in
|
||||
var prefixWidth: CGFloat = 0
|
||||
if let prefixText = value.prefix {
|
||||
prefixWidth = (prefixText as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: [.font: prefixFont],
|
||||
context: nil).width.rounded(.up) + margins
|
||||
}
|
||||
|
||||
let labelWidth = (value.title as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: [.font: labelsFont],
|
||||
context: nil).width.rounded(.up) + margins
|
||||
|
||||
let valueWidth = (value.value as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: [.font: valuesFont],
|
||||
context: nil).width.rounded(.up)
|
||||
return (prefixWidth, labelWidth, valueWidth)
|
||||
}
|
||||
|
||||
let titleWidth = (detailsViewModel.title as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: [.font: titleFont],
|
||||
context: nil).width
|
||||
let prefixesWidth = widths.map { $0.prefix }.max() ?? 0
|
||||
let labelsWidth = widths.map { $0.label }.max() ?? 0
|
||||
let valuesWidth = widths.map { $0.value }.max() ?? 0
|
||||
|
||||
let totalWidth: CGFloat = max(prefixesWidth + labelsWidth + valuesWidth, titleWidth + iconWidth) + margins * 2
|
||||
let totalHeight: CGFloat = CGFloat(detailsViewModel.values.count + 1) * rowHeight + margins * 2
|
||||
let backgroundColor = UIColor.valueBetween(start: fromColorMode.chartDetailsViewColor,
|
||||
end: currentColorMode.chartDetailsViewColor,
|
||||
offset: Double(colorAnimator.current))
|
||||
let titleAndTextColor = UIColor.valueBetween(start: fromColorMode.chartDetailsTextColor,
|
||||
end: currentColorMode.chartDetailsTextColor,
|
||||
offset: Double(colorAnimator.current))
|
||||
let detailsViewFrame: CGRect
|
||||
if totalWidth + detailViewTopOffset > detailsViewPosition {
|
||||
detailsViewFrame = CGRect(x: detailsViewPosition + detailViewTopOffset,
|
||||
y: detailViewTopOffset + chartFrame.minY,
|
||||
width: totalWidth,
|
||||
height: totalHeight)
|
||||
} else {
|
||||
detailsViewFrame = CGRect(x: detailsViewPosition - totalWidth - detailViewTopOffset,
|
||||
y: detailViewTopOffset + chartFrame.minY,
|
||||
width: totalWidth,
|
||||
height: totalHeight)
|
||||
}
|
||||
previousRenderBannerFrame = detailsViewFrame
|
||||
context.saveGState()
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.beginPath()
|
||||
context.addPath(CGPath(roundedRect: detailsViewFrame, cornerWidth: 5, cornerHeight: 5, transform: nil))
|
||||
context.fillPath()
|
||||
context.endPage()
|
||||
context.restoreGState()
|
||||
|
||||
var drawY = detailsViewFrame.minY + margins + (rowHeight - titleFont.pointSize) / 2
|
||||
(detailsViewModel.title as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + margins, y: drawY), withAttributes: [.font: titleFont,
|
||||
.foregroundColor: titleAndTextColor])
|
||||
drawY += rowHeight
|
||||
|
||||
for (index, row) in widths.enumerated() {
|
||||
let value = detailsViewModel.values[index]
|
||||
if let prefixText = value.prefix {
|
||||
(prefixText as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth - row.prefix,
|
||||
y: drawY),
|
||||
withAttributes: [.font: prefixText, .foregroundColor: titleAndTextColor])
|
||||
}
|
||||
|
||||
(value.title as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth + margins,
|
||||
y: drawY),
|
||||
withAttributes: [.font: labelsFont, .foregroundColor: titleAndTextColor])
|
||||
|
||||
(value.value as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth + labelsWidth + valuesWidth - row.value + margins,
|
||||
y: drawY),
|
||||
withAttributes: [.font: labelsFont, .foregroundColor: value.color])
|
||||
|
||||
drawY += rowHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// HorizontalScalesRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/8/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class HorizontalScalesRenderer: BaseChartRenderer {
|
||||
private var horizontalLabels: [LinesChartLabel] = []
|
||||
private var animatedHorizontalLabels: [AnimatedLinesChartLabels] = []
|
||||
|
||||
var labelsVerticalOffset: CGFloat = 8
|
||||
var labelsFont: UIFont = .systemFont(ofSize: 11)
|
||||
var labelsColor: UIColor = .gray
|
||||
|
||||
func setup(labels: [LinesChartLabel], animated: Bool) {
|
||||
if animated {
|
||||
var labelsToKeepVisible: [LinesChartLabel] = []
|
||||
let labelsToHide: [LinesChartLabel]
|
||||
var labelsToShow: [LinesChartLabel] = []
|
||||
|
||||
for label in labels {
|
||||
if horizontalLabels.contains(label) {
|
||||
labelsToKeepVisible.append(label)
|
||||
} else {
|
||||
labelsToShow.append(label)
|
||||
}
|
||||
}
|
||||
labelsToHide = horizontalLabels.filter { !labels.contains($0) }
|
||||
animatedHorizontalLabels.removeAll()
|
||||
horizontalLabels = labelsToKeepVisible
|
||||
|
||||
let showAnimation = AnimatedLinesChartLabels(labels: labelsToShow, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
|
||||
showAnimation.isAppearing = true
|
||||
showAnimation.alphaAnimator.set(current: 0)
|
||||
showAnimation.alphaAnimator.animate(to: 1, duration: .defaultDuration)
|
||||
showAnimation.alphaAnimator.completionClosure = { [weak self, weak showAnimation] in
|
||||
guard let self = self, let showAnimation = showAnimation else { return }
|
||||
self.animatedHorizontalLabels.removeAll(where: { $0 === showAnimation })
|
||||
self.horizontalLabels = labels
|
||||
}
|
||||
|
||||
let hideAnimation = AnimatedLinesChartLabels(labels: labelsToHide, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
|
||||
hideAnimation.isAppearing = false
|
||||
hideAnimation.alphaAnimator.set(current: 1)
|
||||
hideAnimation.alphaAnimator.animate(to: 0, duration: .defaultDuration)
|
||||
hideAnimation.alphaAnimator.completionClosure = { [weak self, weak hideAnimation] in
|
||||
guard let self = self, let hideAnimation = hideAnimation else { return }
|
||||
self.animatedHorizontalLabels.removeAll(where: { $0 === hideAnimation })
|
||||
}
|
||||
|
||||
animatedHorizontalLabels.append(showAnimation)
|
||||
animatedHorizontalLabels.append(hideAnimation)
|
||||
} else {
|
||||
horizontalLabels = labels
|
||||
animatedHorizontalLabels = []
|
||||
}
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let itemsAlpha = chartAlphaAnimator.current
|
||||
guard itemsAlpha > 0 else { return }
|
||||
|
||||
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
|
||||
|
||||
func drawHorizontalLabels(_ labels: [LinesChartLabel], color: UIColor) {
|
||||
let attributes: [NSAttributedString.Key : Any] = [.foregroundColor: color,
|
||||
.font: labelsFont]
|
||||
let y = chartFrame.origin.y + chartFrame.height + labelsVerticalOffset
|
||||
|
||||
if let start = labels.firstIndex(where: { $0.value > range.lowerBound }) {
|
||||
for index in start..<labels.count {
|
||||
let label = labels[index]
|
||||
|
||||
let x = transform(toChartCoordinateHorizontal: label.value, chartFrame: chartFrame)
|
||||
|
||||
let rect = (label.text as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: attributes,
|
||||
context: nil)
|
||||
(label.text as NSString).draw(at: CGPoint(x: x - rect.width, y: y), withAttributes: attributes)
|
||||
if label.value > range.upperBound {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let labelColorAlpha = labelsColor.alphaValue * itemsAlpha
|
||||
drawHorizontalLabels(horizontalLabels, color: labelsColor.withAlphaComponent(labelColorAlpha * itemsAlpha))
|
||||
for animation in animatedHorizontalLabels {
|
||||
let color = labelsColor.withAlphaComponent(animation.alphaAnimator.current * labelColorAlpha)
|
||||
drawHorizontalLabels(animation.labels, color: color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// LineBulletsRenerer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/8/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class LineBulletsRenerer: BaseChartRenderer {
|
||||
struct Bullet {
|
||||
var coordinate: CGPoint
|
||||
var color: UIColor
|
||||
}
|
||||
|
||||
var bullets: [Bullet] = [] {
|
||||
willSet {
|
||||
if alphaAnimators.count != newValue.count {
|
||||
alphaAnimators = newValue.map { _ in AnimationController<CGFloat>(current: 1.0, refreshClosure: refreshClosure) }
|
||||
}
|
||||
}
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
private var alphaAnimators: [AnimationController<CGFloat>] = []
|
||||
|
||||
private lazy var innerColorAnimator = AnimationController(current: UIColorContainer(color: .white), refreshClosure: refreshClosure)
|
||||
public func setInnerColor(_ color: UIColor, animated: Bool) {
|
||||
if animated {
|
||||
innerColorAnimator.animate(to: UIColorContainer(color: color), duration: .defaultDuration)
|
||||
} else {
|
||||
innerColorAnimator.set(current: UIColorContainer(color: color))
|
||||
}
|
||||
}
|
||||
|
||||
var linesWidth: CGFloat = 2
|
||||
var bulletRadius: CGFloat = 6
|
||||
|
||||
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
alphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let generalAlpha = chartAlphaAnimator.current
|
||||
if generalAlpha == 0 { return }
|
||||
|
||||
for (index, bullet) in bullets.enumerated() {
|
||||
let alpha = alphaAnimators[index].current
|
||||
if alpha == 0 { continue }
|
||||
|
||||
let centerX = transform(toChartCoordinateHorizontal: bullet.coordinate.x, chartFrame: chartFrame)
|
||||
let centerY = transform(toChartCoordinateVertical: bullet.coordinate.y, chartFrame: chartFrame)
|
||||
context.setFillColor(innerColorAnimator.current.color.withAlphaComponent(alpha).cgColor)
|
||||
context.setStrokeColor(bullet.color.withAlphaComponent(alpha).cgColor)
|
||||
context.setLineWidth(linesWidth)
|
||||
let rect = CGRect(x: centerX - bulletRadius / 2,
|
||||
y: centerY - bulletRadius / 2,
|
||||
width: bulletRadius,
|
||||
height: bulletRadius)
|
||||
context.fillEllipse(in: rect)
|
||||
context.strokeEllipse(in: rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,538 @@
|
|||
//
|
||||
// LinesChartRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class LinesChartRenderer: BaseChartRenderer {
|
||||
struct LineData {
|
||||
var color: UIColor
|
||||
var points: [CGPoint]
|
||||
}
|
||||
|
||||
private var linesAlphaAnimators: [AnimationController<CGFloat>] = []
|
||||
|
||||
var lineWidth: CGFloat = 1 {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
private lazy var linesShapeAnimator = AnimationController<Double>(current: 1, refreshClosure: self.refreshClosure)
|
||||
private var fromLines: [LineData] = []
|
||||
private var toLines: [LineData] = []
|
||||
|
||||
func setLines(lines: [LineData], animated: Bool) {
|
||||
if toLines.count != lines.count {
|
||||
linesAlphaAnimators = lines.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
|
||||
}
|
||||
if animated {
|
||||
self.fromLines = self.toLines
|
||||
self.toLines = lines
|
||||
linesShapeAnimator.set(current: 1.0 - linesShapeAnimator.current)
|
||||
linesShapeAnimator.completionClosure = {
|
||||
self.fromLines = []
|
||||
}
|
||||
linesShapeAnimator.animate(to: 1, duration: .defaultDuration)
|
||||
} else {
|
||||
self.fromLines = []
|
||||
self.toLines = lines
|
||||
linesShapeAnimator.set(current: 1)
|
||||
}
|
||||
}
|
||||
|
||||
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let chartsAlpha = chartAlphaAnimator.current
|
||||
if chartsAlpha == 0 { return }
|
||||
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
|
||||
|
||||
for (index, toLine) in toLines.enumerated() {
|
||||
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||
if alpha == 0 { continue }
|
||||
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
|
||||
if linesShapeAnimator.isAnimating {
|
||||
let animationOffset = linesShapeAnimator.current
|
||||
|
||||
let path = CGMutablePath()
|
||||
let fromPoints = fromLines.safeElement(at: index)?.points ?? []
|
||||
let toPoints = toLines.safeElement(at: index)?.points ?? []
|
||||
|
||||
var fromIndex: Int? = fromPoints.firstIndex(where: { $0.x >= range.lowerBound })
|
||||
var toIndex: Int? = toPoints.firstIndex(where: { $0.x >= range.lowerBound })
|
||||
|
||||
let fromRange = verticalRange.start
|
||||
let currentRange = verticalRange.current
|
||||
let toRange = verticalRange.end
|
||||
|
||||
func convertFromPoint(_ fromPoint: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: fromPoint.x,
|
||||
y: (fromPoint.y - fromRange.lowerBound) / fromRange.distance * currentRange.distance + currentRange.lowerBound)
|
||||
}
|
||||
|
||||
func convertToPoint(_ toPoint: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: toPoint.x,
|
||||
y: (toPoint.y - toRange.lowerBound) / toRange.distance * currentRange.distance + currentRange.lowerBound)
|
||||
}
|
||||
|
||||
var previousFromPoint: CGPoint
|
||||
var previousToPoint: CGPoint
|
||||
let startFromPoint: CGPoint?
|
||||
let startToPoint: CGPoint?
|
||||
|
||||
if let validFrom = fromIndex {
|
||||
previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)])
|
||||
startFromPoint = previousFromPoint
|
||||
} else {
|
||||
previousFromPoint = .zero
|
||||
startFromPoint = nil
|
||||
}
|
||||
if let validTo = toIndex {
|
||||
previousToPoint = convertToPoint(toPoints[max(0, validTo - 1)])
|
||||
startToPoint = previousToPoint
|
||||
} else {
|
||||
previousToPoint = .zero
|
||||
startToPoint = nil
|
||||
}
|
||||
|
||||
var combinedPoints: [CGPoint] = []
|
||||
|
||||
func add(pointToDraw: CGPoint) {
|
||||
if let startFromPoint = startFromPoint,
|
||||
pointToDraw.x < startFromPoint.x {
|
||||
let animatedPoint = CGPoint(x: pointToDraw.x,
|
||||
y: CGFloat.valueBetween(start: startFromPoint.y, end: pointToDraw.y, offset: animationOffset))
|
||||
combinedPoints.append(transform(toChartCoordinate: animatedPoint, chartFrame: chartFrame))
|
||||
} else if let startToPoint = startToPoint,
|
||||
pointToDraw.x < startToPoint.x {
|
||||
let animatedPoint = CGPoint(x: pointToDraw.x,
|
||||
y: CGFloat.valueBetween(start: startToPoint.y, end: pointToDraw.y, offset: 1 - animationOffset))
|
||||
combinedPoints.append(transform(toChartCoordinate: animatedPoint, chartFrame: chartFrame))
|
||||
} else {
|
||||
combinedPoints.append(transform(toChartCoordinate: pointToDraw, chartFrame: chartFrame))
|
||||
}
|
||||
}
|
||||
|
||||
if previousToPoint != .zero && previousFromPoint != .zero {
|
||||
add(pointToDraw: (previousToPoint.x < previousFromPoint.x ? previousToPoint : previousFromPoint))
|
||||
} else if previousToPoint != .zero {
|
||||
add(pointToDraw: previousToPoint)
|
||||
} else if previousFromPoint != .zero {
|
||||
add(pointToDraw: previousFromPoint)
|
||||
}
|
||||
|
||||
while let validFromIndex = fromIndex,
|
||||
let validToIndex = toIndex,
|
||||
validFromIndex < fromPoints.count,
|
||||
validToIndex < toPoints.count {
|
||||
let currentFromPoint = convertFromPoint(fromPoints[validFromIndex])
|
||||
let currentToPoint = convertToPoint(toPoints[validToIndex])
|
||||
let pointToAdd: CGPoint
|
||||
if currentFromPoint.x == currentToPoint.x {
|
||||
pointToAdd = CGPoint.valueBetween(start: currentFromPoint, end: currentToPoint, offset: animationOffset)
|
||||
previousFromPoint = currentFromPoint
|
||||
previousToPoint = currentToPoint
|
||||
fromIndex = validFromIndex + 1
|
||||
toIndex = validToIndex + 1
|
||||
} else if currentFromPoint.x < currentToPoint.x {
|
||||
if previousToPoint.x < currentFromPoint.x {
|
||||
let offset = Double((currentFromPoint.x - previousToPoint.x) / (currentToPoint.x - previousToPoint.x))
|
||||
let intermidiateToPoint = CGPoint.valueBetween(start: previousToPoint, end: currentToPoint, offset: offset)
|
||||
pointToAdd = CGPoint.valueBetween(start: currentFromPoint, end: intermidiateToPoint, offset: animationOffset)
|
||||
} else {
|
||||
pointToAdd = currentFromPoint
|
||||
}
|
||||
previousFromPoint = currentFromPoint
|
||||
fromIndex = validFromIndex + 1
|
||||
} else {
|
||||
if previousFromPoint.x < currentToPoint.x {
|
||||
let offset = Double((currentToPoint.x - previousFromPoint.x) / (currentFromPoint.x - previousFromPoint.x))
|
||||
let intermidiateFromPoint = CGPoint.valueBetween(start: previousFromPoint, end: currentFromPoint, offset: offset)
|
||||
pointToAdd = CGPoint.valueBetween(start: intermidiateFromPoint, end: currentToPoint, offset: animationOffset)
|
||||
} else {
|
||||
pointToAdd = currentToPoint
|
||||
}
|
||||
previousToPoint = currentToPoint
|
||||
toIndex = validToIndex + 1
|
||||
}
|
||||
add(pointToDraw: pointToAdd)
|
||||
if (pointToAdd.x > range.upperBound) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
while let validToIndex = toIndex, validToIndex < toPoints.count {
|
||||
var pointToAdd = convertToPoint(toPoints[validToIndex])
|
||||
pointToAdd.y = CGFloat.valueBetween(start: previousFromPoint.y,
|
||||
end: pointToAdd.y,
|
||||
offset: animationOffset)
|
||||
|
||||
add(pointToDraw: pointToAdd)
|
||||
if (pointToAdd.x > range.upperBound) {
|
||||
break
|
||||
}
|
||||
|
||||
toIndex = validToIndex + 1
|
||||
}
|
||||
|
||||
while let validFromIndex = fromIndex, validFromIndex < fromPoints.count {
|
||||
var pointToAdd = convertFromPoint(fromPoints[validFromIndex])
|
||||
pointToAdd.y = CGFloat.valueBetween(start: previousToPoint.y,
|
||||
end: pointToAdd.y,
|
||||
offset: 1 - animationOffset)
|
||||
|
||||
add(pointToDraw: pointToAdd)
|
||||
if (pointToAdd.x > range.upperBound) {
|
||||
break
|
||||
}
|
||||
|
||||
fromIndex = validFromIndex + 1
|
||||
}
|
||||
|
||||
var index = 0
|
||||
var lines: [CGPoint] = []
|
||||
var currentChartPoint = combinedPoints[index]
|
||||
lines.append(currentChartPoint)
|
||||
|
||||
var chartPoints = [currentChartPoint]
|
||||
var minIndex = 0
|
||||
var maxIndex = 0
|
||||
index += 1
|
||||
|
||||
while index < combinedPoints.count {
|
||||
currentChartPoint = combinedPoints[index]
|
||||
|
||||
if currentChartPoint.x - chartPoints[0].x < lineWidth * optimizationLevel {
|
||||
chartPoints.append(currentChartPoint)
|
||||
|
||||
if currentChartPoint.y > chartPoints[maxIndex].y {
|
||||
maxIndex = chartPoints.count - 1
|
||||
}
|
||||
if currentChartPoint.y < chartPoints[minIndex].y {
|
||||
minIndex = chartPoints.count - 1
|
||||
}
|
||||
|
||||
index += 1
|
||||
} else {
|
||||
if chartPoints.count == 1 {
|
||||
lines.append(currentChartPoint)
|
||||
lines.append(currentChartPoint)
|
||||
chartPoints[0] = currentChartPoint
|
||||
index += 1
|
||||
minIndex = 0
|
||||
maxIndex = 0
|
||||
} else {
|
||||
if minIndex < maxIndex {
|
||||
if minIndex != 0 {
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
}
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
if maxIndex != chartPoints.count - 1 {
|
||||
chartPoints = [chartPoints[maxIndex], chartPoints.last!]
|
||||
} else {
|
||||
chartPoints = [chartPoints[maxIndex]]
|
||||
}
|
||||
} else {
|
||||
if maxIndex != 0 {
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
}
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
if minIndex != chartPoints.count - 1 {
|
||||
chartPoints = [chartPoints[minIndex], chartPoints.last!]
|
||||
} else {
|
||||
chartPoints = [chartPoints[minIndex]]
|
||||
}
|
||||
}
|
||||
if chartPoints.count == 2 {
|
||||
if chartPoints[0].y < chartPoints[1].y {
|
||||
minIndex = 0
|
||||
maxIndex = 1
|
||||
} else {
|
||||
minIndex = 1
|
||||
maxIndex = 0
|
||||
}
|
||||
} else {
|
||||
minIndex = 0
|
||||
maxIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chartPoints.count == 1 {
|
||||
lines.append(currentChartPoint)
|
||||
lines.append(currentChartPoint)
|
||||
} else {
|
||||
if minIndex < maxIndex {
|
||||
if minIndex != 0 {
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
}
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
if maxIndex != chartPoints.count - 1 {
|
||||
lines.append(chartPoints.last!)
|
||||
lines.append(chartPoints.last!)
|
||||
}
|
||||
} else {
|
||||
if maxIndex != 0 {
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
}
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
if minIndex != chartPoints.count - 1 {
|
||||
lines.append(chartPoints.last!)
|
||||
lines.append(chartPoints.last!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.count % 2) == 1 {
|
||||
lines.removeLast()
|
||||
}
|
||||
|
||||
context.setLineCap(.round)
|
||||
context.strokeLineSegments(between: lines)
|
||||
|
||||
} else {
|
||||
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||
if alpha == 0 { continue }
|
||||
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
|
||||
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
|
||||
var lines: [CGPoint] = []
|
||||
index = max(0, index - 1)
|
||||
var currentPoint = toLine.points[index]
|
||||
var currentChartPoint = transform(toChartCoordinate: currentPoint, chartFrame: chartFrame)
|
||||
lines.append(currentChartPoint)
|
||||
//context.move(to: currentChartPoint)
|
||||
|
||||
var chartPoints = [currentChartPoint]
|
||||
var minIndex = 0
|
||||
var maxIndex = 0
|
||||
index += 1
|
||||
|
||||
while index < toLine.points.count {
|
||||
currentPoint = toLine.points[index]
|
||||
currentChartPoint = transform(toChartCoordinate: currentPoint, chartFrame: chartFrame)
|
||||
|
||||
if currentChartPoint.x - chartPoints[0].x < lineWidth * optimizationLevel {
|
||||
chartPoints.append(currentChartPoint)
|
||||
|
||||
if currentChartPoint.y > chartPoints[maxIndex].y {
|
||||
maxIndex = chartPoints.count - 1
|
||||
}
|
||||
if currentChartPoint.y < chartPoints[minIndex].y {
|
||||
minIndex = chartPoints.count - 1
|
||||
}
|
||||
|
||||
index += 1
|
||||
} else {
|
||||
if chartPoints.count == 1 {
|
||||
lines.append(currentChartPoint)
|
||||
lines.append(currentChartPoint)
|
||||
chartPoints[0] = currentChartPoint
|
||||
index += 1
|
||||
minIndex = 0
|
||||
maxIndex = 0
|
||||
} else {
|
||||
if minIndex < maxIndex {
|
||||
if minIndex != 0 {
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
}
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
if maxIndex != chartPoints.count - 1 {
|
||||
chartPoints = [chartPoints[maxIndex], chartPoints.last!]
|
||||
} else {
|
||||
chartPoints = [chartPoints[maxIndex]]
|
||||
}
|
||||
} else {
|
||||
if maxIndex != 0 {
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
}
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
if minIndex != chartPoints.count - 1 {
|
||||
chartPoints = [chartPoints[minIndex], chartPoints.last!]
|
||||
} else {
|
||||
chartPoints = [chartPoints[minIndex]]
|
||||
}
|
||||
}
|
||||
if chartPoints.count == 2 {
|
||||
if chartPoints[0].y < chartPoints[1].y {
|
||||
minIndex = 0
|
||||
maxIndex = 1
|
||||
} else {
|
||||
minIndex = 1
|
||||
maxIndex = 0
|
||||
}
|
||||
} else {
|
||||
minIndex = 0
|
||||
maxIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if currentPoint.x > range.upperBound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if chartPoints.count == 1 {
|
||||
lines.append(currentChartPoint)
|
||||
lines.append(currentChartPoint)
|
||||
} else {
|
||||
if minIndex < maxIndex {
|
||||
if minIndex != 0 {
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
}
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
if maxIndex != chartPoints.count - 1 {
|
||||
lines.append(chartPoints.last!)
|
||||
lines.append(chartPoints.last!)
|
||||
}
|
||||
} else {
|
||||
if maxIndex != 0 {
|
||||
lines.append(chartPoints[maxIndex])
|
||||
lines.append(chartPoints[maxIndex])
|
||||
}
|
||||
lines.append(chartPoints[minIndex])
|
||||
lines.append(chartPoints[minIndex])
|
||||
if minIndex != chartPoints.count - 1 {
|
||||
lines.append(chartPoints.last!)
|
||||
lines.append(chartPoints.last!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.count % 2) == 1 {
|
||||
lines.removeLast()
|
||||
}
|
||||
|
||||
context.setLineCap(.round)
|
||||
context.strokeLineSegments(between: lines)
|
||||
}
|
||||
|
||||
// if var start = toLine.points.firstIndex(where: { $0.x > range.lowerBound }) {
|
||||
// let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||
// if alpha == 0 { continue }
|
||||
// context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
||||
// context.setLineWidth(lineWidth)
|
||||
//
|
||||
// context.setLineCap(.round)
|
||||
// start = max(0, start - 1)
|
||||
// let startPoint = toLine.points[start]
|
||||
// var lines: [CGPoint] = []
|
||||
// var pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: startPoint.x, chartFrame: chartFrame),
|
||||
// y: transform(toChartCoordinateVertical: startPoint.y, chartFrame: chartFrame))
|
||||
// for index in (start + 1)..<toLine.points.count {
|
||||
// lines.append(pointToDraw)
|
||||
// let point = toLine.points[index]
|
||||
// pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame),
|
||||
// y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame))
|
||||
// lines.append(pointToDraw)
|
||||
// if point.x > range.upperBound {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// context.strokeLineSegments(between: lines)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LinesChartRenderer.LineData {
|
||||
static func initialComponents(chartsCollection: ChartsCollection) -> (linesData: [LinesChartRenderer.LineData],
|
||||
totalHorizontalRange: ClosedRange<CGFloat>,
|
||||
totalVerticalRange: ClosedRange<CGFloat>) {
|
||||
let lines: [LinesChartRenderer.LineData] = chartsCollection.chartValues.map { chart in
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
return LinesChartRenderer.LineData(color: chart.color, points: points)
|
||||
}
|
||||
let horizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: lines) ?? BaseConstants.defaultRange
|
||||
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: lines) ?? BaseConstants.defaultRange
|
||||
return (linesData: lines, totalHorizontalRange: horizontalRange, totalVerticalRange: verticalRange)
|
||||
}
|
||||
|
||||
static func horizontalRange(lines: [LinesChartRenderer.LineData]) -> ClosedRange<CGFloat>? {
|
||||
guard let firstPoint = lines.first?.points.first else { return nil }
|
||||
var hMin: CGFloat = firstPoint.x
|
||||
var hMax: CGFloat = firstPoint.x
|
||||
|
||||
for line in lines {
|
||||
if let first = line.points.first,
|
||||
let last = line.points.last {
|
||||
hMin = min(hMin, first.x)
|
||||
hMax = max(hMax, last.x)
|
||||
}
|
||||
}
|
||||
|
||||
return hMin...hMax
|
||||
}
|
||||
|
||||
static func verticalRange(lines: [LinesChartRenderer.LineData], calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
||||
if let calculatingRange = calculatingRange {
|
||||
guard let initalStart = lines.first?.points.first(where: { $0.x >= calculatingRange.lowerBound &&
|
||||
$0.x <= calculatingRange.upperBound }) else { return nil }
|
||||
var vMin: CGFloat = initalStart.y
|
||||
var vMax: CGFloat = initalStart.y
|
||||
for line in lines {
|
||||
if var index = line.points.firstIndex(where: { $0.x > calculatingRange.lowerBound }) {
|
||||
if addBounds {
|
||||
index = max(0, index - 1)
|
||||
}
|
||||
while index < line.points.count {
|
||||
let point = line.points[index]
|
||||
if point.x < calculatingRange.upperBound {
|
||||
vMin = min(vMin, point.y)
|
||||
vMax = max(vMax, point.y)
|
||||
} else if addBounds {
|
||||
vMin = min(vMin, point.y)
|
||||
vMax = max(vMax, point.y)
|
||||
break
|
||||
} else {
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return vMin...vMax
|
||||
} else {
|
||||
guard let firstPoint = lines.first?.points.first else { return nil }
|
||||
var vMin: CGFloat = firstPoint.y
|
||||
var vMax: CGFloat = firstPoint.y
|
||||
for line in lines {
|
||||
for point in line.points {
|
||||
vMin = min(vMin, point.y)
|
||||
vMax = max(vMax, point.y)
|
||||
}
|
||||
}
|
||||
return vMin...vMax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
//
|
||||
// PecentChartRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PecentChartRenderer: BaseChartRenderer {
|
||||
struct PercentageData {
|
||||
static let blank = PecentChartRenderer.PercentageData(locations: [], components: [])
|
||||
var locations: [CGFloat]
|
||||
var components: [Component]
|
||||
|
||||
struct Component {
|
||||
var color: UIColor
|
||||
var values: [CGFloat]
|
||||
}
|
||||
}
|
||||
|
||||
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
|
||||
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
|
||||
}
|
||||
|
||||
private var componentsAnimators: [AnimationController<CGFloat>] = []
|
||||
var percentageData: PercentageData = PercentageData(locations: [], components: []) {
|
||||
willSet {
|
||||
if percentageData.components.count != newValue.components.count {
|
||||
componentsAnimators = newValue.components.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
|
||||
}
|
||||
}
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let alpha = chartAlphaAnimator.current
|
||||
guard alpha > 0 else { return }
|
||||
|
||||
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
|
||||
|
||||
var paths: [CGMutablePath] = percentageData.components.map { _ in CGMutablePath() }
|
||||
var vertices: [CGFloat] = Array<CGFloat>(repeating: 0, count: percentageData.components.count)
|
||||
|
||||
if var locationIndex = percentageData.locations.firstIndex(where: { $0 > range.lowerBound }) {
|
||||
locationIndex = max(0, locationIndex - 1)
|
||||
|
||||
var currentLocation = transform(toChartCoordinateHorizontal: percentageData.locations[locationIndex], chartFrame: chartFrame)
|
||||
|
||||
let startPoint = CGPoint(x: currentLocation,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
|
||||
for path in paths {
|
||||
path.move(to: startPoint)
|
||||
}
|
||||
paths.last?.addLine(to: CGPoint(x: currentLocation,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
|
||||
|
||||
while locationIndex < percentageData.locations.count {
|
||||
currentLocation = transform(toChartCoordinateHorizontal: percentageData.locations[locationIndex], chartFrame: chartFrame)
|
||||
var summ: CGFloat = 0
|
||||
|
||||
for (index, component) in percentageData.components.enumerated() {
|
||||
let visibilityPercent = componentsAnimators[index].current
|
||||
|
||||
let value = component.values[locationIndex] * visibilityPercent
|
||||
if index == 0 {
|
||||
vertices[index] = value
|
||||
} else {
|
||||
vertices[index] = value + vertices[index - 1]
|
||||
}
|
||||
summ += value
|
||||
}
|
||||
|
||||
if summ > 0 {
|
||||
for (index, value) in vertices.dropLast().enumerated() {
|
||||
paths[index].addLine(to: CGPoint(x: currentLocation,
|
||||
y: transform(toChartCoordinateVertical: value / summ, chartFrame: chartFrame)))
|
||||
}
|
||||
}
|
||||
|
||||
if currentLocation > range.upperBound {
|
||||
break
|
||||
}
|
||||
|
||||
locationIndex += 1
|
||||
}
|
||||
|
||||
paths.last?.addLine(to: CGPoint(x: currentLocation,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
|
||||
|
||||
let endPoint = CGPoint(x: currentLocation,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
|
||||
for (index, path) in paths.enumerated().reversed() {
|
||||
let visibilityPercent = componentsAnimators[index].current
|
||||
if visibilityPercent == 0 { continue }
|
||||
|
||||
path.addLine(to: endPoint)
|
||||
path.closeSubpath()
|
||||
|
||||
context.saveGState()
|
||||
context.beginPath()
|
||||
context.addPath(path)
|
||||
|
||||
context.setFillColor(percentageData.components[index].color.cgColor)
|
||||
context.fillPath()
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PecentChartRenderer.PercentageData {
|
||||
static func horizontalRange(data: PecentChartRenderer.PercentageData) -> ClosedRange<CGFloat>? {
|
||||
guard let firstPoint = data.locations.first,
|
||||
let lastPoint = data.locations.last,
|
||||
firstPoint <= lastPoint else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return firstPoint...lastPoint
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
//
|
||||
// PercentPieAnimationRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PercentPieAnimationRenderer: BaseChartRenderer {
|
||||
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
|
||||
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
|
||||
}
|
||||
|
||||
private lazy var transitionAnimator = AnimationController<CGFloat>(current: 0, refreshClosure: refreshClosure)
|
||||
private var animationComponentsPoints: [[CGPoint]] = []
|
||||
var visiblePercentageData: PecentChartRenderer.PercentageData = .blank {
|
||||
didSet {
|
||||
animationComponentsPoints = []
|
||||
}
|
||||
}
|
||||
var visiblePieComponents: [PieChartRenderer.PieComponent] = []
|
||||
|
||||
func animate(fromDataToPie: Bool, animated: Bool, completion: @escaping () -> Void) {
|
||||
assert(visiblePercentageData.components.count == visiblePieComponents.count)
|
||||
|
||||
isEnabled = true
|
||||
transitionAnimator.completionClosure = { [weak self] in
|
||||
self?.isEnabled = false
|
||||
completion()
|
||||
}
|
||||
transitionAnimator.animate(to: fromDataToPie ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
private func generateAnimationComponentPoints(bounds: CGRect, chartFrame: CGRect) {
|
||||
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
|
||||
|
||||
let componentsCount = visiblePercentageData.components.count
|
||||
guard componentsCount > 0 else { return }
|
||||
animationComponentsPoints = visiblePercentageData.components.map { _ in [] }
|
||||
var vertices: [CGFloat] = Array<CGFloat>(repeating: 0, count: visiblePercentageData.components.count)
|
||||
|
||||
if var locationIndex = visiblePercentageData.locations.firstIndex(where: { $0 > range.lowerBound }) {
|
||||
locationIndex = max(0, locationIndex - 1)
|
||||
var currentLocation = transform(toChartCoordinateHorizontal: visiblePercentageData.locations[locationIndex], chartFrame: chartFrame)
|
||||
let startPoint = CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
for index in 0..<componentsCount {
|
||||
animationComponentsPoints[index].append(startPoint)
|
||||
}
|
||||
animationComponentsPoints[componentsCount - 1].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
|
||||
while locationIndex < visiblePercentageData.locations.count {
|
||||
currentLocation = transform(toChartCoordinateHorizontal: visiblePercentageData.locations[locationIndex], chartFrame: chartFrame)
|
||||
var summ: CGFloat = 0
|
||||
|
||||
for (index, component) in visiblePercentageData.components.enumerated() {
|
||||
let value = component.values[locationIndex]
|
||||
if index == 0 {
|
||||
vertices[index] = value
|
||||
} else {
|
||||
vertices[index] = value + vertices[index - 1]
|
||||
}
|
||||
summ += value
|
||||
}
|
||||
|
||||
for (index, value) in vertices.dropLast().enumerated() {
|
||||
animationComponentsPoints[index].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: value / summ, chartFrame: chartFrame)))
|
||||
}
|
||||
if visiblePercentageData.locations[locationIndex] > range.upperBound {
|
||||
break
|
||||
}
|
||||
locationIndex += 1
|
||||
}
|
||||
|
||||
animationComponentsPoints[componentsCount - 1].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
|
||||
let endPoint = CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
for index in 0..<componentsCount {
|
||||
animationComponentsPoints[index].append(endPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var initialPieAngle: CGFloat = .pi / 3
|
||||
var backgroundColor: UIColor = .white
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
self.optimizationLevel = 1
|
||||
|
||||
if animationComponentsPoints.isEmpty {
|
||||
generateAnimationComponentPoints(bounds: bounds, chartFrame: chartFrame)
|
||||
}
|
||||
|
||||
let numberOfComponents = animationComponentsPoints.count
|
||||
guard numberOfComponents > 0 else { return }
|
||||
let destinationRadius = max(chartFrame.width, chartFrame.height)
|
||||
|
||||
let animationFraction = transitionAnimator.current
|
||||
let animationFractionD = Double(transitionAnimator.current)
|
||||
let easeInAnimationFractionD = animationFractionD * animationFractionD * animationFractionD * animationFractionD
|
||||
let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY)
|
||||
let totalPieSumm: CGFloat = visiblePieComponents.map { $0.value } .reduce(0, +)
|
||||
|
||||
let pathsToDraw: [CGMutablePath] = (0..<numberOfComponents).map { _ in CGMutablePath() }
|
||||
|
||||
var startAngle: CGFloat = initialPieAngle
|
||||
for componentIndex in 0..<(numberOfComponents - 1) {
|
||||
let componentPoints = animationComponentsPoints[componentIndex]
|
||||
guard componentPoints.count > 4 else {
|
||||
return
|
||||
}
|
||||
|
||||
let percent = visiblePieComponents[componentIndex].value / totalPieSumm
|
||||
let segmentSize = 2 * .pi * percent
|
||||
let endAngle = startAngle + segmentSize
|
||||
let centerAngle = (startAngle + endAngle) / 2
|
||||
|
||||
let lineCenterPoint = CGPoint.valueBetween(start: componentPoints[componentPoints.count / 2],
|
||||
end: center,
|
||||
offset: animationFractionD)
|
||||
|
||||
let startDestinationPoint = lineCenterPoint + CGPoint(x: destinationRadius, y: 0)
|
||||
let centerDestinationPoint = lineCenterPoint + CGPoint(x: 0, y: destinationRadius)
|
||||
let endDestinationPoint = lineCenterPoint + CGPoint(x: -destinationRadius, y: 0)
|
||||
let initialStartDestinationAngle: CGFloat = 0
|
||||
let initialCenterDestinationAngle: CGFloat = .pi / 2
|
||||
let initialEndDestinationAngle: CGFloat = .pi
|
||||
|
||||
var previousAddedPoint = (componentPoints[0] * 2 - center)
|
||||
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: centerAngle - initialCenterDestinationAngle, offset: animationFractionD))
|
||||
|
||||
pathsToDraw[componentIndex].move(to: previousAddedPoint)
|
||||
|
||||
func addPointToPath(_ point: CGPoint) {
|
||||
if (point - previousAddedPoint).lengthSquared() > optimizationLevel {
|
||||
pathsToDraw[componentIndex].addLine(to: point)
|
||||
previousAddedPoint = point
|
||||
}
|
||||
}
|
||||
|
||||
for endPointIndex in 1..<(componentPoints.count / 2) {
|
||||
addPointToPath(CGPoint.valueBetween(start: componentPoints[endPointIndex], end: endDestinationPoint, offset: easeInAnimationFractionD)
|
||||
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: endAngle - initialEndDestinationAngle, offset: animationFractionD)))
|
||||
}
|
||||
|
||||
addPointToPath(lineCenterPoint)
|
||||
|
||||
for startPointIndex in (componentPoints.count / 2 + 1)..<(componentPoints.count - 1) {
|
||||
addPointToPath(CGPoint.valueBetween(start: componentPoints[startPointIndex], end: startDestinationPoint, offset: easeInAnimationFractionD)
|
||||
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: startAngle - initialStartDestinationAngle, offset: animationFractionD)))
|
||||
}
|
||||
|
||||
if let lastPoint = componentPoints.last {
|
||||
addPointToPath((lastPoint * 2 - center)
|
||||
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: centerAngle - initialCenterDestinationAngle, offset: animationFractionD)))
|
||||
}
|
||||
|
||||
startAngle = endAngle
|
||||
}
|
||||
|
||||
if let lastPath = animationComponentsPoints.last {
|
||||
pathsToDraw.last?.addLines(between: lastPath)
|
||||
}
|
||||
|
||||
for (index, path) in pathsToDraw.enumerated().reversed() {
|
||||
path.closeSubpath()
|
||||
|
||||
context.saveGState()
|
||||
context.beginPath()
|
||||
context.addPath(path)
|
||||
|
||||
context.setFillColor(visiblePieComponents[index].color.cgColor)
|
||||
context.fillPath()
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
let diagramRadius = (min(chartFrame.width, chartFrame.height) / 2) * 0.925
|
||||
let targetFrame = CGRect(origin: CGPoint(x: center.x - diagramRadius,
|
||||
y: center.y - diagramRadius),
|
||||
size: CGSize(width: diagramRadius * 2,
|
||||
height: diagramRadius * 2))
|
||||
|
||||
let minX = animationComponentsPoints.last?.first?.x ?? 0
|
||||
let maxX = animationComponentsPoints.last?.last?.x ?? 0
|
||||
let startFrame = CGRect(x: minX,
|
||||
y: chartFrame.minY,
|
||||
width: maxX - minX,
|
||||
height: chartFrame.height)
|
||||
let cornerRadius = diagramRadius * animationFraction
|
||||
let fadeOutFrame = CGRect.valueBetween(start: startFrame, end: targetFrame, offset: animationFractionD)
|
||||
let fadeOutPath = CGMutablePath()
|
||||
fadeOutPath.addRect(bounds)
|
||||
fadeOutPath.addPath(CGPath(roundedRect: fadeOutFrame, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
|
||||
|
||||
context.saveGState()
|
||||
context.beginPath()
|
||||
context.addPath(fadeOutPath)
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillPath(using: .evenOdd)
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// PerformanceRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/10/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PerformanceRenderer: ChartViewRenderer {
|
||||
var containerViews: [UIView] = []
|
||||
|
||||
private var previousTickTime: TimeInterval = CACurrentMediaTime()
|
||||
|
||||
func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
let currentTime = CACurrentMediaTime()
|
||||
let delta = currentTime - previousTickTime
|
||||
previousTickTime = currentTime
|
||||
|
||||
let normalDelta = 0.017
|
||||
let redDelta = 0.05
|
||||
|
||||
if delta > normalDelta || delta < 0.75 {
|
||||
let green = CGFloat( 1.0 - crop(0, (delta - normalDelta) / (redDelta - normalDelta), 1))
|
||||
let color = UIColor(red: 1.0, green: green, blue: 0, alpha: 1)
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(CGRect(x: 0, y: 0, width: bounds.width, height: 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
191
submodules/Charts/Sources/Charts/Renderes/PieChartRenderer.swift
Normal file
191
submodules/Charts/Sources/Charts/Renderes/PieChartRenderer.swift
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
//
|
||||
// PieChartRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PieChartRenderer: BaseChartRenderer {
|
||||
struct PieComponent: Hashable {
|
||||
var color: UIColor
|
||||
var value: CGFloat
|
||||
}
|
||||
|
||||
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
|
||||
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
|
||||
}
|
||||
|
||||
var valuesFormatter: NumberFormatter = NumberFormatter()
|
||||
var drawValues: Bool = true
|
||||
|
||||
private var componentsAnimators: [AnimationController<CGFloat>] = []
|
||||
private lazy var transitionAnimator: AnimationController<CGFloat> = { AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }()
|
||||
private var oldPercentageData: [PieComponent] = []
|
||||
private var percentageData: [PieComponent] = []
|
||||
private var setlectedSegmentsAnimators: [AnimationController<CGFloat>] = []
|
||||
|
||||
var drawPie: Bool = true
|
||||
var initialAngle: CGFloat = .pi / 3
|
||||
var hasSelectedSegments: Bool {
|
||||
return selectedSegment != nil
|
||||
}
|
||||
private(set) var selectedSegment: Int?
|
||||
func selectSegmentAt(at indexToSelect: Int?, animated: Bool) {
|
||||
selectedSegment = indexToSelect
|
||||
for (index, animator) in setlectedSegmentsAnimators.enumerated() {
|
||||
let fraction: CGFloat = (index == indexToSelect) ? 1.0 : 0.0
|
||||
if animated {
|
||||
animator.animate(to: fraction, duration: .defaultDuration / 2)
|
||||
} else {
|
||||
animator.set(current: fraction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePercentageData(_ percentageData: [PieComponent], animated: Bool) {
|
||||
if self.percentageData.count != percentageData.count {
|
||||
componentsAnimators = percentageData.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
|
||||
setlectedSegmentsAnimators = percentageData.map { _ in AnimationController<CGFloat>(current: 0, refreshClosure: self.refreshClosure) }
|
||||
}
|
||||
if animated {
|
||||
self.oldPercentageData = self.currentTransitionAnimationData
|
||||
self.percentageData = percentageData
|
||||
transitionAnimator.completionClosure = { [weak self] in
|
||||
self?.oldPercentageData = []
|
||||
}
|
||||
transitionAnimator.set(current: 0)
|
||||
transitionAnimator.animate(to: 1, duration: .defaultDuration)
|
||||
} else {
|
||||
self.oldPercentageData = []
|
||||
self.percentageData = percentageData
|
||||
transitionAnimator.set(current: 0)
|
||||
}
|
||||
}
|
||||
|
||||
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
var lastRenderedBounds: CGRect = .zero
|
||||
var lastRenderedChartFrame: CGRect = .zero
|
||||
func selectedItemIndex(at point: CGPoint) -> Int? {
|
||||
let touchPosition = lastRenderedChartFrame.origin + point * lastRenderedChartFrame.size
|
||||
let center = CGPoint(x: lastRenderedChartFrame.midX, y: lastRenderedChartFrame.midY)
|
||||
let radius = min(lastRenderedChartFrame.width, lastRenderedChartFrame.height) / 2
|
||||
if center.distanceTo(touchPosition) > radius { return nil }
|
||||
let angle = (center - touchPosition).angle + .pi
|
||||
let currentData = currentlyVisibleData
|
||||
let total: CGFloat = currentData.map({ $0.value }).reduce(0, +)
|
||||
var startAngle: CGFloat = initialAngle
|
||||
for (index, piece) in currentData.enumerated() {
|
||||
let percent = piece.value / total
|
||||
let segmentSize = 2 * .pi * percent
|
||||
let endAngle = startAngle + segmentSize
|
||||
if angle >= startAngle && angle <= endAngle ||
|
||||
angle + .pi * 2 >= startAngle && angle + .pi * 2 <= endAngle {
|
||||
return index
|
||||
}
|
||||
startAngle = endAngle
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var currentTransitionAnimationData: [PieComponent] {
|
||||
if transitionAnimator.isAnimating {
|
||||
let animationFraction = transitionAnimator.current
|
||||
return percentageData.enumerated().map { arg in
|
||||
return PieComponent(color: arg.element.color,
|
||||
value: oldPercentageData[arg.offset].value * (1 - animationFraction) + arg.element.value * animationFraction)
|
||||
}
|
||||
} else {
|
||||
return percentageData
|
||||
}
|
||||
}
|
||||
|
||||
var currentlyVisibleData: [PieComponent] {
|
||||
return currentTransitionAnimationData.enumerated().map { arg in
|
||||
return PieComponent(color: arg.element.color,
|
||||
value: arg.element.value * componentsAnimators[arg.offset].current)
|
||||
}
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
lastRenderedBounds = bounds
|
||||
lastRenderedChartFrame = chartFrame
|
||||
let chartAlpha = chartAlphaAnimator.current
|
||||
if chartAlpha == 0 { return }
|
||||
|
||||
let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY)
|
||||
let radius = min(chartFrame.width, chartFrame.height) / 2
|
||||
|
||||
let currentData = currentlyVisibleData
|
||||
|
||||
let total: CGFloat = currentData.map({ $0.value }).reduce(0, +)
|
||||
guard total > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
let animationSelectionOffset: CGFloat = radius / 15
|
||||
let maximumFontSize: CGFloat = radius / 7
|
||||
let minimumFontSize: CGFloat = 4
|
||||
let centerOffsetStartAngle = CGFloat.pi / 4
|
||||
let minimumValueToDraw: CGFloat = 0.01
|
||||
let diagramRadius = radius - animationSelectionOffset
|
||||
|
||||
let numberOfVisibleItems = currentlyVisibleData.filter { $0.value > 0 }.count
|
||||
var startAngle: CGFloat = initialAngle
|
||||
for (index, piece) in currentData.enumerated() {
|
||||
let percent = piece.value / total
|
||||
guard percent > 0 else { continue }
|
||||
let segmentSize = 2 * .pi * percent * chartAlpha
|
||||
let endAngle = startAngle + segmentSize
|
||||
let centerAngle = (startAngle + endAngle) / 2
|
||||
let labelVector = CGPoint(x: cos(centerAngle),
|
||||
y: sin(centerAngle))
|
||||
|
||||
let selectionAnimationFraction = (numberOfVisibleItems > 1 ? setlectedSegmentsAnimators[index].current : 0)
|
||||
|
||||
let updatedCenter = CGPoint(x: center.x + labelVector.x * selectionAnimationFraction * animationSelectionOffset,
|
||||
y: center.y + labelVector.y * selectionAnimationFraction * animationSelectionOffset)
|
||||
if drawPie {
|
||||
context.saveGState()
|
||||
context.setFillColor(piece.color.withAlphaComponent(piece.color.alphaValue * chartAlpha).cgColor)
|
||||
context.move(to: updatedCenter)
|
||||
context.addArc(center: updatedCenter,
|
||||
radius: radius - animationSelectionOffset,
|
||||
startAngle: startAngle,
|
||||
endAngle: endAngle,
|
||||
clockwise: false)
|
||||
context.fillPath()
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
if drawValues && percent >= minimumValueToDraw {
|
||||
context.saveGState()
|
||||
|
||||
let text = valuesFormatter.string(from: percent * 100)
|
||||
let fraction = crop(0, segmentSize / centerOffsetStartAngle, 1)
|
||||
let fontSize = (minimumFontSize + (maximumFontSize - minimumFontSize) * fraction).rounded(.up)
|
||||
let labelPotisionOffset = diagramRadius / 2 + diagramRadius / 2 * (1 - fraction)
|
||||
let font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
|
||||
let labelsEaseInColor = crop(0, chartAlpha * chartAlpha * 2 - 1, 1)
|
||||
let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white.withAlphaComponent(labelsEaseInColor),
|
||||
.font: font]
|
||||
let rect = (text as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: attributes,
|
||||
context: nil)
|
||||
let labelPoint = CGPoint(x: labelVector.x * labelPotisionOffset + updatedCenter.x - rect.width / 2,
|
||||
y: labelVector.y * labelPotisionOffset + updatedCenter.y - rect.height / 2)
|
||||
(text as NSString).draw(at: labelPoint, withAttributes: attributes)
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
startAngle = endAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// VerticalLinesRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/8/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class VerticalLinesRenderer: BaseChartRenderer {
|
||||
var values: [CGFloat] = [] {
|
||||
didSet {
|
||||
alphaAnimators = values.map { _ in AnimationController<CGFloat>(current: 1.0, refreshClosure: refreshClosure) }
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
private var alphaAnimators: [AnimationController<CGFloat>] = []
|
||||
|
||||
var linesColor: UIColor = .black
|
||||
var linesWidth: CGFloat = UIView.oneDevicePixel
|
||||
|
||||
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
alphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
|
||||
context.setLineWidth(linesWidth)
|
||||
|
||||
for (index, value) in values.enumerated() {
|
||||
let alpha = alphaAnimators[index].current
|
||||
if alpha == 0 { continue }
|
||||
|
||||
context.setStrokeColor(linesColor.withAlphaComponent(linesColor.alphaValue * alpha).cgColor)
|
||||
let pointX = transform(toChartCoordinateHorizontal: value, chartFrame: chartFrame)
|
||||
context.strokeLineSegments(between: [CGPoint(x: pointX, y: chartFrame.minY),
|
||||
CGPoint(x: pointX, y: chartFrame.maxY)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
//
|
||||
// VerticalScalesRenderer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/8/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class VerticalScalesRenderer: BaseChartRenderer {
|
||||
private var verticalLabelsAndLines: [LinesChartLabel] = []
|
||||
private var animatedVerticalLabelsAndLines: [AnimatedLinesChartLabels] = []
|
||||
private lazy var horizontalLinesAlphaAnimator: AnimationController<CGFloat> = {
|
||||
return AnimationController(current: 1, refreshClosure: self.refreshClosure)
|
||||
}()
|
||||
|
||||
var drawAxisX: Bool = true
|
||||
var axisXColor: UIColor = .black
|
||||
var axisXWidth: CGFloat = UIView.oneDevicePixel
|
||||
|
||||
var isRightAligned: Bool = false
|
||||
|
||||
var horizontalLinesColor: UIColor = .black {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var horizontalLinesWidth: CGFloat = UIView.oneDevicePixel
|
||||
var lavelsAsisOffset: CGFloat = 6
|
||||
var labelsColor: UIColor = .black {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var labelsFont: UIFont = .systemFont(ofSize: 11)
|
||||
|
||||
func setHorizontalLinesVisible(_ visible: Bool, animated: Bool) {
|
||||
let destinationValue: CGFloat = visible ? 1 : 0
|
||||
guard self.horizontalLinesAlphaAnimator.end != destinationValue else { return }
|
||||
if animated {
|
||||
self.horizontalLinesAlphaAnimator.animate(to: destinationValue, duration: .defaultDuration)
|
||||
} else {
|
||||
self.horizontalLinesAlphaAnimator.set(current: destinationValue)
|
||||
}
|
||||
}
|
||||
|
||||
func setup(verticalLimitsLabels: [LinesChartLabel], animated: Bool) {
|
||||
if animated {
|
||||
var labelsToKeepVisible: [LinesChartLabel] = []
|
||||
let labelsToHide: [LinesChartLabel]
|
||||
var labelsToShow: [LinesChartLabel] = []
|
||||
|
||||
for label in verticalLimitsLabels {
|
||||
if verticalLabelsAndLines.contains(label) {
|
||||
labelsToKeepVisible.append(label)
|
||||
} else {
|
||||
labelsToShow.append(label)
|
||||
}
|
||||
}
|
||||
labelsToHide = verticalLabelsAndLines.filter { !verticalLimitsLabels.contains($0) }
|
||||
animatedVerticalLabelsAndLines.removeAll(where: { $0.isAppearing })
|
||||
verticalLabelsAndLines = labelsToKeepVisible
|
||||
|
||||
let showAnimation = AnimatedLinesChartLabels(labels: labelsToShow, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
|
||||
showAnimation.isAppearing = true
|
||||
showAnimation.alphaAnimator.set(current: 0)
|
||||
showAnimation.alphaAnimator.animate(to: 1, duration: .defaultDuration)
|
||||
showAnimation.alphaAnimator.completionClosure = { [weak self, weak showAnimation] in
|
||||
guard let self = self, let showAnimation = showAnimation else { return }
|
||||
self.animatedVerticalLabelsAndLines.removeAll(where: { $0 === showAnimation })
|
||||
self.verticalLabelsAndLines = verticalLimitsLabels
|
||||
}
|
||||
|
||||
let hideAnimation = AnimatedLinesChartLabels(labels: labelsToHide, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
|
||||
hideAnimation.isAppearing = false
|
||||
hideAnimation.alphaAnimator.set(current: 1)
|
||||
hideAnimation.alphaAnimator.animate(to: 0, duration: .defaultDuration)
|
||||
hideAnimation.alphaAnimator.completionClosure = { [weak self, weak hideAnimation] in
|
||||
guard let self = self, let hideAnimation = hideAnimation else { return }
|
||||
self.animatedVerticalLabelsAndLines.removeAll(where: { $0 === hideAnimation })
|
||||
}
|
||||
|
||||
animatedVerticalLabelsAndLines.append(showAnimation)
|
||||
animatedVerticalLabelsAndLines.append(hideAnimation)
|
||||
} else {
|
||||
verticalLabelsAndLines = verticalLimitsLabels
|
||||
animatedVerticalLabelsAndLines = []
|
||||
}
|
||||
}
|
||||
|
||||
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
||||
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
||||
let generalAlpha = chartAlphaAnimator.current
|
||||
if generalAlpha == 0 { return }
|
||||
let labelColorAlpha = labelsColor.alphaValue
|
||||
|
||||
func drawLines(_ labels: [LinesChartLabel], alpha: CGFloat) {
|
||||
var lineSegments: [CGPoint] = []
|
||||
let x0 = chartFrame.minX
|
||||
let x1 = chartFrame.maxX
|
||||
|
||||
context.setStrokeColor(horizontalLinesColor.withAlphaComponent(horizontalLinesColor.alphaValue * alpha).cgColor)
|
||||
|
||||
for lineInfo in labels {
|
||||
let y = transform(toChartCoordinateVertical: lineInfo.value, chartFrame: chartFrame).roundedUpToPixelGrid()
|
||||
lineSegments.append(CGPoint(x: x0, y: y))
|
||||
lineSegments.append(CGPoint(x: x1, y: y))
|
||||
}
|
||||
context.strokeLineSegments(between: lineSegments)
|
||||
}
|
||||
|
||||
func drawVerticalLabels(_ labels: [LinesChartLabel], attributes: [NSAttributedString.Key: Any]) {
|
||||
if isRightAligned {
|
||||
for label in labels {
|
||||
let y = transform(toChartCoordinateVertical: label.value, chartFrame: chartFrame) - labelsFont.pointSize - lavelsAsisOffset
|
||||
|
||||
let rect = (label.text as NSString).boundingRect(with: bounds.size,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: attributes,
|
||||
context: nil)
|
||||
|
||||
(label.text as NSString).draw(at: CGPoint(x:chartFrame.maxX - rect.width, y: y), withAttributes: attributes)
|
||||
}
|
||||
} else {
|
||||
for label in labels {
|
||||
let y = transform(toChartCoordinateVertical: label.value, chartFrame: chartFrame) - labelsFont.pointSize - lavelsAsisOffset
|
||||
|
||||
(label.text as NSString).draw(at: CGPoint(x:chartFrame.minX, y: y), withAttributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let horizontalLinesAlpha = horizontalLinesAlphaAnimator.current
|
||||
if horizontalLinesAlpha > 0 {
|
||||
context.setLineWidth(horizontalLinesWidth)
|
||||
|
||||
drawLines(verticalLabelsAndLines, alpha: generalAlpha)
|
||||
for animatedLabesAndLines in animatedVerticalLabelsAndLines {
|
||||
drawLines(animatedLabesAndLines.labels, alpha: animatedLabesAndLines.alphaAnimator.current * generalAlpha * horizontalLinesAlpha)
|
||||
}
|
||||
|
||||
if drawAxisX {
|
||||
context.setLineWidth(axisXWidth)
|
||||
context.setStrokeColor(axisXColor.withAlphaComponent(axisXColor.alphaValue * horizontalLinesAlpha * generalAlpha).cgColor)
|
||||
|
||||
let lineSegments: [CGPoint] = [CGPoint(x: chartFrame.minX, y: chartFrame.maxY.roundedUpToPixelGrid()),
|
||||
CGPoint(x: chartFrame.maxX, y: chartFrame.maxY.roundedUpToPixelGrid())]
|
||||
|
||||
context.strokeLineSegments(between: lineSegments)
|
||||
}
|
||||
}
|
||||
|
||||
drawVerticalLabels(verticalLabelsAndLines, attributes: [.foregroundColor: labelsColor.withAlphaComponent(labelColorAlpha * generalAlpha),
|
||||
.font: labelsFont])
|
||||
for animatedLabesAndLines in animatedVerticalLabelsAndLines {
|
||||
drawVerticalLabels(animatedLabesAndLines.labels,
|
||||
attributes: [.foregroundColor: labelsColor.withAlphaComponent(animatedLabesAndLines.alphaAnimator.current * labelColorAlpha * generalAlpha),
|
||||
.font: labelsFont])
|
||||
}
|
||||
}
|
||||
}
|
||||
178
submodules/Charts/Sources/Helpers/AnimationController.swift
Normal file
178
submodules/Charts/Sources/Helpers/AnimationController.swift
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// RangeAnimatedContainer.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/12/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol Animatable {
|
||||
static func valueBetween(start: Self, end: Self, offset: Double) -> Self
|
||||
}
|
||||
|
||||
enum TimeFunction {
|
||||
case linear
|
||||
case easeOut
|
||||
case easeIn
|
||||
|
||||
func profress(time: TimeInterval, duration: TimeInterval) -> TimeInterval {
|
||||
switch self {
|
||||
case .linear:
|
||||
return time / duration
|
||||
case .easeIn:
|
||||
return (pow(2, 10 * (time / duration - 1)) - 0.0009765625) * 1.0009775171065499
|
||||
|
||||
case .easeOut:
|
||||
return (-pow(2, -10 * time / duration)) + 1 * 1.0009775171065499
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationController<AnimatableObject: Animatable> {
|
||||
|
||||
private(set) var isAnimating: Bool = false
|
||||
private(set) var animationDuration: TimeInterval = 0.0
|
||||
private(set) var currentTime: TimeInterval = 0.0
|
||||
|
||||
private(set) var start: AnimatableObject
|
||||
private(set) var end: AnimatableObject
|
||||
private(set) var current: AnimatableObject
|
||||
|
||||
var timeFunction: TimeFunction = .linear
|
||||
|
||||
var refreshClosure: (() -> Void)?
|
||||
// var updateClosure: ((AnimatableObject) -> Void)?
|
||||
var completionClosure: (() -> Void)?
|
||||
|
||||
init(current: AnimatableObject, refreshClosure: (() -> Void)?) {
|
||||
self.current = current
|
||||
self.start = current
|
||||
self.end = current
|
||||
self.refreshClosure = refreshClosure
|
||||
}
|
||||
|
||||
func animate(to: AnimatableObject, duration: TimeInterval, timeFunction: TimeFunction = .linear) {
|
||||
self.timeFunction = timeFunction
|
||||
currentTime = 0
|
||||
animationDuration = duration
|
||||
if animationDuration > 0 {
|
||||
start = current
|
||||
end = to
|
||||
isAnimating = true
|
||||
DisplayLinkService.shared.add(listner: self)
|
||||
} else {
|
||||
start = to
|
||||
end = to
|
||||
current = to
|
||||
isAnimating = false
|
||||
DisplayLinkService.shared.remove(listner: self)
|
||||
}
|
||||
refreshClosure?()
|
||||
}
|
||||
|
||||
func set(current: AnimatableObject) {
|
||||
self.start = current
|
||||
self.end = current
|
||||
self.current = current
|
||||
|
||||
animationDuration = 0.0
|
||||
currentTime = 0.0
|
||||
// updateClosure?(current)
|
||||
refreshClosure?()
|
||||
if isAnimating {
|
||||
isAnimating = false
|
||||
DisplayLinkService.shared.remove(listner: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AnimationController: DisplayLinkListner {
|
||||
func update(delta: TimeInterval) {
|
||||
guard isAnimating else {
|
||||
DisplayLinkService.shared.remove(listner: self)
|
||||
return
|
||||
}
|
||||
|
||||
currentTime += delta
|
||||
if currentTime > animationDuration || animationDuration <= 0 {
|
||||
start = end
|
||||
current = end
|
||||
isAnimating = false
|
||||
animationDuration = 0.0
|
||||
currentTime = 0.0
|
||||
// updateClosure?(end)
|
||||
completionClosure?()
|
||||
refreshClosure?()
|
||||
DisplayLinkService.shared.remove(listner: self)
|
||||
} else {
|
||||
let offset = timeFunction.profress(time: currentTime, duration: animationDuration)
|
||||
current = AnimatableObject.valueBetween(start: start, end: end, offset: offset)
|
||||
// updateClosure?(current)
|
||||
refreshClosure?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ClosedRange: Animatable where Bound: BinaryFloatingPoint {
|
||||
static func valueBetween(start: ClosedRange<Bound>, end: ClosedRange<Bound>, offset: Double) -> ClosedRange<Bound> {
|
||||
let castedOffset = Bound(offset)
|
||||
return ClosedRange(uncheckedBounds: (lower: start.lowerBound + (end.lowerBound - start.lowerBound) * castedOffset,
|
||||
upper: start.upperBound + (end.upperBound - start.upperBound) * castedOffset))
|
||||
}
|
||||
}
|
||||
|
||||
extension CGFloat: Animatable {
|
||||
static func valueBetween(start: CGFloat, end: CGFloat, offset: Double) -> CGFloat {
|
||||
return start + (end - start) * CGFloat(offset)
|
||||
}
|
||||
}
|
||||
|
||||
extension Double: Animatable {
|
||||
static func valueBetween(start: Double, end: Double, offset: Double) -> Double {
|
||||
return start + (end - start) * Double(offset)
|
||||
}
|
||||
}
|
||||
|
||||
extension Int: Animatable {
|
||||
static func valueBetween(start: Int, end: Int, offset: Double) -> Int {
|
||||
return start + Int(Double(end - start) * offset)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint: Animatable {
|
||||
static func valueBetween(start: CGPoint, end: CGPoint, offset: Double) -> CGPoint {
|
||||
return CGPoint(x: start.x + (end.x - start.x) * CGFloat(offset),
|
||||
y: start.y + (end.y - start.y) * CGFloat(offset))
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect: Animatable {
|
||||
static func valueBetween(start: CGRect, end: CGRect, offset: Double) -> CGRect {
|
||||
return CGRect(x: start.origin.x + (end.origin.x - start.origin.x) * CGFloat(offset),
|
||||
y: start.origin.y + (end.origin.y - start.origin.y) * CGFloat(offset),
|
||||
width: start.width + (end.width - start.width) * CGFloat(offset),
|
||||
height: start.height + (end.height - start.height) * CGFloat(offset))
|
||||
}
|
||||
}
|
||||
|
||||
struct UIColorContainer: Animatable {
|
||||
var color: UIColor
|
||||
|
||||
static func valueBetween(start: UIColorContainer, end: UIColorContainer, offset: Double) -> UIColorContainer {
|
||||
return UIColorContainer(color: UIColor.valueBetween(start: start.color, end: end.color, offset: offset))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
static func valueBetween(start: UIColor, end: UIColor, offset: Double) -> UIColor {
|
||||
let offsetF = CGFloat(offset)
|
||||
let startCIColor = CIColor(color: start)
|
||||
let endCIColor = CIColor(color: end)
|
||||
return UIColor(red: startCIColor.red + (endCIColor.red - startCIColor.red) * offsetF,
|
||||
green: startCIColor.green + (endCIColor.green - startCIColor.green) * offsetF,
|
||||
blue: startCIColor.blue + (endCIColor.blue - startCIColor.blue) * offsetF,
|
||||
alpha: startCIColor.alpha + (endCIColor.alpha - startCIColor.alpha) * offsetF)
|
||||
}
|
||||
}
|
||||
18
submodules/Charts/Sources/Helpers/Array+Utils.swift
Normal file
18
submodules/Charts/Sources/Helpers/Array+Utils.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Array+Utils.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array {
|
||||
func safeElement(at index: Int) -> Element? {
|
||||
if index >= 0 && index < count {
|
||||
return self[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
17
submodules/Charts/Sources/Helpers/CGFloat.swift
Normal file
17
submodules/Charts/Sources/Helpers/CGFloat.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// CGFloat.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private let screenScale: CGFloat = UIScreen.main.scale
|
||||
|
||||
extension CGFloat {
|
||||
func roundedUpToPixelGrid() -> CGFloat {
|
||||
return (self * screenScale).rounded(.up) / screenScale
|
||||
}
|
||||
}
|
||||
219
submodules/Charts/Sources/Helpers/CGPoint+Extensions.swift
Normal file
219
submodules/Charts/Sources/Helpers/CGPoint+Extensions.swift
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
//
|
||||
// CGPoint+Extensions.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension CGPoint {
|
||||
public init(vector: CGVector) {
|
||||
self.init(x: vector.dx, y: vector.dy)
|
||||
}
|
||||
|
||||
|
||||
public init(angle: CGFloat) {
|
||||
self.init(x: cos(angle), y: sin(angle))
|
||||
}
|
||||
|
||||
|
||||
public mutating func offset(dx: CGFloat, dy: CGFloat) -> CGPoint {
|
||||
x += dx
|
||||
y += dy
|
||||
return self
|
||||
}
|
||||
|
||||
public func length() -> CGFloat {
|
||||
return sqrt(x*x + y*y)
|
||||
}
|
||||
|
||||
public func lengthSquared() -> CGFloat {
|
||||
return x*x + y*y
|
||||
}
|
||||
|
||||
func normalized() -> CGPoint {
|
||||
let len = length()
|
||||
return len>0 ? self / len : CGPoint.zero
|
||||
}
|
||||
|
||||
public mutating func normalize() -> CGPoint {
|
||||
self = normalized()
|
||||
return self
|
||||
}
|
||||
|
||||
public func distanceTo(_ point: CGPoint) -> CGFloat {
|
||||
return (self - point).length()
|
||||
}
|
||||
|
||||
public var angle: CGFloat {
|
||||
return atan2(y, x)
|
||||
}
|
||||
|
||||
public var cgSize: CGSize {
|
||||
return CGSize(width: x, height: y)
|
||||
}
|
||||
|
||||
func rotate(origin: CGPoint, angle: CGFloat) -> CGPoint {
|
||||
let point = self - origin
|
||||
let s = sin(angle)
|
||||
let c = cos(angle)
|
||||
return CGPoint(x: c * point.x - s * point.y,
|
||||
y: s * point.x + c * point.y) + origin
|
||||
}
|
||||
}
|
||||
|
||||
extension CGSize {
|
||||
public var cgPoint: CGPoint {
|
||||
return CGPoint(x: width, y: height)
|
||||
}
|
||||
|
||||
public init(point: CGPoint) {
|
||||
self.init(width: point.x, height: point.y)
|
||||
}
|
||||
}
|
||||
|
||||
public func + (left: CGPoint, right: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: left.x + right.x, y: left.y + right.y)
|
||||
}
|
||||
|
||||
public func += (left: inout CGPoint, right: CGPoint) {
|
||||
left = left + right
|
||||
}
|
||||
|
||||
public func + (left: CGPoint, right: CGVector) -> CGPoint {
|
||||
return CGPoint(x: left.x + right.dx, y: left.y + right.dy)
|
||||
}
|
||||
|
||||
public func += (left: inout CGPoint, right: CGVector) {
|
||||
left = left + right
|
||||
}
|
||||
|
||||
public func - (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x - right.x, y: left.y - right.y) }
|
||||
public func - (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width - right.width, height: left.height - right.height) }
|
||||
public func - (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width - right.x, height: left.height - right.x) }
|
||||
public func - (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x - right.width, y: left.y - right.height) }
|
||||
|
||||
public func -= (left: inout CGPoint, right: CGPoint) {
|
||||
left = left - right
|
||||
}
|
||||
|
||||
public func - (left: CGPoint, right: CGVector) -> CGPoint {
|
||||
return CGPoint(x: left.x - right.dx, y: left.y - right.dy)
|
||||
}
|
||||
|
||||
public func -= (left: inout CGPoint, right: CGVector) {
|
||||
left = left - right
|
||||
}
|
||||
|
||||
public func *= (left: inout CGPoint, right: CGPoint) {
|
||||
left = left * right
|
||||
}
|
||||
|
||||
public func * (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x * scalar, y: point.y * scalar) }
|
||||
public func * (point: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: point.width * scalar, height: point.height * scalar) }
|
||||
|
||||
public func *= (point: inout CGPoint, scalar: CGFloat) { point = point * scalar }
|
||||
|
||||
public func * (left: CGPoint, right: CGVector) -> CGPoint {
|
||||
return CGPoint(x: left.x * right.dx, y: left.y * right.dy)
|
||||
}
|
||||
|
||||
public func *= (left: inout CGPoint, right: CGVector) {
|
||||
left = left * right
|
||||
}
|
||||
|
||||
public func / (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x / right.x, y: left.y / right.y) }
|
||||
public func / (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width / right.width, height: left.height / right.height) }
|
||||
public func / (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x / right.width, y: left.y / right.height) }
|
||||
public func / (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width / right.x, height: left.height / right.y) }
|
||||
public func /= (left: inout CGPoint, right: CGPoint) { left = left / right }
|
||||
public func /= (left: inout CGSize, right: CGSize) { left = left / right }
|
||||
public func /= (left: inout CGSize, right: CGPoint) { left = left / right }
|
||||
public func /= (left: inout CGPoint, right: CGSize) { left = left / right }
|
||||
|
||||
|
||||
public func / (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x / scalar, y: point.y / scalar) }
|
||||
public func / (point: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: point.width / scalar, height: point.height / scalar) }
|
||||
|
||||
public func /= (point: inout CGPoint, scalar: CGFloat) {
|
||||
point = point / scalar
|
||||
}
|
||||
|
||||
public func / (left: CGPoint, right: CGVector) -> CGPoint {
|
||||
return CGPoint(x: left.x / right.dx, y: left.y / right.dy)
|
||||
}
|
||||
|
||||
public func / (left: CGSize, right: CGVector) -> CGSize {
|
||||
return CGSize(width: left.width / right.dx, height: left.height / right.dy)
|
||||
}
|
||||
|
||||
public func /= (left: inout CGPoint, right: CGVector) {
|
||||
left = left / right
|
||||
}
|
||||
|
||||
public func * (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x * right.x, y: left.y * right.y) }
|
||||
public func * (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x * right.width, y: left.y * right.height) }
|
||||
public func *= (left: inout CGPoint, right: CGSize) { left = left * right }
|
||||
public func * (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width * right.width, height: left.height * right.height) }
|
||||
public func *= (left: inout CGSize, right: CGSize) { left = left * right }
|
||||
public func * (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width * right.x, height: left.height * right.y) }
|
||||
public func *= (left: inout CGSize, right: CGPoint) { left = left * right }
|
||||
|
||||
|
||||
public func lerp(start: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint {
|
||||
return start + (end - start) * t
|
||||
}
|
||||
|
||||
public func abs(_ point: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: abs(point.x), y: abs(point.y))
|
||||
}
|
||||
|
||||
extension CGSize {
|
||||
var isValid: Bool {
|
||||
return width > 0 && height > 0 && width != .infinity && height != .infinity && width != .nan && height != .nan
|
||||
}
|
||||
|
||||
var ratio: CGFloat {
|
||||
return width / height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension CGRect {
|
||||
static var identity: CGRect {
|
||||
return CGRect(x: 0, y: 0, width: 1, height: 1)
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return origin + size.cgPoint / 2
|
||||
}
|
||||
|
||||
var rounded: CGRect {
|
||||
return CGRect(x: origin.x.rounded(),
|
||||
y: origin.y.rounded(),
|
||||
width: width.rounded(.up),
|
||||
height: height.rounded(.up))
|
||||
}
|
||||
|
||||
var mirroredVertically: CGRect {
|
||||
return CGRect(x: origin.x,
|
||||
y: 1.0 - (origin.y + height),
|
||||
width: width,
|
||||
height: height)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGAffineTransform {
|
||||
func inverted(with size: CGSize) -> CGAffineTransform {
|
||||
var transform = self
|
||||
let transformedSize = CGRect(origin: .zero, size: size).applying(transform).size
|
||||
transform.tx /= transformedSize.width;
|
||||
transform.ty /= transformedSize.height;
|
||||
transform = transform.inverted()
|
||||
transform.tx *= transformedSize.width;
|
||||
transform.ty *= transformedSize.height;
|
||||
return transform
|
||||
}
|
||||
}
|
||||
15
submodules/Charts/Sources/Helpers/ClosedRange+Utils.swift
Normal file
15
submodules/Charts/Sources/Helpers/ClosedRange+Utils.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// ClosedRange+Utils.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ClosedRange where Bound: Numeric {
|
||||
var distance: Bound {
|
||||
return upperBound - lowerBound
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// CustomNavigationController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrew Solovey on 15/03/2019.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class CustomNavigationController: UINavigationController {
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return topViewController?.preferredStatusBarStyle ?? .default
|
||||
}
|
||||
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
return topViewController?.preferredStatusBarUpdateAnimation ?? .fade
|
||||
}
|
||||
}
|
||||
114
submodules/Charts/Sources/Helpers/DisplayLinkService.swift
Normal file
114
submodules/Charts/Sources/Helpers/DisplayLinkService.swift
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// DisplayLinkService.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreGraphics
|
||||
|
||||
public protocol DisplayLinkListner: class {
|
||||
func update(delta: TimeInterval)
|
||||
}
|
||||
|
||||
// DispatchSource mares refreshes more accurate
|
||||
class DisplayLinkService {
|
||||
let listners = NSHashTable<AnyObject>.weakObjects()
|
||||
static let shared = DisplayLinkService()
|
||||
|
||||
public func add(listner: DisplayLinkListner) {
|
||||
listners.add(listner)
|
||||
startDisplayLink()
|
||||
}
|
||||
|
||||
public func remove(listner: DisplayLinkListner) {
|
||||
listners.remove(listner)
|
||||
|
||||
if listners.count == 0 {
|
||||
stopDisplayLink()
|
||||
}
|
||||
}
|
||||
|
||||
// private init() {
|
||||
// displayLink.add(to: .main, forMode: .common)
|
||||
// displayLink.preferredFramesPerSecond = 60
|
||||
// displayLink.isPaused = true
|
||||
// }
|
||||
//
|
||||
// // MARK: - Display Link
|
||||
// private lazy var displayLink: CADisplayLink! = { CADisplayLink(target: self, selector: #selector(displayLinkDidFire)) } ()
|
||||
// private var previousTickTime = 0.0
|
||||
//
|
||||
// private func startDisplayLink() {
|
||||
// guard displayLink.isPaused else {
|
||||
// return
|
||||
// }
|
||||
// previousTickTime = CACurrentMediaTime()
|
||||
// displayLink.isPaused = false
|
||||
// }
|
||||
//
|
||||
// @objc private func displayLinkDidFire(_ displayLink: CADisplayLink) {
|
||||
// let currentTime = CACurrentMediaTime()
|
||||
// let delta = currentTime - previousTickTime
|
||||
// previousTickTime = currentTime
|
||||
// let allListners = listners.allObjects
|
||||
// var hasListners = false
|
||||
// for listner in allListners {
|
||||
// (listner as! DisplayLinkListner).update(delta: delta)
|
||||
// hasListners = true
|
||||
// }
|
||||
//
|
||||
// if !hasListners {
|
||||
// stopDisplayLink()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func stopDisplayLink() {
|
||||
// displayLink.isPaused = true
|
||||
// }
|
||||
|
||||
private init() {
|
||||
dispatchSourceTimer.schedule(deadline: .now() + 1.0 / 60, repeating: 1.0 / 60)
|
||||
dispatchSourceTimer.setEventHandler {
|
||||
DispatchQueue.main.sync {
|
||||
self.fire()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var dispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: .global(qos: .userInteractive))
|
||||
private var dispatchSourceTimerStarted: Bool = false
|
||||
private var previousTickTime = 0.0
|
||||
|
||||
private func startDisplayLink() {
|
||||
guard !dispatchSourceTimerStarted else { return }
|
||||
dispatchSourceTimerStarted = true
|
||||
previousTickTime = CACurrentMediaTime()
|
||||
dispatchSourceTimer.resume()
|
||||
}
|
||||
|
||||
private func stopDisplayLink() {
|
||||
guard dispatchSourceTimerStarted else { return }
|
||||
dispatchSourceTimerStarted = false
|
||||
dispatchSourceTimer.suspend()
|
||||
}
|
||||
|
||||
public func fire() {
|
||||
let currentTime = CACurrentMediaTime()
|
||||
|
||||
let delta = currentTime - previousTickTime
|
||||
previousTickTime = currentTime
|
||||
let allListners = listners.allObjects
|
||||
var hasListners = false
|
||||
for listner in allListners {
|
||||
(listner as! DisplayLinkListner).update(delta: delta)
|
||||
hasListners = true
|
||||
}
|
||||
|
||||
if !hasListners {
|
||||
stopDisplayLink()
|
||||
}
|
||||
}
|
||||
}
|
||||
12
submodules/Charts/Sources/Helpers/GlobalHelpers.swift
Normal file
12
submodules/Charts/Sources/Helpers/GlobalHelpers.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// GlobalHelpers.swift
|
||||
// TrackingRecorder
|
||||
//
|
||||
// Created by Andrew Solovey on 07.09.2018.
|
||||
// Copyright © 2018 Andrew Solovey. All rights reserved.
|
||||
//
|
||||
|
||||
public func crop<Type>(_ lower: Type, _ val: Type, _ upper: Type) -> Type where Type : Comparable {
|
||||
assert(lower < upper, "Invalid lover and upper values")
|
||||
return max(lower, min(upper, val))
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// NumberFormatter+Utils.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/12/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension NumberFormatter {
|
||||
func string(from value: CGFloat) -> String {
|
||||
return string(from: Double(value))
|
||||
}
|
||||
|
||||
func string(from value: Double) -> String {
|
||||
return string(from: NSNumber(value: Double(value))) ?? ""
|
||||
}
|
||||
}
|
||||
17
submodules/Charts/Sources/Helpers/OnePixelConstraint.swift
Normal file
17
submodules/Charts/Sources/Helpers/OnePixelConstraint.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// OnePixelConstraint.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public class OnePixelConstrain: NSLayoutConstraint {
|
||||
public override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
constant = UIView.oneDevicePixel
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// ScalesNumberFormatter.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private let milionsScale = "M"
|
||||
private let thousandsScale = "K"
|
||||
|
||||
class ScalesNumberFormatter: NumberFormatter {
|
||||
override func string(from number: NSNumber) -> String? {
|
||||
let value = number.doubleValue
|
||||
let pow = log10(value)
|
||||
if pow >= 6 {
|
||||
guard let string = super.string(from: NSNumber(value: value / 1_000_000)) else {
|
||||
return nil
|
||||
}
|
||||
return string + milionsScale
|
||||
} else if pow >= 4 {
|
||||
guard let string = super.string(from: NSNumber(value: value / 1_000)) else {
|
||||
return nil
|
||||
}
|
||||
return string + thousandsScale
|
||||
} else {
|
||||
return super.string(from: number)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
submodules/Charts/Sources/Helpers/TimeInterval+Utils.swift
Normal file
27
submodules/Charts/Sources/Helpers/TimeInterval+Utils.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// TimeInterval+Utils.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TimeInterval {
|
||||
static let minute: TimeInterval = 60
|
||||
static let hour: TimeInterval = 60 * 60
|
||||
static let day: TimeInterval = 60 * 60 * 24
|
||||
static let osXDuration: TimeInterval = 0.25
|
||||
static let expandAnimationDuration: TimeInterval = 0.4
|
||||
static var animationDurationMultipler: Double = 1.0
|
||||
|
||||
static var defaultDuration: TimeInterval {
|
||||
return innerDefaultDuration * animationDurationMultipler
|
||||
}
|
||||
private static var innerDefaultDuration: TimeInterval = osXDuration
|
||||
|
||||
static func setDefaultSuration(_ duration: TimeInterval) {
|
||||
innerDefaultDuration = duration
|
||||
}
|
||||
}
|
||||
36
submodules/Charts/Sources/Helpers/TimeZone.swift
Normal file
36
submodules/Charts/Sources/Helpers/TimeZone.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// TimeZone.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/9/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TimeZone {
|
||||
static let utc = TimeZone(secondsFromGMT: 0)!
|
||||
}
|
||||
|
||||
extension Locale {
|
||||
static let posix = Locale(identifier: "en_US_POSIX")
|
||||
}
|
||||
|
||||
extension Calendar {
|
||||
static let utc: Calendar = {
|
||||
var calendar = Calendar.current
|
||||
calendar.locale = Locale.posix
|
||||
calendar.timeZone = TimeZone.utc
|
||||
return calendar
|
||||
}()
|
||||
}
|
||||
|
||||
extension DateFormatter {
|
||||
static func utc(format: String = "") -> DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar.utc
|
||||
formatter.dateFormat = format
|
||||
formatter.timeZone = TimeZone.utc
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
64
submodules/Charts/Sources/Helpers/UIColor+Utils.swift
Normal file
64
submodules/Charts/Sources/Helpers/UIColor+Utils.swift
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// UIColor+Utils.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
public convenience init?(hexString: String) {
|
||||
let r, g, b, a: CGFloat
|
||||
|
||||
if hexString.hasPrefix("#") {
|
||||
let start = hexString.index(hexString.startIndex, offsetBy: 1)
|
||||
let hexColor = String(hexString[start...])
|
||||
|
||||
if hexColor.count == 8 {
|
||||
let scanner = Scanner(string: hexColor)
|
||||
var hexNumber: UInt64 = 0
|
||||
|
||||
if scanner.scanHexInt64(&hexNumber) {
|
||||
r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
|
||||
g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
|
||||
b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
|
||||
a = CGFloat(hexNumber & 0x000000ff) / 255
|
||||
|
||||
self.init(red: r, green: g, blue: b, alpha: a)
|
||||
return
|
||||
}
|
||||
} else if hexColor.count == 6 {
|
||||
let scanner = Scanner(string: hexColor)
|
||||
var hexNumber: UInt64 = 0
|
||||
|
||||
if scanner.scanHexInt64(&hexNumber) {
|
||||
r = CGFloat((hexNumber & 0xff0000) >> 16) / 255
|
||||
g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255
|
||||
b = CGFloat((hexNumber & 0x0000ff) >> 0) / 255
|
||||
|
||||
self.init(red: r, green: g, blue: b, alpha: 1.0)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
|
||||
if #available(iOS 10.0, *) {
|
||||
return UIGraphicsImageRenderer(size: size).image { rendererContext in
|
||||
self.setFill()
|
||||
rendererContext.fill(CGRect(origin: .zero, size: size))
|
||||
}
|
||||
} else {
|
||||
return UIImage()
|
||||
}
|
||||
}
|
||||
|
||||
var redValue: CGFloat{ return CIColor(color: self).red }
|
||||
var greenValue: CGFloat{ return CIColor(color: self).green }
|
||||
var blueValue: CGFloat{ return CIColor(color: self).blue }
|
||||
var alphaValue: CGFloat{ return CIColor(color: self).alpha }
|
||||
}
|
||||
28
submodules/Charts/Sources/Helpers/UIImage+Utils.swift
Normal file
28
submodules/Charts/Sources/Helpers/UIImage+Utils.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// UIImage+Utils.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/8/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
static let arrowRight = UIImage(bundleImageName: "Chart/arrow_right")
|
||||
static let arrowLeft = UIImage(bundleImageName: "Chart/arrow_left")
|
||||
|
||||
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
|
||||
let rect = CGRect(origin: .zero, size: size)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
|
||||
color.setFill()
|
||||
UIRectFill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
guard let cgImage = image?.cgImage else { return nil }
|
||||
self.init(cgImage: cgImage)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
24
submodules/Charts/Sources/Helpers/UIImageView+Utils.swift
Normal file
24
submodules/Charts/Sources/Helpers/UIImageView+Utils.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
37
submodules/Charts/Sources/Helpers/UILabel+Utils.swift
Normal file
37
submodules/Charts/Sources/Helpers/UILabel+Utils.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
57
submodules/Charts/Sources/Helpers/UIView+Extensions.swift
Normal file
57
submodules/Charts/Sources/Helpers/UIView+Extensions.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
76
submodules/Charts/Sources/Models/ChartLineData.swift
Normal file
76
submodules/Charts/Sources/Models/ChartLineData.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// ChartLineData.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/13/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct ChartLineData {
|
||||
var title: String
|
||||
var color: UIColor
|
||||
var width: CGFloat?
|
||||
var points: [CGPoint]
|
||||
}
|
||||
|
||||
extension ChartLineData {
|
||||
static func horizontalRange(lines: [ChartLineData]) -> ClosedRange<CGFloat>? {
|
||||
guard let firstPoint = lines.first?.points.first else { return nil }
|
||||
var hMin: CGFloat = firstPoint.x
|
||||
var hMax: CGFloat = firstPoint.x
|
||||
|
||||
for line in lines {
|
||||
if let first = line.points.first,
|
||||
let last = line.points.last {
|
||||
hMin = min(hMin, first.x)
|
||||
hMax = max(hMax, last.x)
|
||||
}
|
||||
}
|
||||
|
||||
return hMin...hMax
|
||||
}
|
||||
|
||||
static func verticalRange(lines: [ChartLineData], calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
||||
if let calculatingRange = calculatingRange {
|
||||
guard let initalStart = lines.first?.points.first(where: { $0.x > calculatingRange.lowerBound &&
|
||||
$0.x < calculatingRange.upperBound }) else { return nil }
|
||||
var vMin: CGFloat = initalStart.y
|
||||
var vMax: CGFloat = initalStart.y
|
||||
for line in lines {
|
||||
if var index = line.points.firstIndex(where: { $0.x > calculatingRange.lowerBound }) {
|
||||
if addBounds {
|
||||
index = max(0, index - 1)
|
||||
}
|
||||
while index < line.points.count {
|
||||
let point = line.points[index]
|
||||
if point.x < calculatingRange.upperBound {
|
||||
vMin = min(vMin, point.y)
|
||||
vMax = max(vMax, point.y)
|
||||
} else if addBounds {
|
||||
vMin = min(vMin, point.y)
|
||||
vMax = max(vMax, point.y)
|
||||
break
|
||||
} else {
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return vMin...vMax
|
||||
} else {
|
||||
guard let firstPoint = lines.first?.points.first else { return nil }
|
||||
var vMin: CGFloat = firstPoint.y
|
||||
var vMax: CGFloat = firstPoint.y
|
||||
for line in lines {
|
||||
for point in line.points {
|
||||
vMin = min(vMin, point.y)
|
||||
vMax = max(vMax, point.y)
|
||||
}
|
||||
}
|
||||
return vMin...vMax
|
||||
}
|
||||
}
|
||||
}
|
||||
175
submodules/Charts/Sources/Models/ColorMode.swift
Normal file
175
submodules/Charts/Sources/Models/ColorMode.swift
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
//
|
||||
// ColorMode.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrew Solovey on 15/03/2019.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AppBundle
|
||||
|
||||
protocol ColorModeContainer {
|
||||
func apply(colorMode: ColorMode, animated: Bool)
|
||||
}
|
||||
|
||||
enum ColorMode {
|
||||
case day
|
||||
case night
|
||||
}
|
||||
|
||||
extension ColorMode {
|
||||
var chartTitleColor: UIColor { // Текст с датой на чарте
|
||||
switch self {
|
||||
case .day: return .black
|
||||
case .night: return .white
|
||||
}
|
||||
}
|
||||
|
||||
var actionButtonColor: UIColor { // Кнопка Zoom Out/ Смена режима день/ночь
|
||||
switch self {
|
||||
case .day: return UIColor(red: 53/255.0, green: 120/255.0, blue: 246/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 84/255.0, green: 164/255.0, blue: 247/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var tableBackgroundColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 239/255.0, green: 239/255.0, blue: 244/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 24/255.0, green: 34/255.0, blue: 45/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var chartBackgroundColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 34/255.0, green: 47/255.0, blue: 63/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var sectionTitleColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 109/255.0, green: 109/255.0, blue: 114/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 133/255.0, green: 150/255.0, blue: 171/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var tableSeparatorColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 200/255.0, green: 199/255.0, blue: 204/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 18/255.0, green: 26/255.0, blue: 35/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var chartLabelsColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 37/255.0, green: 37/255.0, blue: 41/255.0, alpha: 0.5)
|
||||
case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
var chartHelperLinesColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 24/255.0, green: 45/255.0, blue: 59/255.0, alpha: 0.1)
|
||||
case .night: return UIColor(red: 133/255.0, green: 150/255.0, blue: 171/255.0, alpha: 0.20)
|
||||
}
|
||||
}
|
||||
|
||||
var chartStrongLinesColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 24/255.0, green: 45/255.0, blue: 59/255.0, alpha: 0.35)
|
||||
case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.45)
|
||||
}
|
||||
}
|
||||
|
||||
var barChartStrongLinesColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 37/255.0, green: 37/255.0, blue: 41/255.0, alpha: 0.2)
|
||||
case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.45)
|
||||
}
|
||||
}
|
||||
|
||||
var chartDetailsTextColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 109/255.0, green: 109/255.0, blue: 114/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var chartDetailsArrowColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 197/255.0, green: 199/255.0, blue: 205/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 76/255.0, green: 84/255.0, blue: 96/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var chartDetailsViewColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 245/255.0, green: 245/255.0, blue: 251/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 25/255.0, green: 35/255.0, blue: 47/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var descriptionChatNameColor: UIColor {
|
||||
switch self {
|
||||
case .day: return .black
|
||||
case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var descriptionActionColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 1/255.0, green: 125/255.0, blue: 229/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 24/255.0, green: 145/255.0, blue: 255/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var rangeViewBackgroundColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 34/255.0, green: 47/255.0, blue: 63/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var rangeViewFrameColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 202/255.0, green: 212/255.0, blue: 222/255.0, alpha: 1.0)
|
||||
case .night: return UIColor(red: 53/255.0, green: 70/255.0, blue: 89/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var rangeViewTintColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor(red: 239/255.0, green: 239/255.0, blue: 244/255.0, alpha: 0.5)
|
||||
case .night: return UIColor(red: 24/255.0, green: 34/255.0, blue: 45/255.0, alpha: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
var rangeViewMarkerColor: UIColor {
|
||||
switch self {
|
||||
case .day: return UIColor.white
|
||||
case .night: return UIColor.white
|
||||
}
|
||||
}
|
||||
|
||||
var statusBarStyle: UIStatusBarStyle {
|
||||
switch self {
|
||||
case .day: return .default
|
||||
case .night: return .lightContent
|
||||
}
|
||||
}
|
||||
|
||||
var viewTintColor: UIColor {
|
||||
switch self {
|
||||
case .day: return .black
|
||||
case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var rangeCropImage: UIImage? {
|
||||
switch self {
|
||||
case .day: return UIImage(bundleImageName: "Chart/selection_frame_light")
|
||||
case .night: return UIImage(bundleImageName: "Chart/selection_frame_dark")
|
||||
}
|
||||
}
|
||||
}
|
||||
25
submodules/Charts/Sources/Models/LinesChartLabel.swift
Normal file
25
submodules/Charts/Sources/Models/LinesChartLabel.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// LinesChartLabel.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/18/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct LinesChartLabel: Hashable {
|
||||
let value: CGFloat
|
||||
let text: String
|
||||
}
|
||||
|
||||
class AnimatedLinesChartLabels {
|
||||
var labels: [LinesChartLabel]
|
||||
var isAppearing: Bool = false
|
||||
let alphaAnimator: AnimationController<CGFloat>
|
||||
|
||||
init(labels: [LinesChartLabel], alphaAnimator: AnimationController<CGFloat>) {
|
||||
self.labels = labels
|
||||
self.alphaAnimator = alphaAnimator
|
||||
}
|
||||
}
|
||||
15
submodules/Charts/Sources/Models/LinesSelectionLabel.swift
Normal file
15
submodules/Charts/Sources/Models/LinesSelectionLabel.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// LinesSelectionLabel.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 3/18/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct LinesSelectionLabel {
|
||||
let coordinate: CGPoint
|
||||
let valueText: String
|
||||
let color: UIColor
|
||||
}
|
||||
|
|
@ -22,10 +22,6 @@ import AppBundle
|
|||
import LocalizedPeerData
|
||||
import TelegramIntents
|
||||
|
||||
public func useSpecialTabBarIcons() -> Bool {
|
||||
return (Date(timeIntervalSince1970: 1545642000)...Date(timeIntervalSince1970: 1546387200)).contains(Date())
|
||||
}
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
|
||||
let scrollToItem: ListViewScrollToItem
|
||||
|
|
@ -182,8 +178,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
self.tabBarItem.title = self.presentationData.strings.DialogList_Title
|
||||
|
||||
let icon: UIImage?
|
||||
if (useSpecialTabBarIcons()) {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/NY/IconChats")
|
||||
if useSpecialTabBarIcons() {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconChats")
|
||||
} else {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/IconChats")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -727,7 +727,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||
return (views, local)
|
||||
}
|
||||
}
|
||||
|> mapToSignal{ viewsAndPeers -> Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> in
|
||||
|> mapToSignal { viewsAndPeers -> Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> in
|
||||
return context.account.postbox.unreadMessageCountsView(items: viewsAndPeers.0.map {.peer($0.peerId)}) |> map { values in
|
||||
var unread: [PeerId: (Int32, Bool)] = [:]
|
||||
for peerView in viewsAndPeers.0 {
|
||||
|
|
@ -816,8 +816,18 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||
)
|
||||
}
|
||||
|
||||
return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get())
|
||||
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState -> ([ChatListSearchEntry], Bool)? in
|
||||
let resolvedMessage = .single(nil)
|
||||
|> then(context.sharedContext.resolveUrl(account: context.account, url: query)
|
||||
|> mapToSignal { resolvedUrl -> Signal<Message?, NoError> in
|
||||
if case let .channelMessage(peerId, messageId) = resolvedUrl {
|
||||
return downloadMessage(postbox: context.account.postbox, network: context.account.network, messageId: messageId)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
})
|
||||
|
||||
return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), resolvedMessage)
|
||||
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, resolvedMessage -> ([ChatListSearchEntry], Bool)? in
|
||||
var entries: [ChatListSearchEntry] = []
|
||||
let isSearching = foundRemotePeers.2 || foundRemoteMessages.1
|
||||
var index = 0
|
||||
|
|
@ -950,6 +960,17 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||
}
|
||||
}
|
||||
|
||||
if let message = resolvedMessage {
|
||||
var peer = RenderedPeer(message: message)
|
||||
if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
|
||||
if let channelPeer = message.peers[migrationReference.peerId] {
|
||||
peer = RenderedPeer(peer: channelPeer)
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, nil, presentationData))
|
||||
index += 1
|
||||
}
|
||||
|
||||
if !foundRemotePeers.2 {
|
||||
index = 0
|
||||
for message in foundRemoteMessages.0.0 {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,12 @@ public class ContactsController: ViewController {
|
|||
self.title = self.presentationData.strings.Contacts_Title
|
||||
self.tabBarItem.title = self.presentationData.strings.Contacts_Title
|
||||
|
||||
let icon = UIImage(bundleImageName: "Chat List/Tabs/IconContacts")
|
||||
let icon: UIImage?
|
||||
if useSpecialTabBarIcons() {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconContacts")
|
||||
} else {
|
||||
icon = UIImage(bundleImageName: "Chat List/Tabs/IconContacts")
|
||||
}
|
||||
|
||||
self.tabBarItem.image = icon
|
||||
self.tabBarItem.selectedImage = icon
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ class TabBarNode: ASDisplayNode {
|
|||
if horizontal {
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: originX + 15.0, y: 3.0), size: backgroundSize)
|
||||
} else {
|
||||
let contentWidth = node.contentWidth ?? node.frame.width
|
||||
let contentWidth: CGFloat = 25.0 //node.contentWidth ?? node.frame.width
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.frame.width / 2.0) + contentWidth - backgroundSize.width - 5.0, y: self.centered ? 9.0 : 2.0), size: backgroundSize)
|
||||
}
|
||||
transition.updateFrame(node: container.badgeContainerNode, frame: backgroundFrame)
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||
private let centralItemRightBarButtonItem = Promise<UIBarButtonItem?>()
|
||||
private let centralItemRightBarButtonItems = Promise<[UIBarButtonItem]?>(nil)
|
||||
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
|
||||
private let centralItemFooterContentNode = Promise<GalleryFooterContentNode?>()
|
||||
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
|
||||
private let centralItemAttributesDisposable = DisposableSet();
|
||||
|
||||
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
|
||||
|
|
@ -531,9 +531,9 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||
}
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, overlayContentNode in
|
||||
self?.galleryNode.updatePresentationState({
|
||||
$0.withUpdatedFooterContentNode(footerContentNode)
|
||||
$0.withUpdatedFooterContentNode(footerContentNode).withUpdatedOverlayContentNode(overlayContentNode)
|
||||
}, transition: .immediate)
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -250,8 +250,8 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
|
|||
self.updateThumbnailContainerNodeAlpha(transition)
|
||||
}
|
||||
|
||||
self.footerNode.updateLayout(layout, footerContentNode: self.presentationState.footerContentNode, thumbnailPanelHeight: thumbnailPanelHeight, transition: transition)
|
||||
|
||||
self.footerNode.updateLayout(layout, footerContentNode: self.presentationState.footerContentNode, overlayContentNode: self.presentationState.overlayContentNode, thumbnailPanelHeight: thumbnailPanelHeight, transition: transition)
|
||||
|
||||
let previousContentHeight = self.scrollView.contentSize.height
|
||||
let previousVerticalOffset = self.scrollView.contentOffset.y
|
||||
|
||||
|
|
@ -276,14 +276,14 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
|
|||
let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0
|
||||
self.navigationBar?.alpha = alpha
|
||||
self.statusBar?.updateAlpha(alpha, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
self.footerNode.alpha = alpha
|
||||
self.footerNode.setVisibilityAlpha(alpha)
|
||||
self.updateThumbnailContainerNodeAlpha(.immediate)
|
||||
})
|
||||
} else {
|
||||
let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0
|
||||
self.navigationBar?.alpha = alpha
|
||||
self.statusBar?.updateAlpha(alpha, transition: .immediate)
|
||||
self.footerNode.alpha = alpha
|
||||
self.footerNode.setVisibilityAlpha(alpha)
|
||||
self.updateThumbnailContainerNodeAlpha(.immediate)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,23 @@ import Foundation
|
|||
|
||||
public final class GalleryControllerPresentationState {
|
||||
public let footerContentNode: GalleryFooterContentNode?
|
||||
public let overlayContentNode: GalleryOverlayContentNode?
|
||||
|
||||
public init() {
|
||||
self.footerContentNode = nil
|
||||
self.overlayContentNode = nil
|
||||
}
|
||||
|
||||
public init(footerContentNode: GalleryFooterContentNode?) {
|
||||
public init(footerContentNode: GalleryFooterContentNode?, overlayContentNode: GalleryOverlayContentNode?) {
|
||||
self.footerContentNode = footerContentNode
|
||||
self.overlayContentNode = overlayContentNode
|
||||
}
|
||||
|
||||
public func withUpdatedFooterContentNode(_ footerContentNode: GalleryFooterContentNode?) -> GalleryControllerPresentationState {
|
||||
return GalleryControllerPresentationState(footerContentNode: footerContentNode)
|
||||
return GalleryControllerPresentationState(footerContentNode: footerContentNode, overlayContentNode: self.overlayContentNode)
|
||||
}
|
||||
|
||||
public func withUpdatedOverlayContentNode(_ overlayContentNode: GalleryOverlayContentNode?) -> GalleryControllerPresentationState {
|
||||
return GalleryControllerPresentationState(footerContentNode: self.footerContentNode, overlayContentNode: overlayContentNode)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,3 +31,20 @@ open class GalleryFooterContentNode: ASDisplayNode {
|
|||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
open class GalleryOverlayContentNode: ASDisplayNode {
|
||||
var visibilityAlpha: CGFloat = 1.0
|
||||
open func setVisibilityAlpha(_ alpha: CGFloat) {
|
||||
self.visibilityAlpha = alpha
|
||||
}
|
||||
|
||||
open func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
open func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
open func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ public final class GalleryFooterNode: ASDisplayNode {
|
|||
private let backgroundNode: ASDisplayNode
|
||||
|
||||
private var currentFooterContentNode: GalleryFooterContentNode?
|
||||
private var currentOverlayContentNode: GalleryOverlayContentNode?
|
||||
private var currentLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let controllerInteraction: GalleryControllerInteraction
|
||||
|
|
@ -22,38 +23,59 @@ public final class GalleryFooterNode: ASDisplayNode {
|
|||
self.addSubnode(self.backgroundNode)
|
||||
}
|
||||
|
||||
public func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, thumbnailPanelHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
private var visibilityAlpha: CGFloat = 1.0
|
||||
public func setVisibilityAlpha(_ alpha: CGFloat) {
|
||||
self.visibilityAlpha = alpha
|
||||
self.backgroundNode.alpha = alpha
|
||||
self.currentFooterContentNode?.alpha = alpha
|
||||
self.currentOverlayContentNode?.setVisibilityAlpha(alpha)
|
||||
}
|
||||
|
||||
public func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, overlayContentNode: GalleryOverlayContentNode?, thumbnailPanelHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = (layout, thumbnailPanelHeight)
|
||||
let cleanInsets = layout.insets(options: [])
|
||||
|
||||
var removeCurrentFooterContentNode: GalleryFooterContentNode?
|
||||
var dismissedCurrentFooterContentNode: GalleryFooterContentNode?
|
||||
if self.currentFooterContentNode !== footerContentNode {
|
||||
if let currentFooterContentNode = self.currentFooterContentNode {
|
||||
currentFooterContentNode.requestLayout = nil
|
||||
removeCurrentFooterContentNode = currentFooterContentNode
|
||||
dismissedCurrentFooterContentNode = currentFooterContentNode
|
||||
}
|
||||
self.currentFooterContentNode = footerContentNode
|
||||
if let footerContentNode = footerContentNode {
|
||||
footerContentNode.alpha = self.visibilityAlpha
|
||||
footerContentNode.controllerInteraction = self.controllerInteraction
|
||||
footerContentNode.requestLayout = { [weak self] transition in
|
||||
if let strongSelf = self, let (currentLayout, currentThumbnailPanelHeight) = strongSelf.currentLayout {
|
||||
strongSelf.updateLayout(currentLayout, footerContentNode: strongSelf.currentFooterContentNode, thumbnailPanelHeight: currentThumbnailPanelHeight, transition: transition)
|
||||
strongSelf.updateLayout(currentLayout, footerContentNode: strongSelf.currentFooterContentNode, overlayContentNode: strongSelf.currentOverlayContentNode, thumbnailPanelHeight: currentThumbnailPanelHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
self.addSubnode(footerContentNode)
|
||||
}
|
||||
}
|
||||
|
||||
var dismissedCurrentOverlayContentNode: GalleryOverlayContentNode?
|
||||
if self.currentOverlayContentNode !== overlayContentNode {
|
||||
if let currentOverlayContentNode = self.currentOverlayContentNode {
|
||||
dismissedCurrentOverlayContentNode = currentOverlayContentNode
|
||||
}
|
||||
self.currentOverlayContentNode = overlayContentNode
|
||||
if let overlayContentNode = overlayContentNode {
|
||||
overlayContentNode.setVisibilityAlpha(self.visibilityAlpha)
|
||||
self.addSubnode(overlayContentNode)
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundHeight: CGFloat = 0.0
|
||||
if let footerContentNode = self.currentFooterContentNode {
|
||||
backgroundHeight = footerContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, contentInset: thumbnailPanelHeight, transition: transition)
|
||||
transition.updateFrame(node: footerContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
if let removeCurrentFooterContentNode = removeCurrentFooterContentNode {
|
||||
if let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode {
|
||||
let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
footerContentNode.animateIn(fromHeight: removeCurrentFooterContentNode.bounds.height, previousContentNode: removeCurrentFooterContentNode, transition: contentTransition)
|
||||
removeCurrentFooterContentNode.animateOut(toHeight: backgroundHeight, nextContentNode: footerContentNode, transition: contentTransition, completion: { [weak self, weak removeCurrentFooterContentNode] in
|
||||
if let strongSelf = self, let removeCurrentFooterContentNode = removeCurrentFooterContentNode, removeCurrentFooterContentNode !== strongSelf.currentFooterContentNode {
|
||||
removeCurrentFooterContentNode.removeFromSupernode()
|
||||
footerContentNode.animateIn(fromHeight: dismissedCurrentFooterContentNode.bounds.height, previousContentNode: dismissedCurrentFooterContentNode, transition: contentTransition)
|
||||
dismissedCurrentFooterContentNode.animateOut(toHeight: backgroundHeight, nextContentNode: footerContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentFooterContentNode] in
|
||||
if let strongSelf = self, let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode, dismissedCurrentFooterContentNode !== strongSelf.currentFooterContentNode {
|
||||
dismissedCurrentFooterContentNode.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
contentTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
|
|
@ -61,15 +83,41 @@ public final class GalleryFooterNode: ASDisplayNode {
|
|||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
}
|
||||
} else {
|
||||
if let removeCurrentFooterContentNode = removeCurrentFooterContentNode {
|
||||
removeCurrentFooterContentNode.removeFromSupernode()
|
||||
if let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode {
|
||||
dismissedCurrentFooterContentNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
}
|
||||
|
||||
let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
if let overlayContentNode = self.currentOverlayContentNode {
|
||||
overlayContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: backgroundHeight, transition: transition)
|
||||
transition.updateFrame(node: overlayContentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
overlayContentNode.animateIn(previousContentNode: dismissedCurrentOverlayContentNode, transition: contentTransition)
|
||||
if let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode {
|
||||
dismissedCurrentOverlayContentNode.animateOut(nextContentNode: overlayContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentOverlayContentNode] in
|
||||
if let strongSelf = self, let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode, dismissedCurrentOverlayContentNode !== strongSelf.currentOverlayContentNode {
|
||||
dismissedCurrentOverlayContentNode.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode {
|
||||
dismissedCurrentOverlayContentNode.animateOut(nextContentNode: overlayContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentOverlayContentNode] in
|
||||
if let strongSelf = self, let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode, dismissedCurrentOverlayContentNode !== strongSelf.currentOverlayContentNode {
|
||||
dismissedCurrentOverlayContentNode.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let overlayResult = self.currentOverlayContentNode?.hitTest(point, with: event) {
|
||||
return overlayResult
|
||||
}
|
||||
if !self.backgroundNode.frame.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ open class GalleryItemNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public var toggleControlsVisibility: () -> Void = { }
|
||||
public var goToPreviousItem: () -> Void = { }
|
||||
public var goToNextItem: () -> Void = { }
|
||||
public var dismiss: () -> Void = { }
|
||||
public var beginCustomDismiss: () -> Void = { }
|
||||
public var completeCustomDismiss: () -> Void = { }
|
||||
|
|
@ -54,8 +56,8 @@ open class GalleryItemNode: ASDisplayNode {
|
|||
return .single(nil)
|
||||
}
|
||||
|
||||
open func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(nil)
|
||||
open func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((nil, nil))
|
||||
}
|
||||
|
||||
open func navigationStyle() -> Signal<GalleryItemNodeNavigationStyle, NoError> {
|
||||
|
|
|
|||
|
|
@ -241,6 +241,20 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
private func makeNodeForItem(at index: Int) -> GalleryItemNode {
|
||||
let node = self.items[index].node()
|
||||
node.toggleControlsVisibility = self.toggleControlsVisibility
|
||||
node.goToPreviousItem = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let index = strongSelf.centralItemIndex, index > 0 {
|
||||
strongSelf.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
node.goToNextItem = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let index = strongSelf.centralItemIndex, index < strongSelf.items.count - 1 {
|
||||
strongSelf.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
node.dismiss = self.dismiss
|
||||
node.beginCustomDismiss = self.beginCustomDismiss
|
||||
node.completeCustomDismiss = self.completeCustomDismiss
|
||||
|
|
@ -314,7 +328,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
if centralItemIndex != items.count - 1 {
|
||||
if centralItemIndex != self.items.count - 1 {
|
||||
if self.shouldLoadItems(force: forceLoad) && self.visibleItemNode(at: centralItemIndex + 1) == nil {
|
||||
let node = self.makeNodeForItem(at: centralItemIndex + 1)
|
||||
node.frame = centralItemNode.frame.offsetBy(dx: centralItemNode.frame.size.width + self.pageGap, dy: 0.0)
|
||||
|
|
|
|||
|
|
@ -325,8 +325,8 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return self._rightBarButtonItems.get()
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
|
||||
@objc func statusPressed() {
|
||||
|
|
|
|||
|
|
@ -375,8 +375,8 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
|
|||
self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
|
||||
@objc func statusPressed() {
|
||||
|
|
|
|||
|
|
@ -310,8 +310,8 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
|
|||
self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
|
||||
@objc func statusPressed() {
|
||||
|
|
|
|||
|
|
@ -505,8 +505,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return self._rightBarButtonItem.get()
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
|
||||
@objc func statusPressed() {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,87 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode
|
|||
}
|
||||
}
|
||||
|
||||
private let soundOnImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOn"), color: .white)
|
||||
private let soundOffImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOff"), color: .white)
|
||||
private var roundButtonBackgroundImage = {
|
||||
return generateImage(CGSize(width: 42.0, height: 42), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
|
||||
context.fillEllipse(in: bounds)
|
||||
})
|
||||
}()
|
||||
|
||||
private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode {
|
||||
private let soundButtonNode: HighlightableButtonNode
|
||||
private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat)?
|
||||
|
||||
override init() {
|
||||
self.soundButtonNode = HighlightableButtonNode()
|
||||
self.soundButtonNode.alpha = 0.0
|
||||
self.soundButtonNode.setBackgroundImage(roundButtonBackgroundImage, for: .normal)
|
||||
self.soundButtonNode.setImage(soundOffImage, for: .normal)
|
||||
self.soundButtonNode.setImage(soundOnImage, for: .selected)
|
||||
self.soundButtonNode.setImage(soundOnImage, for: [.selected, .highlighted])
|
||||
|
||||
super.init()
|
||||
|
||||
self.soundButtonNode.addTarget(self, action: #selector(self.soundButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.addSubnode(self.soundButtonNode)
|
||||
}
|
||||
|
||||
func hide() {
|
||||
self.soundButtonNode.isHidden = true
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, metrics, leftInset, rightInset, bottomInset)
|
||||
|
||||
let soundButtonDiameter: CGFloat = 42.0
|
||||
let inset: CGFloat = 12.0
|
||||
let effectiveBottomInset = self.visibilityAlpha < 1.0 ? 0.0 : bottomInset
|
||||
let soundButtonFrame = CGRect(origin: CGPoint(x: size.width - soundButtonDiameter - inset - rightInset, y: size.height - soundButtonDiameter - inset - effectiveBottomInset), size: CGSize(width: soundButtonDiameter, height: soundButtonDiameter))
|
||||
transition.updateFrame(node: self.soundButtonNode, frame: soundButtonFrame)
|
||||
}
|
||||
|
||||
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateAlpha(node: self.soundButtonNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
transition.updateAlpha(node: self.soundButtonNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
override func setVisibilityAlpha(_ alpha: CGFloat) {
|
||||
super.setVisibilityAlpha(alpha)
|
||||
self.updateSoundButtonVisibility()
|
||||
}
|
||||
|
||||
func updateSoundButtonVisibility() {
|
||||
if self.soundButtonNode.isSelected {
|
||||
self.soundButtonNode.alpha = self.visibilityAlpha
|
||||
} else {
|
||||
self.soundButtonNode.alpha = 1.0
|
||||
}
|
||||
|
||||
if let validLayout = self.validLayout {
|
||||
self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
|
||||
@objc func soundButtonPressed() {
|
||||
self.soundButtonNode.isSelected = !self.soundButtonNode.isSelected
|
||||
self.updateSoundButtonVisibility()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.soundButtonNode.frame.contains(point) {
|
||||
return nil
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: () -> Void
|
||||
let cancel: () -> Void
|
||||
|
|
@ -161,6 +242,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
private let scrubberView: ChatVideoGalleryItemScrubberView
|
||||
private let footerContentNode: ChatItemGalleryFooterContentNode
|
||||
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private var videoFramePreview: MediaPlayerFramePreview?
|
||||
|
|
@ -206,6 +288,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
self.footerContentNode.performAction = performAction
|
||||
self.footerContentNode.openActionOptions = openActionOptions
|
||||
|
||||
self.overlayContentNode = UniversalVideoGalleryItemOverlayNode()
|
||||
|
||||
self.statusButtonNode = HighlightableButtonNode()
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
|
||||
self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0))
|
||||
|
|
@ -398,6 +482,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
|
||||
if item.fromPlayingVideo {
|
||||
videoNode.canAttachContent = false
|
||||
self.overlayContentNode.hide()
|
||||
} else {
|
||||
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
|
||||
}
|
||||
|
|
@ -982,7 +1067,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
transformedSuperFrame = transformedSuperFrame.offsetBy(dx: videoNode.position.x - previousFrame.center.x, dy: videoNode.position.y - previousFrame.center.y)
|
||||
}
|
||||
|
||||
let initialScale: CGFloat = 1.0 //min(videoNode.layer.bounds.width / node.0.view.bounds.width, videoNode.layer.bounds.height / node.0.view.bounds.height)
|
||||
let initialScale: CGFloat = 1.0
|
||||
let targetScale = max(transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height)
|
||||
|
||||
videoNode.backgroundColor = .clear
|
||||
|
|
@ -1197,7 +1282,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, self.overlayContentNode))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,36 @@ import UIKit
|
|||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.5).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor] as CFArray
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 64.0, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})
|
||||
|
||||
private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.5).cgColor] as CFArray
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 64.0, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})
|
||||
|
||||
open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate {
|
||||
public let scrollNode: ASScrollNode
|
||||
private let leftFadeNode: ASImageNode
|
||||
private let rightFadeNode: ASImageNode
|
||||
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
|
||||
|
|
@ -32,6 +60,16 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate
|
|||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.leftFadeNode = ASImageNode()
|
||||
self.leftFadeNode.contentMode = .scaleToFill
|
||||
self.leftFadeNode.image = leftFadeImage
|
||||
self.leftFadeNode.isHidden = true
|
||||
|
||||
self.rightFadeNode = ASImageNode()
|
||||
self.rightFadeNode.contentMode = .scaleToFill
|
||||
self.rightFadeNode.image = rightFadeImage
|
||||
self.rightFadeNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.delegate = self
|
||||
|
|
@ -42,41 +80,69 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate
|
|||
self.scrollNode.view.delaysContentTouches = false
|
||||
|
||||
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.contentTap(_:)))
|
||||
tapRecognizer.tapActionAtPoint = { _ in
|
||||
tapRecognizer.tapActionAtPoint = { [weak self] location in
|
||||
if let strongSelf = self {
|
||||
if location.x < 44.0 || location.x > strongSelf.frame.width - 44.0 {
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
return .waitForDoubleTap
|
||||
}
|
||||
tapRecognizer.highlight = { [weak self] location in
|
||||
if let strongSelf = self {
|
||||
if let location = location, location.x < 44.0 {
|
||||
strongSelf.leftFadeNode.isHidden = false
|
||||
} else {
|
||||
strongSelf.leftFadeNode.isHidden = true
|
||||
}
|
||||
if let location = location, location.x > strongSelf.frame.width - 44.0 {
|
||||
strongSelf.rightFadeNode.isHidden = false
|
||||
} else {
|
||||
strongSelf.rightFadeNode.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.scrollNode.view.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
self.addSubnode(self.leftFadeNode)
|
||||
self.addSubnode(self.rightFadeNode)
|
||||
}
|
||||
|
||||
@objc open func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
if recognizer.state == .ended {
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
self.toggleControlsVisibility()
|
||||
case .doubleTap:
|
||||
if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) {
|
||||
let pointInView = self.scrollNode.view.convert(location, to: contentView)
|
||||
|
||||
let newZoomScale = self.scrollNode.view.maximumZoomScale
|
||||
let scrollViewSize = self.scrollNode.view.bounds.size
|
||||
|
||||
let w = scrollViewSize.width / newZoomScale
|
||||
let h = scrollViewSize.height / newZoomScale
|
||||
let x = pointInView.x - (w / 2.0)
|
||||
let y = pointInView.y - (h / 2.0)
|
||||
|
||||
let rectToZoomTo = CGRect(x: x, y: y, width: w, height: h)
|
||||
|
||||
self.scrollNode.view.zoom(to: rectToZoomTo, animated: true)
|
||||
} else {
|
||||
self.scrollNode.view.setZoomScale(self.scrollNode.view.minimumZoomScale, animated: true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
if location.x < 44.0 {
|
||||
self.goToPreviousItem()
|
||||
} else if location.x > self.frame.width - 44.0 {
|
||||
self.goToNextItem()
|
||||
} else {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
self.toggleControlsVisibility()
|
||||
case .doubleTap:
|
||||
if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) {
|
||||
let pointInView = self.scrollNode.view.convert(location, to: contentView)
|
||||
|
||||
let newZoomScale = self.scrollNode.view.maximumZoomScale
|
||||
let scrollViewSize = self.scrollNode.view.bounds.size
|
||||
|
||||
let w = scrollViewSize.width / newZoomScale
|
||||
let h = scrollViewSize.height / newZoomScale
|
||||
let x = pointInView.x - (w / 2.0)
|
||||
let y = pointInView.y - (h / 2.0)
|
||||
|
||||
let rectToZoomTo = CGRect(x: x, y: y, width: w, height: h)
|
||||
|
||||
self.scrollNode.view.zoom(to: rectToZoomTo, animated: true)
|
||||
} else {
|
||||
self.scrollNode.view.setZoomScale(self.scrollNode.view.minimumZoomScale, animated: true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +159,9 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate
|
|||
}
|
||||
self.containerLayout = layout
|
||||
|
||||
self.leftFadeNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width * 0.2, height: layout.size.height)
|
||||
self.rightFadeNode.frame = CGRect(x: layout.size.width - layout.size.width * 0.2, y: 0.0, width: layout.size.width * 0.2, height: layout.size.height)
|
||||
|
||||
if shouldResetContents {
|
||||
var previousFrame: CGRect?
|
||||
var previousScale: CGFloat?
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return self._title.get()
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
|
|||
private let centralItemTitleView = Promise<UIView?>()
|
||||
private let centralItemRightBarButtonItem = Promise<UIBarButtonItem?>()
|
||||
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
|
||||
private let centralItemFooterContentNode = Promise<GalleryFooterContentNode?>()
|
||||
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
|
||||
private let centralItemAttributesDisposable = DisposableSet();
|
||||
|
||||
private let _hiddenMedia = Promise<InstantPageGalleryEntry?>(nil)
|
||||
|
|
@ -243,7 +243,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
|
|||
self?.navigationItem.rightBarButtonItem = rightBarButtonItem
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in
|
||||
self?.galleryNode.updatePresentationState({
|
||||
$0.withUpdatedFooterContentNode(footerContentNode)
|
||||
}, transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ public enum ItemListPeerActionItemHeight {
|
|||
case peerList
|
||||
}
|
||||
|
||||
public enum ItemListPeerActionItemColor {
|
||||
case accent
|
||||
case destructive
|
||||
}
|
||||
|
||||
public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let icon: UIImage?
|
||||
|
|
@ -19,16 +24,18 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
|||
public let alwaysPlain: Bool
|
||||
let editing: Bool
|
||||
let height: ItemListPeerActionItemHeight
|
||||
let color: ItemListPeerActionItemColor
|
||||
public let sectionId: ItemListSectionId
|
||||
let action: (() -> Void)?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, editing: Bool, action: (() -> Void)?) {
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool, action: (() -> Void)?) {
|
||||
self.presentationData = presentationData
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.alwaysPlain = alwaysPlain
|
||||
self.editing = editing
|
||||
self.height = height
|
||||
self.color = color
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
}
|
||||
|
|
@ -167,7 +174,15 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
|||
|
||||
let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let textColor: UIColor
|
||||
switch item.color {
|
||||
case .accent:
|
||||
textColor = item.presentationData.theme.list.itemAccentColor
|
||||
case .destructive:
|
||||
textColor = item.presentationData.theme.list.itemDestructiveColor
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public enum ItemListStickerPackItemControl: Equatable {
|
|||
case none
|
||||
case installation(installed: Bool)
|
||||
case selection
|
||||
case check(checked: Bool)
|
||||
}
|
||||
|
||||
public final class ItemListStickerPackItem: ListViewItem, ItemListItem {
|
||||
|
|
@ -305,6 +306,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
case .selection:
|
||||
rightInset += 16.0
|
||||
checkImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme)
|
||||
case .check:
|
||||
rightInset += 16.0
|
||||
}
|
||||
|
||||
var unreadImage: UIImage?
|
||||
|
|
@ -531,6 +534,10 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.selectionIconNode.image = image
|
||||
strongSelf.selectionIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
case let .check(checked):
|
||||
strongSelf.installationActionNode.isHidden = true
|
||||
strongSelf.installationActionImageNode.isHidden = true
|
||||
strongSelf.selectionIconNode.isHidden = true
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo
|
|||
private let centralItemTitle = Promise<String>()
|
||||
private let centralItemTitleView = Promise<UIView?>()
|
||||
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
|
||||
private let centralItemFooterContentNode = Promise<GalleryFooterContentNode?>()
|
||||
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
|
||||
private let centralItemAttributesDisposable = DisposableSet();
|
||||
|
||||
private let _hiddenMedia = Promise<SecureIdDocumentGalleryEntry?>(nil)
|
||||
|
|
@ -123,7 +123,7 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo
|
|||
self?.navigationItem.titleView = titleView
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in
|
||||
self?.galleryNode.updatePresentationState({
|
||||
$0.withUpdatedFooterContentNode(footerContentNode)
|
||||
}, transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -210,8 +210,8 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
return self._title.get()
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||
private let centralItemTitle = Promise<String>()
|
||||
private let centralItemTitleView = Promise<UIView?>()
|
||||
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
|
||||
private let centralItemFooterContentNode = Promise<GalleryFooterContentNode?>()
|
||||
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
|
||||
private let centralItemAttributesDisposable = DisposableSet();
|
||||
|
||||
private let _hiddenMedia = Promise<AvatarGalleryEntry?>(nil)
|
||||
|
|
@ -263,7 +263,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||
self?.navigationItem.titleView = titleView
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in
|
||||
self?.galleryNode.updatePresentationState({
|
||||
$0.withUpdatedFooterContentNode(footerContentNode)
|
||||
}, transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ static_library(
|
|||
"//submodules/TelegramIntents:TelegramIntents",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
|
||||
"//submodules/StatisticsUI:StatisticsUI",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ static_library(
|
|||
"//submodules/PeersNearbyIconNode:PeersNearbyIconNode",
|
||||
"//submodules/Geocoding:Geocoding",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue