• Jan
  • Feb
  • Mar
  • Apr
  • May
  • Jun
  • Jul
  • Aug
  • Sep
  • Oct
  • Nov
  • Dec
  • Sun
  • Mon
  • Tue
  • Wed
  • Thu
  • Fri
  • Sat
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

SwiftUI progress bar custom하기

SwiftUI ProgressView 레퍼런스 너무 없어 …. ( ・ᴗ・̥̥̥ )

구현할 뷰

image

검색해보면 ZStack 써서 View 구조체 짜고 난리를 치길래 오… 망했는데? 라고 생각했는데,
iOS14 에서 추가된 ProgressView를 사용하면 된다! 야호

Basic

우선 기본은 이렇다.

ProgressView(value: 40, total: 100)
ProgressView("", value: 40, total: 100) // title을 넣을수도 있다.

image

요것이 기본 디자인!

나같은 경우에는 위에 벌도 따라다녀야되고, 꿀 이모지도 있어야해서 커스텀을 해야하지만,
SwiftUI에서 기본적으로 제공하는 ProgressViewStyle이 몇개가 있다.

LinearProgressViewStyle : 위 사진 같은 가로로 긴 모양

CircularProgressViewStyle : 동그란 모양 - 주로 로딩 때 돌아가는 원 모양

.progressViewStyle(LinearProgressViewStyle(tint: .red))

이런 식으로 초기에 accentColor값을 지정해 줄 수도 있다.

ProgressViewStyle

하지만 기본 스타일로는 내가 구현해야 하는 뷰는 택도 없다ㅎㅎ
ProgressViewStyle 프로토콜을 따르는 새로운 뷰 스타일을 정의해보자

struct honeyBeeProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
            ProgressView(configuration)
                .accentColor(.yellow)
        }
    }
}

이런식으로 ViewStyle을 짜주면 되는데, 그전에 저 뷰를 어떻게 구성할지 생각을 해봤다.

Stack은 이렇게 구성하고,
벌 indicator는 progressView의 value값이랑 바인딩을 해서 위치값을 주든,
왼쪽 spacing을 늘리든 해서 조절하면 될 것 같았다.

우선 Stack구조를 짜보자

struct honeyBeeProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack(spacing:0){
            HStack{
                Text("🐝")
                Spacer()
                Text("🍯")
            }
            ProgressView(configuration)
        }
    }
}

image

이런 뷰가 나온다. 이제 색상 등 detail한 부분들을 잡아줘야지! 하던 찰나,,,
벌이 꿀을 안 보고 반대방향으로 날고있었다… 근데 이걸 이미지로 받긴 싫고…
어떻게하지 고민하다가 scaleEffect로 텍스트를 좌우반전 시켜줬다.

Text("🐝")
   .scaleEffect(x: -1, y: 1, anchor: .center) //텍스트 좌우반전
ProgressView(configuration)
   .accentColor(.yellow) //progres view tint color

progress view의 색상을 정해줄 때, .foregroundColor 안먹는다. .accentColor로 해야한다 !

image

벌 움직이기

 HStack{
    Text("🐝")
        .font(.system(size: 21))
        .scaleEffect(x: -1, y: 1, anchor: .center)
   			.padding(.leading, CGFloat(value))

처음에는 이렇게 leading에 padding을 주려고 했었다.

image

근데 이렇게 벌이 일정 padding값 이상에서는 잘리다가, 값이 더 커지면 그냥 가려져 버린다.

HStack{
    Text("🐝")
        .font(.system(size: 21))
        .scaleEffect(x: -1, y: 1, anchor: .center)
        .frame(width: CGFloat(value), height: 30, alignment: .bottomTrailing)

그래서 이렇게 frame으로 조절했다. Text를 frame의 오른쪽아래에 고정시키고,
frame의 width값을 조절하는걸로.

value는 바인딩 (property wrapper @State, @Binding 사용)을 해줘야 한다.

벌 - progress bar 연동(?)하기

변수에 바인딩 처리를 해서 벌이 변수 value에 따라 움직이게는 해 놨는데,
사실은 progress bar의 value값에 따라 움직여야 한다.
progress bar의 value/width 값 만큼 value를 지정해주면 되겠다고 생각했다.

근데… progress bar의 width값을 어떻게 받아오지?

GeometryReader

ZStack이랑 View 써서 progress bar 구현하는 포스트에서 많이 본 애… 내가 쓰게 될 줄이야
GeometryReaders는 해당 뷰가 포함되어있는 부모 뷰를 기준으로 view의 frame을 조절할 수 있게 해 준다.

예를 들어 화면의 딱 절반만큼 뷰를 채우고 싶다거나… 할 때 쓰인다.

GeometryReader(content: { geometry in
    Text("Content")
})

요것의 핵심은 바로 geometry.size.width , geometry.size.height !!
바로 짜보자

struct honeyBeeProgressViewStyle: ProgressViewStyle {
    @Binding var value: Double
    @Binding var total: Double
    
    func makeBody(configuration: Configuration) -> some View {
        return GeometryReader{ geometry in
            VStack(spacing:0){
                HStack{
                    Text("🐝")
                        .font(.system(size: 21))
                        .scaleEffect(x: -1, y: 1, anchor: .center)
                        .frame(width: CGFloat(geometry.size.width / 100 * CGFloat(value) + 15), height: 30, alignment: .bottomTrailing)
                    Spacer()
                    Text("🍯")
                        .font(.system(size: 23))
                        .frame(width: 24, height: 30, alignment: .bottomTrailing)
                }
                ProgressView(configuration)
                    .accentColor(.yellow)
            }
        }
    }
}

GeometryReader 안에서는 변수를 선언할 수 없다.
내가 원하는 꿀벌의 위치는 geometry.size.width / 100 * CGFloat(value) + 15 이만큼인데,
저렇게 이 식을 다 때려박자니 너무 지저분해보여서 변수로 빼고 싶었다.
geometry.size.width를 변수 초기화할 때 사용하려면 GeometryReader 내에서 변수를 만들어야하는데,
그게 불가능하고, 굳이 쓰려면 geometryProxy를 쓰면 된다.

근데 나같은 경우엔 ProgressView(configuration) 이 있어서 굉장히 복잡해진 상황…
그래서 geometry.size.width를 제외한 수식들을 makeBody()의 local 변수로 만들었다.

func makeBody(configuration: Configuration) -> some View {
        let offset = CGFloat(value) / 100
        return GeometryReader{ geometry in
            VStack(spacing:0){
                HStack{
                    Text("🐝")
                        .font(.system(size: 21))
                        .scaleEffect(x: -1, y: 1, anchor: .center)
                        .frame(width: CGFloat(geometry.size.width * offset + 15), height: 30, alignment: .bottomTrailing)

image

굿 ! ( ◠‿◠ )