궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
PROJECT/Simple

SwiftUI Project11 : Shape를 활용하여 뷰 꾸미기

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

 

이번에는 PathShape를 사용하여 화면을 구성해보겠습니다.

 

Project11 

1. 사다리꼴 만들기

우선 Trapezium이라는 struct를 선언하고 아래와 같이 코드를 작성합니다.

struct Trapezium: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        return path
    }
}

그리고 커서를 화면 왼쪽 상단 끝으로 이동시켜줍니다.

path.move(to: CGPoint(x: 0, y: 0))

보통 이렇게 xy0으로 둘 수도 있지만 아래와 같이 CGPoint를 삭제하고 간단하게 작성할 수 있습니다.

path.move(to: .zero)

이러한 박스를 만들어 주려고 하는데 이 도형을 만들기 위한 방법은 여러 가지가 있습니다. 이번에는 두 가지 방법을 알아보도록 하겠습니다.

 

일단 오른쪽 끝으로 선을 이동하고 그 후 화면 아래로 선을 그어주어야 합니다.

첫 번째 방법

첫 번째 방법은 rect.size.width을 사용하여 화면 오른쪽 끝으로 이동하고 rect.size.heighty좌표 끝으로 이동하는 코드로 구성할 수 있습니다.

struct Trapezium: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        //커서 이동
        path.move(to: .zero)
        //화면 오른쪽 끝으로 선을 그어줌
        path.addLine(to: CGPoint(x: rect.size.width, y: 0))
        //화면 하단으로 선을 그어줌
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height))
        
        return path
    }
}

두 번째 방법

두 번째 방법은 rect.maxX를 사용하는 것입니다.

struct Trapezium: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: .zero)
        //화면 오른쪽 끝으로 선을 그어줌
        path.addLine(to: CGPoint(x: rect.maxX, y: 0))
        //화면 하단으로 선을 그어줌
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        
        return path
    }
}

결과는 동일합니다. 

 

그렇다면 여기서 사각형을 만들려면 아래와 같이 작성 하면 됩니다.

struct Trapezium: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: .zero)
        //화면 오른쪽 끝으로 선을 그어줌
        path.addLine(to: CGPoint(x: rect.maxX, y: 0))
        //화면 하단으로 선을 그어줌
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        //화면 왼쪽 끝으로 선을 그어줌
        path.addLine(to: CGPoint(x: 0, y: rect.maxY))
        
        return path
    }
}

그리고 마지막으로 closeSubpath()수정자를 끝에 작성해줘야 합니다.

struct Trapezium: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        ...
        
        path.closeSubpath()
        
        return path
    }
}
// body

struct ContentView: View {
    var body: some View {
        Trapezium()
            
    }
}

그런데 우리는 사다리꼴 모양을 만들어야 합니다. 세 번째 줄에 있는 Y좌표를 이동해주면 될 것 같네요.

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

일단 올라오긴 왔습니다. Y좌표 값에 저런 식으로 절댓값을 주는 거보다는 변수를 하나 생성해서 자유롭게 수정 가능하도록 하는 게 좋겠습니다.

struct Trapezium: Shape {
    
    var offset: CGFloat = 100
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: .zero)
        path.addLine(to: CGPoint(x: rect.maxX, y: 0))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: 0, y: offset))
        path.closeSubpath()
        
        return path
    }
}

그리고 body부분 쪽에서 .frame()수정자를 사용하여 사다리꼴을 완성시켜 보도록 하겠습니다.

// MARK : body

struct ContentView: View {
    var body: some View {

        VStack(spacing: 50) {
            Trapezium()
                .frame(height: 400)
            
            Text("서근 개발블로그")
                .font(.title)
            
                Spacer()
        }
    }
}

offset이 마음에 들지 않네요. body부분에서 offset값을 수정해줄 수 있습니다.

// MARK : body

struct ContentView: View {
    var body: some View {

        VStack(spacing: 50) {
            Trapezium(offset: 300)
                .fill(Color.pink)
                .frame(height: 400)
                .edgesIgnoringSafeArea(.top)
                         
            Text("서근 개발블로그")
                .font(.title)        
                Spacer()
        }
    }
}

어느 정도 틀이 나타났습니다. :) 

이미지 넣기

이제 이 사다리꼴 안에 이미지를 넣어주죠.

mountain.jpg
0.11MB

// MARK : body

