iOS ) 커스텀 막대 차트 구현 하기

영모·2022년 5월 10일
0
post-thumbnail

UI 미리보기

💻 개발 정리

# 0 구현 아이디어

TradeBarChartView 안의 UIStackView를 넣고, UIStackView안에 TradeBarView를 여러개 넣는 구조입니다.
TradeBarView는 BarView를 UIStackView안에 두개를 가지고 있는 형태입니다.

막대차트의 핵심은 높이 값이고 이를 처리하는 방식을 정리해보겠습니다.

# 1 BarView 제작

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) 이부분이 핵심인데, 차트 값의 최대 크기를 알아내고 그것에 맞추어 비율을 설정해 주었습니다.

# 2 BarView를 N개 넣은 TradeBarView 제작

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()
        })
    }
}

# 3 TradeBarView를 N개 넣은 TradeBarChartView 제작


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개로 고정해 놓고 사용하였습니다.

# 4 정리 하기

단순 UIView를 for문으로 생성하고 UIStackView에 넣는 과정을 반복하였습니다. 핵심은 높이값이 동적으로 바뀌어야 하는 것이고 부모의 100% 높이를 가져와서 multipler로 0 ~ 1 사이의 값을 넣어 부모의 높이에 따라 최대 높이가 결정되도록 하였습니다.

profile
iOS Developer

0개의 댓글