iyOmSd/Title: SwiftUI

[SwiftUI] Charts 실전편 (feat. iOS16+ apple framework)

냄수 2023. 7. 29. 19:21
반응형

2023.06.29 - [iyOmSd/Title: SwiftUI] - [SwiftUI] Charts 이론편 (feat. iOS16+ apple framework)

 

[SwiftUI] Charts 이론편 (feat. iOS16+ apple framework)

슬슬 미니멈버전이 16이 되는 시대가 다가오고 있는 만큼 iOS16 부터 사용할 수 있는 내장 프레임워크인 Charts 를 알아보려고 합니다! 간단하게 이론적인 부분위주로 어떻게 쓰이고 사용할 수 있는

nsios.tistory.com

을 통해서 기본적인 사용법을 익혔으니 이젠 직접 구현해보면서

다양한 케이스를 확인하려고해요!

기본적인것들만 알아볼게요

 

BarMark

간단한 BarChart부터 뜯어볼까요

struct Case1: Identifiable {
    let name: String
    let age: Int
    
    var id: String { name }
}

struct NSBarChart: View {
    let case1: [Case1] = [
        Case1(name: "김씨", age: 10),
        Case1(name: "이씨", age: 20),
        Case1(name: "박씨", age: 30),
        Case1(name: "정씨", age: 40),
        Case1(name: "남씨", age: 45),
    ]
    
    var body: some View {
        Chart(case1) {
            BarMark(
                x: .value("name", $0.name),
                y: .value("age", $0.age),
                width: .automatic,
                height: .automatic,
                stacking: .center
            )
        }
    }
}

Chart타입에 배열을 넣을 수도있고 안넣을 수도 있어요

넣으면 각 데이터를 바로 사용해서 Mark를 그릴 수 있고

배열을 안넣어준다면 ForEach를 사용해서 반복되는 작업으로 Mark를 그려줘야해요

 

 

stacking이라는 프로퍼티를 이용해서 차트의 정렬을 바꿀수 있어요

struct NSBarChart: View {
    enum Stacking {
        case center // center offset 사용(가운데를 기준으로 위아래로 표시)
        case normalized // 누적막대형
        case standard // 0에서 시작
        case unstacked // 스택사용안함
    }
    @State private var stacking: Stacking = .center
    
    ...
    
    // 변환해주는 역할 Stacking -> MarkStackingMethod
    var stackingMethod: MarkStackingMethod { ... }
    
    var body: some View {
        VStack {
            Picker("stacking", selection: $stacking) { ... }
            .pickerStyle(.segmented)
            Chart(case1) {
                BarMark(
                    x: .value("name", $0.name),
                    y: .value("age", $0.age),
                    width: .automatic,
                    height: .automatic,
                    stacking: stackingMethod
                )
                BarMark(
                    x: .value("name", $0.name),
                    y: .value("age", $0.age),
                    width: .automatic,
                    height: .automatic,
                    stacking: stackingMethod
                )
                .foregroundStyle(.brown)
            }
        }
    }
}

같은데이터의 차트를 구분하기위해 색만 변경했어요

위에 주석으로 써있듯 

center - center offset을 사용하여 중앙부터 시작하는 차트를 그림 

normalized - 차트영역을 모두 사용하여 차트를 그림

standard - 0부터 시작하는 차트를 그림

unstacked - 스택을 사용하지않음(겹쳐져서 처음에그린 차트가 가려져요)

 

를 고려해서 그리면 좋을 것같아요

 

다음으론 

chartYScale() 을 사용해볼게요

Chart {
}
.chartYScale(range: 50...200) // 차트 그려지는영역의 위치 변경
.chartYScale(domain: 50...200) // 차트 영역에서 값 범위만 변경

좌: chartYScale(range: ), 우: chartYScale(domain: )

range매개변수는 차트가 그려지는 영역에 대한 위치를 변경하는 함수이고

domain매개변수는 차트의 최대,최솟값을 변경하는 함수에요

어떻게 구성하느냐에 따라 다양하게 그려볼 수 있을 것같네요

 

 

단순모델이아니라 차트데이터처럼 사용하고 비교를 하기위해서 모델을 아래와같이 변경했습니다 

struct Case2 {
    let date: Int
    let value: Int
    
    static var dummy = [
        (
            name: "김씨",
            data: [
                Case2(date: 1, value: 1),
                Case2(date: 2, value: 10),
                Case2(date: 3, value: 100),
                Case2(date: 4, value: 50),
                Case2(date: 5, value: 120)
            ]
        ),
        (
            name: "이씨",
            data: [
                Case2(date: 1, value: 3),
                Case2(date: 2, value: 30),
                Case2(date: 3, value: 300),
                Case2(date: 4, value: 30),
                Case2(date: 5, value: 130)
            ]
        )
    ]
}

범주는 name으로 `김씨`와 `이씨`를 사용하고