struct ContentView: View {
    var body: some View {

        VStack(spacing: 50) {
            
            Image("mountain")
                .resizable()
                .scaledToFill()
                .clipShape(Trapezium())
            
            // 사다리꼴 프리뷰 라고 생각하면 됨
            Trapezium(offset: 300)
                .fill(Color.pink)
                .frame(height: 400)
                .edgesIgnoringSafeArea(.top)
                
            
            Text("서근 개발블로그")
                .font(.title)
            
                Spacer()
        }
    }
}

화면에서 보이듯이 이미지의 사다리꼴 형태를 재설정해야 합니다. 아래 도형은 프리뷰라고 생각하시면 됩니다. 우리는 Trapezium메서드의 offset값을 재 설정해야 하는데 숫자 대신에 백분율을 넣어주겠습니다. 

struct Trapezium: Shape {
    
    var offset: CGFloat = 0.75
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: .zero)
        path.addLine(to: CGPoint(x: rect.maxX, y: 0))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: 0, y: rect.maxY * offset))
        path.closeSubpath()
        
        return path
    }
}

이제 제대로 이미지가 사다리꼴 형태로 잘 나왔습니다. body쪽 코드를 정리해주겠습니다.

var body: some View {
    VStack(spacing: 50) {       
        Image("mountain")
            .resizable()
            .scaledToFill()
            .frame(height: 400)
            .clipShape(Trapezium())
            .edgesIgnoringSafeArea(.top)       
        Text("서근 개발블로그")
            .font(.title)
        Spacer()
    }
}

Slider 넣기

그 다음에는 .overlay수정자를 사용하여 테두리 효과를 주고 offsetslider를 통해 움직이도록 해주겠습니다. 그 아래에는 reset버튼을 만들어서 슬라이더를 초기화할 수 있도록 만들어 줍니다.

// MARK : body

struct ContentView: View {
    // 1.
    @State private var offset: CGFloat = 0.5
    var body: some View {
        
        VStack(spacing: 50) {
            Image("mountain")
                .resizable()
                .scaledToFill()
                .frame(height: 400)
                // 2.
                .clipShape(Trapezium(offset: offset))
                // 3.
                .overlay(Trapezium(offset: offset).stroke(Color.yellow, lineWidth: 10))
                
                .edgesIgnoringSafeArea(.top)
            Text("서근 개발블로그")
                .font(.title)
            
            // 4.
            Slider(value: $offset, in: 0.1 ... 1)
            // 5.
            Button("초기화", action: {
                self.offset = 0.5
            })
            Spacer()
        }
    }
}

Animation 넣기

ButtonwithAniamtion을 사용하여 reset버튼을 누를 때마다 애니메이션 효과를 넣어주도록 하겠습니다.

Slider(value: $offset, in: 0.1 ... 1)
// 5.
Button("초기화", action: {
    withAnimation() {
        self.offset = 0.5
    }
})

하지만 아무런 애니메이션 효과가 적용되지 않죠?  이럴 때는 animatableData 프로퍼티를 정의해줘야 합니다. 

 

Trapezium struct로 돌아가서 이곳에 이 프로퍼티를 정의해주겠습니다.

struct Tarpezium: Shape {
  var animatableData: CGFloat {
   
  } 
}

animatableData에는 getset이 와줘야겠죠?

struct Trapezium: Shape {
    
    var offset: CGFloat = 0.75
    var animatableData: CGFloat {
        get { return offset }
        set { offset = newValue }
    }
    
    func path(in rect: CGRect) -> Path {
        ...
    }
}

withAnimation수정자 안에 다른 Animation효과를 추가해 볼까요?

 

 

Button("초기화", action: {
    withAnimation(Animation.linear(duration: 3)) {
        self.offset = 0.5
    }
})

앱이 실행되면 애니메이션 효과

실제 앱에서는 저런 식으로 초기화를 하고 슬라이더를 움직여서 애니메이션 효과를 볼일은 별로 없을 것 같네요. 그렇기 때문에 앱이 실행되면 자동으로 애니메이션 효과와 함께 offset이 바뀌도록 해주겠습니다.

 

우선 만들어뒀던 SliderButton을 주석 처리하고 시작하겠습니다.

