이번에는 Path
와 Shape
를 사용하여 화면을 구성해보겠습니다.
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))
보통 이렇게 x
와 y
를 0
으로 둘 수도 있지만 아래와 같이 CGPoint
를 삭제하고 간단하게 작성할 수 있습니다.
path.move(to: .zero)
이러한 박스를 만들어 주려고 하는데 이 도형을 만들기 위한 방법은 여러 가지가 있습니다. 이번에는 두 가지 방법을 알아보도록 하겠습니다.
일단 오른쪽 끝으로 선을 이동하고 그 후 화면 아래로 선을 그어주어야 합니다.
첫 번째 방법
첫 번째 방법은 rect.size.width
을 사용하여 화면 오른쪽 끝으로 이동하고 rect.size.height
로 y
좌표 끝으로 이동하는 코드로 구성할 수 있습니다.
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()
}
}
}
어느 정도 틀이 나타났습니다. :)
이미지 넣기
이제 이 사다리꼴 안에 이미지를 넣어주죠.
// 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
수정자를 사용하여 테두리 효과를 주고 offset
을 slider
를 통해 움직이도록 해주겠습니다. 그 아래에는 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 넣기
Button
에 withAniamtion
을 사용하여 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
에는 get
과 set
이 와줘야겠죠?
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
이 바뀌도록 해주겠습니다.
우선 만들어뒀던 Slider
와 Button
을 주석 처리하고 시작하겠습니다.
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효과와 Text
의 offset
효과를 주석처리해주고 아래와 같이 수정해줍니다.
VStack(spacing: 50) {
...
Trapezium(offset: 0.3, corner: .topLeft)
.edgesIgnoringSafeArea(.bottom)
}
이제 아래 Trapezium
에 opacity
(투명도) 와 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)
}
}
}
body
의 ZStack
에 .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
가 제일 뒤로 에 배치되어 있는 것이죠. Image
를 ZStack
하단으로 내려주겠습니다.
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 👇🏻
'PROJECT > Simple' 카테고리의 다른 글
SwiftUI Project10 : 영화 캐릭터 정보 앱 #4 - 디테일뷰 (3) | 2021.04.20 |
---|---|
SwiftUI Project10 : 영화 캐릭터 정보 앱 #3 - JSON (0) | 2021.04.19 |
SwiftUI Project10 : 영화 캐릭터 정보 앱 #2 - 모델생성 (0) | 2021.04.17 |
SwiftUI Project10 : 영화 캐릭터 정보 앱 #1 (0) | 2021.04.16 |
SwiftUI Project9 : CustomTabView (geometryReader) (0) | 2021.03.26 |