DEV_Larva

SwiftUI Tutorials - Landmarks(4) 본문

SwiftUI & UIKit/Tutorials

SwiftUI Tutorials - Landmarks(4)

NelchuPapa 2022. 10. 19. 16:43
반응형

카카오톡 데이터센터 화재로 인해 며칠 동안 글을 쓰지 못했는데 여하튼 지난 시간에 이어서 계속 진행해보겠다.

이번 시간에는 사용자가 해당 리스트의 랜드마크를 방문할 때나 여러 가지 수행 과목을 성공하게 되면 얻게 되는 배지를 받게 할 것이다. 그러기 위해서는 당연히 배지를 만들어 주어야 하는데 이번 시간에는 서로 다른 도형을 결합하거나 오버레이 하여 여러 개의 배지를 한번 만들어보자. 

 


1.  Create Drawing Data for a Badge View

배지를 만들려면 먼저 배지 배경의 육각형 모양을 그리는 데 사용할 수 있는 데이터를 정의해야 합니다. HexagonParameters라는 swift파일을 하나 생성해주고, 그 내부에 HexagonParameters라는 struct를 하나 만들자. 그리고 육각형의 한 면을 나타내는 세 개의 점을 유지하는 세그 먼트 구조를 정의한다. CGPoint를 이용할 수 있도록 CoreGraphics를 import 해주자. 현재까지의 코드 진행 상황은 다음과 같다.

import CoreGraphics

struct HexagonParameters {
    struct Segment {
        let line: CGPoint
        let curve: CGPoint
        let control: CGPoint
    }
}

다음으로 바로 아래에 세그먼트를 저장할 배열을 만들어준다. 이 배열 안에 육각형의 각 측면에 대해 한 개당 총 6개의 세그먼트 데이터를 추가한다. 그리고 육각형의 모양을 조정할 수 있는 조정 값을 추가한다.

 

import CoreGraphics

struct HexagonParameters {
    struct Segment {
        let line: CGPoint
        let curve: CGPoint
        let control: CGPoint
    }

    static let adjustment: CGFloat = 0.085  // 조정값 추가

    static let segments = [ // 육각형 세그먼트 데이터 배열
        Segment(
            line:    CGPoint(x: 0.60, y: 0.05),
            curve:   CGPoint(x: 0.40, y: 0.05),
            control: CGPoint(x: 0.50, y: 0.00)
        ),
        Segment(
            line:    CGPoint(x: 0.05, y: 0.20 + adjustment),
            curve:   CGPoint(x: 0.00, y: 0.30 + adjustment),
            control: CGPoint(x: 0.00, y: 0.25 + adjustment)
        ),
        Segment(
            line:    CGPoint(x: 0.00, y: 0.70 - adjustment),
            curve:   CGPoint(x: 0.05, y: 0.80 - adjustment),
            control: CGPoint(x: 0.00, y: 0.75 - adjustment)
        ),
        Segment(
            line:    CGPoint(x: 0.40, y: 0.95),
            curve:   CGPoint(x: 0.60, y: 0.95),
            control: CGPoint(x: 0.50, y: 1.00)
        ),
        Segment(
            line:    CGPoint(x: 0.95, y: 0.80 - adjustment),
            curve:   CGPoint(x: 1.00, y: 0.70 - adjustment),
            control: CGPoint(x: 1.00, y: 0.75 - adjustment)
        ),
        Segment(
            line:    CGPoint(x: 1.00, y: 0.30 + adjustment),
            curve:   CGPoint(x: 0.95, y: 0.20 + adjustment),
            control: CGPoint(x: 1.00, y: 0.25 + adjustment)
        )
    ]
}

 

 


2.  Draw the Badge Background

SwifUI의 그래픽 API를 이용해서 맞춤형 배지 모양을 그려보자. 새로운 파일 BadgeBackground을 하나 만들어주고 Path shape을 추가하고 여기에 fill 수정자를 이용하여 모양을 view로 바꿀 거다. 크기가 100 * 100 픽셀인 컨테이너를 가정하고 경로에 시작점을 추가한다. 대략적인 육각형을 만들기 위해 모양 데이터의 각 점에 대한 선을 그려줄 건데 여기서 사용하는 매서드에 대해서 자세히 짚고 넘어가 보자. 

  1. move(to:)  = 이것은 가상의 펜이나 연필이 그리기 시작을 기다리는 영역 위로 마우스를 가져가는 것처럼, 모양의 경계 내에서 그리기 커서를 이동한다. 
  2. addline(to:) = addline은 단일 점을 가져와서 그린다. addLine(to:)에 대한 연속적인 호출은 이전 지점에서 줄을 시작하고 새 지점으로 계속됩니다.

 

 