data로 date와 value가 있습니다

각날에 해당하는 매출? 이라고 생각하면 편할거같아요

 

차트색을 구분하기위해서 위에서는 .foregroundStyle를 Mark에 직접 지정하는 식이였어요

하지만 데이터가 많아지거나 범주별로 색을 주길 원한다면 하나하나 지정해주는게 어려울 수도 있어요

 

Chart(Case2.dummy, id: \.name) { element in
    ForEach(element.data, id: \.date) {
        BarMark(
            x: .value("date", $0.date),
            y: .value("value", $0.value),
            width: 20,
            height: .automatic,
            stacking: stackingMethod
        )
    }
    .foregroundStyle(by: .value("name", element.name))
}
.chartForegroundStyleScale([
    "김씨": .brown,
    "이씨": .purple
])

.chartForegroundStyleScale([
    "김씨": .brown,
    "이씨": .purple
])

두개의 이미지차이는 위의 코드 차이에요

왼쪽은 .foregroundStyle(by: .value("name", element.name)) 만적용했고

오른쪽은 chartForegroundStyleScale 까지 적용해준 차트에요

 

 

foregroundStyle(by:)를 이용해서 색을 구분하는 기준 값을 정해줘요

foregroundStyle만 사용하면 디폴트로 시스템에서 자동으로 색을 구분해줘요

 

범주색을 커스텀하고싶다면 chartForegroundStyleScale를 이용해서 foregroundStyle에서 기준으로 준 값들에 대한 색을 지정할 수 있어요

저같은경우는 name 즉 김씨, 이씨에대한 색을 구분했기때문에

두개의 범주색을 지정할 수 있어요

 

 

chartLegend

를 이용해서 밑에 범주표시를 숨길수 있어요

좌: .chartLegend(.hidden), 우:  .chartLegend(.visible)

 

BarMark말고 다른차트도 보고싶다..!

변경하는 방법은 아주 쉽습니다

나름 호환성좋게 디자인 돼서 나온것같아요

 

같은데이터를 이용해서

LineMark, PointMark, AreaMark, RuleMark, RectangleMark, SectorMark

다양한 차트를 그려봤어요

struct NSLineChart: View {
    let case2: [ChartData]
    
    var body: some View {
        Chart(case2, id: \.name) { element in
            ForEach(element.data, id: \.date) {
                LineMark(
                    x: .value("date", $0.date),
                    y: .value("value", $0.value)
                )
            }
            .foregroundStyle(by: .value("name", element.name))
        }
    }
}

struct NSPointChart: View {
    let case2: [ChartData]
    
    var body: some View {
        Chart(case2, id: \.name) { element in
            ForEach(element.data, id: \.date) {
                PointMark(
                    x: .value("date", $0.date),
                    y: .value("value", $0.value)
                )
            }
            .foregroundStyle(by: .value("name", element.name))
        }
    }
}

struct NSAreaChart: View {
    enum Stacking {
        case center // center offset 사용(가운데를 기준으로 위아래로 표시)
        case normalized // 누적막대형
        case standard // 0에서 시작
        case unstacked // 스택사용안함
    }
    @State private var stacking: Stacking = .center
    var stackingMethod: MarkStackingMethod {
        switch stacking {
        case .center: return .center
        case .normalized: return .normalized
        case .standard: return .standard
        case .unstacked: return .unstacked
        }
    }
    let case2: [ChartData]
    
    var body: some View {
        VStack {
            Picker("stacking", selection: $stacking) {
                Text("center").tag(Stacking.center)
                Text("normalized").tag(Stacking.normalized)
                Text("standard").tag(Stacking.standard)
                Text("unstacked").tag(Stacking.unstacked)
            }
            .pickerStyle(.segmented)
            Chart(case2, id: \.name) { element in
                ForEach(element.data, id: \.date) {
                    AreaMark(
                        x: .value("date", $0.date),
                        y: .value("value", $0.value),
                        stacking: stackingMethod
                    )
                }
                .foregroundStyle(by: .value("name", element.name))
            }
        }
    }
}

struct NSRuleChart: View {
    let case2: [ChartData]
    
    var body: some View {
        Chart(case2, id: \.name) { element in
            ForEach(element.data, id: \.date) {
                RuleMark(y: .value("value", $0.value))
            }
            .foregroundStyle(by: .value("name", element.name))
        }
    }
}

struct NSRectangleChart: View {
    let case2: [ChartData]
    
    var body: some View {
        Chart(case2, id: \.name) { element in
            ForEach(element.data, id: \.date) {
                RectangleMark(x: .value("date", $0.date), y: .value("value", $0.value))
            }
            .foregroundStyle(by: .value("name", element.name))
        }
    }
}

struct NSPieChart: View {
    let case2: [ChartData]
    
