// // ChartVisibilityView.swift // GraphTest // // Created by Andrei Salavei on 4/13/19. // Copyright © 2019 Andrei Salavei. All rights reserved. // import UIKit import GraphCore import Display private enum Constants { static let itemHeight: CGFloat = 30 static let itemSpacing: CGFloat = 8 static let labelTextApproxInsets: CGFloat = 40 static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16) } public func calculateVisiblityHeight(width: CGFloat, items: [ChartVisibilityItem]) -> CGFloat { let frames = generateItemsFrames(frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)), items: items) guard let lastFrame = frames.last else { return .zero } return lastFrame.maxY + Constants.insets.bottom } private func generateItemsFrames(frame: CGRect, items: [ChartVisibilityItem]) -> [CGRect] { var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top) var frames: [CGRect] = [] for item in items { let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont]) let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up) if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) { frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight))) } else if previousPoint.x <= Constants.insets.left { frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight))) } else { previousPoint.y += Constants.itemHeight + Constants.itemSpacing previousPoint.x = Constants.insets.left frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight))) } previousPoint.x += width + Constants.itemSpacing } return frames } class ChartVisibilityView: UIView { var items: [ChartVisibilityItem] = [] { didSet { selectedItems = items.map { _ in true } while selectionViews.count > selectedItems.count { selectionViews.last?.removeFromSuperview() selectionViews.removeLast() } while selectionViews.count < selectedItems.count { let view = ChartVisibilityItemView(frame: bounds) addSubview(view) selectionViews.append(view) } for (index, item) in items.enumerated() { let view = selectionViews[index] view.item = item view.tapClosure = { [weak self, weak view] in guard let self = self else { return } let selected = !self.selectedItems[index] let selectedItemsCount = self.selectedItems.filter { $0 }.count if selectedItemsCount == 1 && !selected { view?.layer.addShakeAnimation() } else { self.setItemSelected(selected, at: index, animated: true) self.notifyItemSelection() } } view.longTapClosure = { [weak self] in guard let self = self else { return } let hasSelectedItem = self.selectedItems.enumerated().contains(where: { $0.element && $0.offset != index }) if hasSelectedItem { for (itemIndex, _) in self.items.enumerated() { self.setItemSelected(itemIndex == index, at: itemIndex, animated: true) } } else { for (itemIndex, _) in self.items.enumerated() { self.setItemSelected(true, at: itemIndex, animated: true) } } self.notifyItemSelection() } } } } private(set) var selectedItems: [Bool] = [] var isExpanded: Bool = true { didSet { invalidateIntrinsicContentSize() setNeedsUpdateConstraints() } } private var selectionViews: [ChartVisibilityItemView] = [] var selectionCallbackClosure: (([Bool]) -> Void)? func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) { self.selectedItems[index] = selected self.selectionViews[index].setChecked(isChecked: selected, animated: animated) } func setItemsSelection(_ selection: [Bool]) { assert(selection.count == items.count) self.selectedItems = selection for (index, selected) in self.selectedItems.enumerated() { selectionViews[index].setChecked(isChecked: selected, animated: false) } } private func notifyItemSelection() { selectionCallbackClosure?(selectedItems) } override func layoutSubviews() { super.layoutSubviews() updateFrames() } private func updateFrames() { for (index, frame) in generateItemsFrames(frame: bounds, items: self.items).enumerated() { selectionViews[index].frame = frame } } override var intrinsicContentSize: CGSize { guard isExpanded else { var size = self.bounds.size size.height = 0 return size } let frames = generateItemsFrames(frame: UIScreen.main.bounds, items: self.items) guard let lastFrame = frames.last else { return .zero } let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom) return size } } extension ChartVisibilityView: ChartThemeContainer { func apply(theme: ChartTheme, strings: ChartStrings, animated: Bool) { UIView.perform(animated: animated) { self.backgroundColor = theme.chartBackgroundColor } } }