import SwiftUI

struct BadgeBackground: View {
    var body: some View {
        Path { path in
            var width: CGFloat = 100.0
            let height = width
            path.move(
                to: CGPoint(
                    x: width * 0.95,
                    y: height * 0.20
                )
            )
        }
        .fill(.black)
    }
}

struct BadgeBackground_Previews: PreviewProvider {
    static var previews: some View {
        BadgeBackground()
    }
}

 

 

(1) 계속해서 배지의 모서리의 곡선을 addQuadCurve 메서드를 이용해줘서 그려주는데 이것도 조금 있다 다시 언급하겠다.

(2) 배지 값을 하드 코딩하는 대신에 크기를 포함하는 뷰의 크기를 사용할 수 있게 GeometryReader에서 경로를 래핑 한다.

(3) xScale을 사용하여 x축에서 모양의 크기를 조정한 다음 xOffset을 추가해서 지오메트리 내부에서 모양을 가운데로 잡아주자

(4) 검은색 배경을 디자인에 맞게 그러데이션으로 교체 후

(5) aspectRatio를 이용해서 그라디언트를 채워주면 끝이다.

 

import SwiftUI

struct BadgeBackground: View {
    var body: some View {
        GeometryReader { geometry in
            Path { path in
                var width: CGFloat = min(geometry.size.width, geometry.size.height)
                let height = width
                let xScale: CGFloat = 0.832
                let xOffset = (width * (1.0 - xScale))
                width *= xScale
                path.move(
                    to: CGPoint(
                        x: width * 0.95 + xOffset, //x축에서 크기 조정 이후 모양 가운데 정렬
                        y: height * (0.20 + HexagonParameters.adjustment)
                    )
                )

                HexagonParameters.segments.forEach { segment in
                    path.addLine(
                        to: CGPoint(
                            x: width * segment.line.x + xOffset, //x축에서 크기 조정 이후 모양 가운데 정렬
                            y: height * segment.line.y
                        )
                    )

                    path.addQuadCurve(
                        to: CGPoint(
                            x: width * segment.curve.x + xOffset, //x축에서 크기 조정 이후 모양 가운데 정렬
                            y: height * segment.curve.y
                        ),
                        control: CGPoint(
                            x: width * segment.control.x + xOffset,
                            y: height * segment.control.y
                        )
                    )
                }
            }
            .fill(.linearGradient(  //검정색 배경으로 그라데이션 교체 
                Gradient(colors: [Self.gradientStart, Self.gradientEnd]),
                startPoint: UnitPoint(x: 0.5, y: 0),
                endPoint: UnitPoint(x: 0.5, y: 0.6)
            ))
        }
        .aspectRatio(1, contentMode: .fit) 
    }
    static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
    static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
}

struct BadgeBackground_Previews: PreviewProvider {
    static var previews: some View {
        BadgeBackground()
    }
}

 

여기까지의 과정이 워낙 말로 설명하기에는 모호한 부분이 많으니 시각적으로 어떻게 과정마다 변화하는지 확인해보면서 넘어가자.

왼쪽 위에서부터 1이다. 

 

넘어가기 전에 addQuadCurve에 대해서 간략하게 알아보고 넘어가자. 

공식문서에 따르면 path에 2차 베지에 곡선을 추가한다고 하는데 여기서 또 베지에 곡선은 무엇인가? 찾아보니 베지어 곡선(Bezier Curve)은 컴퓨터 그래픽스에서 사용되는 특별한 형태의 곡선으로, CSS 애니메이션 등에서 도형을 그릴 때 사용한다고 한다~

 


 

3.  Draw the Badge Symbol

앱 아이콘이 아마 따로 편집하지 않았다면 빈 항목으로 되어 있을 텐데 이걸 지워버리고 튜토리얼 페이지에서 제공하는 파일을 다운로드하여 드래그를 통해 아래와 같이 만들어주자.

그리고 새로운 커스텀 뷰인 BadgeSymbol 파일을 하나 생성해주고 경로 API를 사용하여 기호의 상단 부분과 하단 부분을 그려준다. 

 

 