    var body: some View {
        Chart(case2, id: \.name) { element in
            ForEach(element.data, id: \.date) {
                SectorMark(angle: .value("value", $0.value))
            }
            .foregroundStyle(by: .value("name", element.name))
        }
    }
}

 

Mark의 생성자만 약간식 다를 뿐 사용하는 방법이나 뷰모디파이어는 동일하게 적용시킬 수 있어요

예외적으로 몇몇 차트에는 적용안돼는것도 있으니 잘 선택해야해요

예를들어 

[iOS17] chartScrollableAxes(.horizontal) 을 적용해줬다면 가로로 스크롤이 가능하겠구나를 기대할거에요

여기에 이제 [iOS17] chartScrollPosition 를 추가하면 스크롤포지션도 자동으로 받을 수 있는데

SectorMark에서는 원으로 크게뜨는거라 스크롤할일이 없기때문에 동작하지않아요

 

이모든건 iOS17... 입니다

따라서 16에선..

chartOverlay를 이용해서 커스텀으로 계산하고 만들어 줘야합니다.. ㅠ

struct NSLineChart: View {
    @State private var scroll: Double = 0
    let case2: [ChartData]
    
    var body: some View {
        VStack {
            Text("Scroll: \(scroll)")
            Chart(case2, id: \.name) { element in
                ForEach(element.data, id: \.date) {
                    LineMark(
                        x: .value("date", $0.date),
                        y: .value("value", $0.value)
                    )
                }
                .foregroundStyle(by: .value("name", element.name))
            }
            .chartScrollableAxes(.horizontal)
            .chartScrollPosition(x: $scroll)
        }
    }
}

잘동작하죠 하지만 이 함수를 SectorMark에 적용하면 아무런 동작이 일어나지않아요 

이렇듯 연산자는 모든 차트에 적용할 수 있지만 안되는것도 있다아~ 알아두시면 됩니다!

 

또한 클릭된 위치를 알고싶다면

struct NSLineChart: View {
    ...
    
    @State private var select: Int?
    
    var body: some View {
        VStack {
            ...
            Text("select: \(select ?? 0)")
            Chart(case2, id: \.name) { element in
                ...
                if let select {
                    RuleMark(x: .value("select", select))
                }
            }
            .chartScrollableAxes(.horizontal)
            .chartScrollPosition(x: $scroll)
            .chartPlotStyle { content in
                content.background(Color.red.opacity(0.3))
            }
            .chartXSelection(value: $select)
        }
    }
}

chartPlotStyle을 통해서 차트영역의 뷰를 받아와서 빨간색영역을 준것처럼 커스텀할 수 있고

[iOS17]chartXSelection(value: ) 를통해서 값을 받거나 chartXSelection(range: )를 통해서 범위를 받아 올 수 있어요

하지만 iOS16에선 못쓰겠죠...? ㅠ

커스텀하기 쉽지않을것같네요

 

chartOverlay를 사용하면

ChartProxy를 받아서 사용할 수 있어요

struct NSRectangleChart: View {
    let case2: [ChartData]
    
    var body: some View {
        Chart(case2, id: \.name) { element in
            ForEach(element.data, id: \.date) {
                RectangleMark(x: .value("date", $0.date), y: .value("value", $0.value))
            }
            .foregroundStyle(by: .value("name", element.name))
        }
        .chartOverlay { proxy in
            Color.clear.onAppear {
                let xPos: CGFloat = proxy.position(forX: 4) ?? 0
                print(xPos)
                if let xValue: Int = proxy.value(atX: xPos) {
                    print(xValue)
                }
            }
        }
    }
}

proxy를 이용하면 차트의 x, y값에 접근할 수 있어요

또한 position, value 함수를 통해서 

좌표 -> 값

값 -> 좌표

로 변환해서 이용할 수도 있어요

 

위의코드와 차트를 보면

position(forX: 4)를 이용해서

x값이 4인곳의 위치는 어디인가를 가져올 수 있었고

 

반대로 x위치가

value(atX: 245.33333333333331)(x = 4의 위치입니다)을 이용해서

해당 좌표의 X값은 4라는걸 얻을 수 있습니다

 

 

 

 

annotation을 이용해서 주석도 달아줄수있습니다

위치관련한 매개변수가 2개가있는데 

position - 그려질 곳 좌표계기준에서의 위치

alignment- 위치가 지정된뒤 어느부분을 정렬로 맞출것인지

default
position: leading
alignment: leading

 

 

이렇듯 차트타입과 차트에 부가적인 기능사용법을 조금알아봤는데요

생각보다 사용하기 쉽게 간단해서 

OS버전만 맞으면 차트도이제 금방 구현할 수 있을 것 같다는 생각이 드네요

 

또한...! 차트는 iOS17을 타겟으로 해야 쓸만할것같은 느낌이 듭니다..!

단순히 차트만 보여주는거라면 16이면 충분할것같은데...

부가적인 기능을 추가하려면 골치아파질것같네요 🥲

 

반응형