TradeBarChartView 안의 UIStackView를 넣고, UIStackView안에 TradeBarView를 여러개 넣는 구조입니다.
TradeBarView는 BarView를 UIStackView안에 두개를 가지고 있는 형태입니다.
막대차트의 핵심은 높이 값이고 이를 처리하는 방식을 정리해보겠습니다.
struct Chart {
var value: Double = 0.0
var color: UIColor = Const.Color.orange
}
class BarView: UIView {
var topLabel = UILabel().then {
$0.font = Const.Font.caption4
$0.textAlignment = .center
}
var contentBackgroundView = UIView()
var contentView = UIView().then {
$0.layer.cornerRadius = 5
}
var chart: Chart!
var maxValue: Double!
required init?(coder: NSCoder) {
super.init(coder: coder)
setView()
}
init(chart: Chart, maxValue: Double) {
super.init(frame: .zero)
self.chart = chart
self.maxValue = maxValue
setView()
}
func setView() {
addSubview(contentBackgroundView)
addSubview(topLabel)
addSubview(contentView)
contentBackgroundView.translatesAutoresizingMaskIntoConstraints = false
topLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
if chart.value == 0.0 {
contentView.backgroundColor = Const.Color.systemGray4
chart.value = 0.05
maxValue = 1
} else {
contentView.backgroundColor = chart.color
topLabel.text = String((ceil((chart.value / 10000.0) * Double(10))) / Double(10))
}
NSLayoutConstraint.activate([
contentBackgroundView.topAnchor.constraint(equalTo: topAnchor),
contentBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: contentBackgroundView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: contentBackgroundView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentBackgroundView.bottomAnchor),
contentView.heightAnchor.constraint(equalTo: contentBackgroundView.heightAnchor, multiplier: chart.value / maxValue),
topLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
topLabel.bottomAnchor.constraint(equalTo: contentView.topAnchor),
])
}
}
contentView.heightAnchor.constraint(equalTo: contentBackgroundView.heightAnchor, multiplier: chart.value / maxValue) 이부분이 핵심인데, 차트 값의 최대 크기를 알아내고 그것에 맞추어 비율을 설정해 주었습니다.
struct TradeChartZip {
var tradeCharts: [TradeChart] = []
var label: String = ""
func getMaxValue() -> Double {
return max(self.tradeCharts.map { $0.buyChart.value }.max() ?? 0.0, self.tradeCharts.map { $0.sellChart.value }.max() ?? 0.0)
}
}
class TradeBarView: UIView {
var topLabel = UILabel().then {
$0.font = Const.Font.caption5
$0.textColor = Const.Color.black
$0.textAlignment = .center
}
var bottomLabelBackgroundView = UIView().then {
$0.backgroundColor = .systemGray6
}
var bottomLabel = UILabel().then {
$0.font = Const.Font.itemFootnote
$0.textColor = Const.Color.black
$0.textAlignment = .center
}
var barStackView = UIStackView().then {
$0.alignment = .center
$0.distribution = .fillEqually
$0.spacing = 2
}
var buyBarViews: [BarView] = []
var sellBarViews: [BarView] = []
var buyBarViewWidthConstraints: [NSLayoutConstraint] = []
var sellBarViewWidthConstraints: [NSLayoutConstraint] = []
var tradeChartZip: TradeChartZip!
var maxValue: Double!
required init?(coder: NSCoder) {
super.init(coder: coder)
}
init(tradeChartZip: TradeChartZip, maxValue: Double) {
super.init(frame: .zero)
self.tradeChartZip = tradeChartZip
self.maxValue = maxValue
setView()
}
func setView() {
addSubview(topLabel)
addSubview(barStackView)
addSubview(bottomLabelBackgroundView)
addSubview(bottomLabel)
topLabel.translatesAutoresizingMaskIntoConstraints = false
barStackView.translatesAutoresizingMaskIntoConstraints = false
bottomLabelBackgroundView.translatesAutoresizingMaskIntoConstraints = false
bottomLabel.translatesAutoresizingMaskIntoConstraints = false
bottomLabel.text = tradeChartZip.label
for tradeChart in tradeChartZip.tradeCharts {
buyBarViews.append(BarView(chart: tradeChart.buyChart, maxValue: maxValue))
sellBarViews.append(BarView(chart: tradeChart.sellChart, maxValue: maxValue))
}
NSLayoutConstraint.activate([
topLabel.topAnchor.constraint(equalTo: topAnchor),
topLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
topLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
topLabel.heightAnchor.constraint(equalToConstant: 30),
barStackView.topAnchor.constraint(equalTo: topLabel.bottomAnchor),
barStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
barStackView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.7),
bottomLabel.topAnchor.constraint(equalTo: barStackView.bottomAnchor, constant: 2),
bottomLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
bottomLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
bottomLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
bottomLabel.heightAnchor.constraint(equalToConstant: 20),
])
for i in 0..<tradeChartZip.tradeCharts.count {
barStackView.addArrangedSubview(buyBarViews[i])
barStackView.addArrangedSubview(sellBarViews[i])
buyBarViews[i].translatesAutoresizingMaskIntoConstraints = false
sellBarViews[i].translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buyBarViews[i].topAnchor.constraint(equalTo: barStackView.topAnchor),
buyBarViews[i].bottomAnchor.constraint(equalTo: barStackView.bottomAnchor),
sellBarViews[i].topAnchor.constraint(equalTo: barStackView.topAnchor),
sellBarViews[i].bottomAnchor.constraint(equalTo: barStackView.bottomAnchor),
])
}
}
}
extension TradeBarView {
func showAllBarView() {
for i in 0..<tradeChartZip.tradeCharts.count {
buyBarViews[i].isHidden = false
sellBarViews[i].isHidden = false
}
UIView.animate(withDuration: 0.25, animations: {
self.layoutIfNeeded()
})
}
func showBuyBarView() {
for i in 0..<tradeChartZip.tradeCharts.count {
buyBarViews[i].isHidden = false
sellBarViews[i].isHidden = true
}
UIView.animate(withDuration: 0.25, animations: {
self.layoutIfNeeded()
})
}
func showSellBarView() {
for i in 0..<tradeChartZip.tradeCharts.count {
buyBarViews[i].isHidden = true
sellBarViews[i].isHidden = false
}
UIView.animate(withDuration: 0.25, animations: {
self.layoutIfNeeded()
})
}
}
class TradeBarChartView: UIView {
var titleLabel = UILabel().then {
$0.text = "누적 투자 내역"
$0.font = Const.Font.headline
$0.textColor = Const.Color.black
}
var typeSegmentControl = UISegmentedControl(items: ["전체", "매수", "매도"])
var periodSegmentControl = UISegmentedControl(items: ["일", "주", "월"])
var priceUnitLabel = UILabel().then {
$0.text = "단위: 만원"
$0.font = Const.Font.itemFootnote
$0.textColor = Const.Color.black
}
var tradeBarViews: [TradeBarView] = []
var tradeChartZips: [TradeChartZip]!
var typeOption = 0
var periodOption = 0
required init?(coder: NSCoder) {
super.init(coder: coder)
setView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setView()
}
func setView() {
addSubview(titleLabel)
addSubview(typeSegmentControl)
addSubview(periodSegmentControl)
addSubview(priceUnitLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
typeSegmentControl.translatesAutoresizingMaskIntoConstraints = false
periodSegmentControl.translatesAutoresizingMaskIntoConstraints = false
priceUnitLabel.translatesAutoresizingMaskIntoConstraints = false
typeSegmentControl.selectedSegmentIndex = 0
periodSegmentControl.selectedSegmentIndex = 0
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
typeSegmentControl.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
typeSegmentControl.leadingAnchor.constraint(equalTo: leadingAnchor),
typeSegmentControl.widthAnchor.constraint(equalToConstant: 130),
periodSegmentControl.topAnchor.constraint(equalTo: typeSegmentControl.topAnchor),
periodSegmentControl.trailingAnchor.constraint(equalTo: trailingAnchor),
periodSegmentControl.widthAnchor.constraint(equalToConstant: 100),
priceUnitLabel.topAnchor.constraint(equalTo: typeSegmentControl.bottomAnchor, constant: 10),
priceUnitLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
])
}
}
extension TradeBarChartView {
func setTradeChartZips(tradeChartZips: [TradeChartZip]) {
self.tradeChartZips = tradeChartZips
updateTradeChartZips(tradeChartZips: self.tradeChartZips)
}
func setTypeOption(option: Int) {
if self.typeOption == option { return }
self.typeOption = option
updateTypeOption(option: self.typeOption)
}
func setPeriodOption(option: Int) {
if self.periodOption == option { return }
self.periodOption = option
updatePeriodOption(option: self.periodOption)
}
func updateTradeChartZips(tradeChartZips: [TradeChartZip]) {
refresh()
let maxValue = tradeChartZips.map { $0.getMaxValue() }.max() ?? 1.0
for tradeChartZip in tradeChartZips {
tradeBarViews.append(TradeBarView(tradeChartZip: tradeChartZip, maxValue: maxValue))
}
for (i, tradeBarView) in tradeBarViews.enumerated() {
addSubview(tradeBarView)
tradeBarView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tradeBarView.topAnchor.constraint(equalTo: priceUnitLabel.bottomAnchor, constant: 10),
tradeBarView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: CGFloat(1) / CGFloat(7)),
tradeBarView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
if tradeBarView == tradeBarViews.first {
NSLayoutConstraint.activate([
tradeBarView.leadingAnchor.constraint(equalTo: leadingAnchor),
])
} else if tradeBarView == tradeBarViews.last {
NSLayoutConstraint.activate([
tradeBarView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
} else {
NSLayoutConstraint.activate([
tradeBarView.leadingAnchor.constraint(equalTo: tradeBarViews[i-1].trailingAnchor),
])
}
}
}
func updateTypeOption(option: Int) {
if option == 0 {
tradeBarViews.forEach({ $0.showAllBarView() })
} else if option == 1 {
tradeBarViews.forEach({ $0.showBuyBarView() })
} else {
tradeBarViews.forEach({ $0.showSellBarView() })
}
}
func updatePeriodOption(option: Int) {
}
func refresh() {
for view in tradeBarViews {
view.removeFromSuperview()
}
tradeBarViews.removeAll()
}
}
현재는 7개로 고정해 놓고 사용하였습니다.
단순 UIView를 for문으로 생성하고 UIStackView에 넣는 과정을 반복하였습니다. 핵심은 높이값이 동적으로 바뀌어야 하는 것이고 부모의 100% 높이를 가져와서 multipler로 0 ~ 1 사이의 값을 넣어 부모의 높이에 따라 최대 높이가 결정되도록 하였습니다.