struct ContentView: View {
     // 1.
     @State private var startChange = false
    var body: some View {
        
        VStack(spacing: 50) {
            Image("mountain")
                .resizable()
                .scaledToFill()
                .frame(height: 400)
              // 2. startChange가 true이면 0.3
                .clipShape(Trapezium(offset: startChange ? 0.3 : 1 ))
              // 3.
                .overlay(Trapezium(offset: startChange ? 0.3 : 1 )
                .stroke(Color.yellow, lineWidth: 10))
                
                .edgesIgnoringSafeArea(.top)
            Text("서근 개발블로그")
                .font(.title)

이제 앱이 실행되는 코드를 작성해줘야 합니다. .onAppear수정자를 통해 이것을 구현할 수 있습니다.

// MARK : body

struct ContentView: View {

    @State private var startChange = false
    
    var body: some View {
        
        VStack(spacing: 50) {
             ...
        }
        // 앱이 실행될때 구현
        .onAppear {
            //애니메이션 적용
            withAnimation(Animation.linear(duration: 3)) {
                self.startChange = true
            }
        }
    }
}

이왕 했으니 Text쪽도 적용시켜 줄게요

Text("서근 개발블로그")
    .font(.largeTitle)
    .offset(x: startChange ? 0 : -300, y: 0)

2. 사다리꼴 추가

이제 위에 만들어둔 Trapezium을 아래에 한번 더 재사용 해보도록 합시다.

// MARK : body

struct ContentView: View {
   
    @State private var startChange = false
    var body: some View {
        
        VStack(spacing: 50) {
               ...
               
            Trapezium()
            
        }
    }
}

그런데 Trapezium에서 첫번째 커서를 이동할 때 우리는 .zero값을 주었는데 이것은 두 번째 호출한 메서드에서는 사용을 할 수 없습니다. 그리고 자유롭게 도형을 변형시킬 수 없죠. 이것을 사용하기 위해서는 함수를 다시 수정해야 합니다.

struct Trapezium: Shape {
    
    var offset: CGFolat = 0.75
    // 1.
    var corner: UIRectCorner = .bottomLeft
    
    var animatableData: CGFloat {
        get { return offset }
        set { offset = newValue }
    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // 시작점 - TopLeft
        // 2. Y좌표에서 cornerdl .topLeft라면 rect.maxy * offset
        path.move(to: CGPoint(x: 0, y: corner == .topLeft ? rect.maxY * offset : 0 ))
        // TopRight
        path.addLine(to: CGPoint(x: rect.maxX, y: 0))
        // Bottom Right
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        
        // 3.
        path.addLine(to: CGPoint(x: 0, y: corner == .bottomLeft ? rect.maxY * offset : rect.maxY))
        
        path.closeSubpath()
        
        return path
    }
}

이렇게 함수를 수정해줬고, 이제 잠시 Animation효과와 Textoffset효과를 주석처리해주고 아래와 같이 수정해줍니다.

VStack(spacing: 50) {
    ...
        
     Trapezium(offset: 0.3, corner: .topLeft)
       .edgesIgnoringSafeArea(.bottom)
    
}

이제 아래 Trapeziumopacity(투명도) 와 shadow효과를 주고 또 다른 Trapezium을 추가해보죠

ZStack {
    Trapezium(offset: 0.5, corner: .topLeft)
        .fill(Color.yellow.opacity(0.5))
        .shadow(radius: 10)
    
    Trapezium(offset: 0.5, corner: .topRight)
        .fill(Color.yellow.opacity(0.5))
        .shadow(radius: 10)
}
.edgesIgnoringSafeArea(.bottom)

topRight도 함수를 재 정의 해줘야 겠군요.

func path(in rect: CGRect) -> Path {
    var path = Path()
    
    ...
    
    // TopRight
    path.addLine(to: CGPoint(x: rect.maxX, y: corner == .topRight ? rect.maxY * offset : 0))
    
    ...
    
    return path
}

여기까지 완성이 됐습니다. :) 이제 화면을 정리해가면서 텍스트와 애니메이션 효과 등을 추가해주도록 하겠습니다.

// MARK : body

struct ContentView: View {
    @State private var startChange = true
    var body: some View {
        
        VStack(spacing: 50) {
            
            ZStack {
                Image("mountain")
                    .resizable()
                    .scaledToFill()
                    .frame(height: 400)
                    .clipShape(Trapezium(offset: startChange ? 0.8 : 1 ))
                
                Trapezium(offset: 0.6, corner: .bottomRight)
                    .fill(Color.yellow.opacity(0.5))
                    .shadow(radius: 10)
                    .frame(height: 400)
            }
              .edgesIgnoringSafeArea(.top)
            
            
            Text("서근 개발블로그")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            Spacer()
            
            ZStack {
                Trapezium(offset: 0.5, corner: .topLeft)
                    .fill(Color.yellow.opacity(0.5))
                    .shadow(radius: 10)
                
                Trapezium(offset: 0.5, corner: .topRight)
                    .fill(Color.yellow.opacity(0.5))
                    .shadow(radius: 10)
            }
            .edgesIgnoringSafeArea(.bottom)
        }
    }
}

bodyZStack.bottomRight를 넣어줬으니 다시 Trapezium함수도 수정해줘야 합니다.

func path(in rect: CGRect) -> Path {
    var path = Path()
    
    ...
    
    // Bottom Right
    path.addLine(to: CGPoint(x: rect.maxX, y: corner == .bottomRight ? rect.maxY * offset : rect.maxY))
    
    ...
    
    return path
}

아시다시피 ZStack은 제일 마지막에 호출한 자식 뷰가 제일 상단으로 올라오게 되어있습니다. 그렇기 때문에 Image가 제일 뒤로 에 배치되어 있는 것이죠. ImageZStack하단으로 내려주겠습니다.

ZStack {
    Trapezium(offset: 0.8, corner: .bottomRight)
        .fill(Color.yellow.opacity(0.5))
        .shadow(radius: 10)
        .frame(height: 400)
    
    Image("mountain")
        .resizable()
        .scaledToFill()
        .frame(height: 400)
        .clipShape(Trapezium(offset: startChange ? 0.5 : 1 ))
    
}

마지막으로 주석처리했던 애니메이션 효과를 활성화해주면 아래와 같은 결과를 얻을 수 있습니다. 

전체 코드

<hide/>

//  ContentView.swift
//  SwiftUI_Project11_Trapezium

import SwiftUI

struct Trapezium: Shape {
    
    var offset: CGFloat = 0.8
    var corner: UIRectCorner = .bottomLeft
    
    var animatableData: CGFloat {
        get { return offset }
        set { offset = newValue }
    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // 시작점 - TopLeft
        //Y좌표에서 cornerdl .topLeft라면 rect.maxy * offset
        path.move(to: CGPoint(x: 0, y: corner == .topLeft ? rect.maxY * offset : 0 ))
        // TopRight
        path.addLine(to: CGPoint(x: rect.maxX, y: corner == .topRight ? rect.maxY * offset : 0))
        // Bottom Right
        path.addLine(to: CGPoint(x: rect.maxX, y: corner == .bottomRight ? rect.maxY * offset : rect.maxY))
        
        path.addLine(to: CGPoint(x: 0, y: corner == .bottomLeft ? rect.maxY * offset : rect.maxY))
        
        path.closeSubpath()
        
        return path
    }
}

// MARK : body

struct ContentView: View {
    
    @State private var startChange = false
    
    var body: some View {
        
        VStack(spacing: 50) {
            
            ZStack {
                Trapezium(offset: 0.8, corner: .bottomRight)
                    .fill(Color.yellow.opacity(0.5))
                    .shadow(radius: 10)
                    .frame(height: 400)
                
                Image("mountain")
                    .resizable()
                    .scaledToFill()
                    .frame(height: 400)
                    .clipShape(Trapezium(offset: startChange ? 0.5 : 1.1 ))
                
            }
            .edgesIgnoringSafeArea(.top)
            
            
            Text("서근 개발블로그")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            
            Spacer()
            
            
            ZStack {
                Trapezium(offset: 0.5, corner: .topLeft)
                    .fill(Color.yellow.opacity(0.5))
                    .shadow(radius: 10)
                
                Trapezium(offset: 0.5, corner: .topRight)
                    .fill(Color.yellow.opacity(0.5))
                    .shadow(radius: 10)
            }
            .edgesIgnoringSafeArea(.bottom)
        }
        //앱이 실행될때 구현
        .onAppear {
            //애니메이션 적용
            withAnimation(Animation.linear(duration: 1)) {
                self.startChange = true
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

읽어주셔셔 감사합니다🤟

 

본 게시글의 전체코드 GitHub 👇🏻

 

Seogun95/SwiftUI_Project11_Trapezium

Shape를 사용하여 이미지 커스튬. Contribute to Seogun95/SwiftUI_Project11_Trapezium development by creating an account on GitHub.

github.com

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.