IOS 개발일지

백준 티어 배지를 직접 구현해보기

호종이 2022. 2. 17. 14:55

백준 프로필의 IOS 버전을 개발하던 도중, 티어 배지를 Swift UI 의 Shape 프로토콜을 사용하는 커스텀 도형으로 직접 구현해보기로 하였다. 

티어 배지

티어 배지는 위와 같이 생겼으며, 물론 어도비 XD 를 이용해 svg파일을 직접 추출해도 되지만 6가지의 티어 (브론즈, 실버, 골드, 플래티넘, 다이아, 루비)  색상과, 1~5단계에 해당하는 티어 단계로 인해 만약 위와 같은 방식으로 구현할 경우 총 30가지의 svg 파일을 추출해야 한다. Android 버전은 티어 단계를 텍스트 뷰로 구현해 총 6가지의 svg 파일만으로 구현을 완료했지만, IOS 버전에서는 직접 저 모양을 그려서 구현해보았다. 

 

Swift UI Shape 프로토콜


Swift UI 에서 사각형, 원, 둥근 사각형 등은 각각 Rectangle, Circle, RoundedRectangle 등으로 구현되어 간단히 사용할 수 있다. 해당 도형은 모두 Shape 프로토콜을 따르는 구조체로 구현되있으며, Shape 프로토콜의 정의는 다음과 같다. 

public protocol Shape : Animatable, View {
    func path(in rect: CGRect) -> Path
}

Shape 프로토콜은 기본적으로 Path 를 반환하는 함수를 구현해야 하는데, 해당 함수는 CGRect 형태의 사각형을 받아 그 사각형에 그려질 것을 정의하는 Path 객체를 반환한다. 여기서 CGRect 형태의 사각형이 도형이 그려질 캔버스라고 생각하면 된다. 이 사각형은 우리가 View 의 frame 을 통해 지정해주는 사이즈를 가지고 있으며, 화면상의 좌표에 대한 정보를 가지고 있다. 

 

Path 인스턴스는 포인트 간의 좌표를 지정하고 그려질 선을 정의하여 2차원 도형을 제공한다. 포인트 간의 직선은 직선, 3차 및 2차 베지어 곡선, 호, 타원, 그리고 사각형을 사용하여 그릴 수 있다. 

 

아래 예제를 통해 직접 Shape 프로토콜을 따르는 사각형을 구현해보자. 우선 Shape 프로토콜을 구현하는 구조체를 선언해주자

struct CustomRectangle : Shape {
    func path(in rect: CGRect) -> Path {
        
    }
}

이제 Path 인스턴스를 생성해, 사각형을 그리게 작업하면 된다.

struct CustomRectangle : Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.minX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
        path.closeSubpath()
        return path
    }
}

위 코드를 해석해보자

  • move()
    • 위 메소드를 사용하여 시작점의 좌표로 경로가 시작된다. 시작점의 좌표는 캔버스의 좌측 상단 좌표인, rect.minX, rect.minY 좌표로 지정된다.
  • addLine(to: CGPoint)
    • 직전 좌표에서 to 좌표까지 직선을 추가한다. 
  • closeSubpath()
    • 끝점과 시작점을 연결하여 경로를 닫는다.

즉 먼저 캔버스의 좌측 상단으로 좌표를 지정한 다음, 캔버스의 좌측 하단으로 선을 추가하고, 그 다음 좌측 하단 -> 우측 하단으로 선을 추가하고, 우측 하단 -> 우측 상단으로 선을 추가한뒤 우측 상단 -> 좌측 상단을 연결하여 경로를 닫는 것이다. 

 

위 방법을 사용해 간단히 사각형을 구현했다. 직접 사용해보자.

struct ContentView: View {
    var body: some View {
        CustomRectangle()
            .frame(width: 50, height: 100, alignment: .leading)
    }
}

아이폰 시뮬레이터에서 해당 사각형을 사용해보았다. frame 을 통해 넓이를 50, 높이를 100으로 지정해줬으며, 이것은 위에서 path 함수에 전해질 캔버스의 크기를 지정해준것이다. 따라서 rect 의 maxX 는 50이 되며, maxY는 100이 된다. 만약 넓이와 높이를 똑같이 50으로 지정해주면 정사각형이 그려질 것이다. 

 

위 예제는 간단하게 직선만을 추가했지만, 곡선, 호 등도 추가할 수 있으며, 자세한 내용은 아래 사이트를 참고하자. 

 

Apple Developer Documentation

 

developer.apple.com

티어 배지를 구현하기


티어 배지

티어 배지의 모양이 저렇기 때문에, 두 가지 모양을 만들어서 ZStack 으로 합치는 방법을 선택했다. 일단 첫 번째 모양은

위와 같고, 5개의 path 만 그려주면 되기 때문에 비교적 쉽게 구현할 수 있었다. 

struct TierBadgeShape : Shape {
    
    func path(in rect: CGRect) -> Path {
        let path = Path { path in
            let bottomOfRect : CGFloat = rect.width * 1.09 //직사각형의 바닥 좌표
            let heightOfMask = rect.midX / 1.8 //직사각형 밑 삼각형의 높이
            
            path.move(to: CGPoint(x: rect.minX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.minX, y: bottomOfRect))
            path.addLine(to: CGPoint(x: rect.midX, y: bottomOfRect + heightOfMask))
            path.addLine(to: CGPoint(x: rect.maxX, y: bottomOfRect))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
            path.closeSubpath()
        }
        
        return path
        
    }
}

그 다음 두 번째 모양인 V 자 모양을 만들기 위해 다음과 같이 구현했다.

V자 모양

struct TierBadgeMask : Shape {
    func path(in rect: CGRect) -> Path {
        let bottomOfRect : CGFloat = rect.width * 1.09 //직사각형의 바닥 좌표
        let height = bottomOfRect * 0.1 //V 자 모양의 높이
        let heightOfMask = rect.midX / 1.8 //직사각형 밑 삼각형의 높이
        let startOfMaskY = bottomOfRect - (height * 2) //V자 모양 시작 Y좌표
        var path = Path()
        path.move(to: CGPoint(x: rect.minX, y:startOfMaskY ))
        path.addLine(to: CGPoint(x: rect.minX, y: startOfMaskY + height))
        path.addLine(to: CGPoint(x: rect.midX, y: startOfMaskY + heightOfMask + height))
        path.addLine(to: CGPoint(x: rect.maxX, y: startOfMaskY+height))
        path.addLine(to: CGPoint(x: rect.maxX, y: startOfMaskY))
        path.addLine(to: CGPoint(x: rect.midX, y: startOfMaskY+heightOfMask))
        path.closeSubpath()
        
        return path
    }
}

그 다음 위 두 가지 모양을 ZStack 으로 합치고, Text 뷰를 추가하여 손쉽게 티어 배지 모양을 구현할 수 있었다. 

struct TierBadge: View {
    let width : CGFloat
    let tier: Int
    var body: some View {
        ZStack(alignment:.center) {
            TierBadgeShape()
                .frame(width:width,height: width)
                .foregroundColor(Profile.getTierColor(tier: tier))
                .shadow(radius:1.5)
           TierBadgeMask()
                .frame(width: width, height: width)
                .foregroundColor(.white)

            Text(String(Profile.getTierNumber(tier:tier)))
                .fontWeight(.bold)
                .font(.system(size: 100))
                .minimumScaleFactor(0.0001)
                .foregroundColor(.white)
                .frame(width: width, height: width, alignment: .center)
        }
    }
}

결과물