struct BadgeSymbol: View {
static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255)'

    var body: some View {
        GeometryReader { geometry in
            Path { path in
                let width = min(geometry.size.width, geometry.size.height)
                let height = width * 0.75
                let spacing = width * 0.030
                let middle = width * 0.5
                let topWidth = width * 0.226
                let topHeight = height * 0.488

                path.addLines([   // 상단 
                    CGPoint(x: middle, y: spacing),
                    CGPoint(x: middle - topWidth, y: topHeight - spacing),
                    CGPoint(x: middle, y: topHeight / 2 + spacing),
                    CGPoint(x: middle + topWidth, y: topHeight - spacing),
                    CGPoint(x: middle, y: spacing)
                ])
               
                path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3)) // 하단
                path.addLines([
                    CGPoint(x: middle - topWidth, y: topHeight + spacing),
                    CGPoint(x: spacing, y: height - spacing),
                    CGPoint(x: width - spacing, y: height - spacing),
                    CGPoint(x: middle + topWidth, y: topHeight + spacing),
                    CGPoint(x: middle, y: topHeight / 2 + spacing * 3)
                ])
            }
        }
    }

저기 하단 부분 코드를 보면 move(to:)라는 수정자가 보이는데 이는 동일한 경로에 있는 여러 가지의 모양 사이에 간격을 원하는 만큼 삽입할 때 사용한다. 그리고 이 디자인의 색상을 static으로 미리 선언 해었다.


다음 섹션으로 넘어가기 전에 새로운 파일 RotatedBadgeSymbol을 만들어 주고 여기서 회전된 문양의 캡슐화하는 역할을 부여한다. 

import SwiftUI

struct RotatedBadgeSymbol: View {
    let angle: Angle
    
    var body: some View {
        BadgeSymbol()
            .padding(-60)
            .rotationEffect(angle, anchor: .bottom)
    }
}

struct RotatedBadgeSymbol_Previews: PreviewProvider {
    static var previews: some View {
        RotatedBadgeSymbol(angle: Angle(degrees: 5))
    }
}

색상 변경 전  후,  RotatedBadgeSymbol view

 


 

4. Combine the Badge Foreground and Background

 

배지 디자인을 더 다양하게 만들어 보자. 여기서는 회전할 새 유형을 정의하고, For Each뷰를 활용 햇 동일한 조정을 여러 개의 산 모양 복사본에 적용할 것이다. 

 

새로운 UI파일 Badge를 생성하고 body에 기본 text가 아닌 BadgeBackground를 배치한다. 그리고 배지의 문양을 ZStack에 배치하고 배지 배경위에 놓는다. 

 

 

import SwiftUI

struct Badge: View {
    var badgeSymbols: some View {
        RotatedBadgeSymbol(angle: Angle(degrees: 0))
            .opacity(0.5)
    }
    
    var body: some View {
        ZStack {
            BadgeBackground()
            
            badgeSymbols
        }
    }
}

struct Badge_Previews: PreviewProvider {
    static var previews: some View {
        Badge()
    }
}

 

 

주변 지오메트리를 읽고 배지 문양의 크기를 수정하고, ForEach 보기를 추가해서 배지 문양의 복사본을 회전하고 표시하면 다음과 같다.

 

import SwiftUI

struct Badge: View {
    var badgeSymbols: some View {
        ForEach(0..<8) { index in //ForEach를 이용한 복사본 회전 표시
            RotatedBadgeSymbol(
                angle: .degrees(Double(index) / Double(8)) * 360.0
            )
        }
        .opacity(0.5)
    }
    
    var body: some View {
        ZStack {
            BadgeBackground()
            
            GeometryReader { geometry in  // 지오메트리를 읽고 배지 크기 수정 
                badgeSymbols
                    .scaleEffect(1.0 / 4.0, anchor: .top)
                    .position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height)
            }
        }
        .scaledToFit()
    }
}

struct Badge_Previews: PreviewProvider {
    static var previews: some View {
        Badge()
    }
}

 

 

 위 과정을 순서대로 실행 하였을때 확인 할 수있는 Preview 결과이다. 

 

이렇게 하나의 배지를 만드는 데 성공했다.

 


이미지를 포개고 이동시키는 과정에서 생각보다 많은 작업과, 다양한 수정자를 사용하면서 뭐 하나 쉽게 넘어가는 것은 진짜 없는 것 같다.

반응형

'SwiftUI & UIKit > Tutorials' 카테고리의 다른 글

SwiftUI Tutorials - Landmarks(6)  (0) 2022.11.01
SwiftUI Tutorials - Landmarks(5)  (0) 2022.10.29
SwiftUI Tutorials - Landmarks(3)  (0) 2022.10.15
SwiftUI Tutorials - Landmarks(2)  (0) 2022.10.08
SwiftUI Tutorials - Landmarks(1)  (0) 2022.10.06