-
[SwiftUI] 4. Drawing Paths and ShapesiOS Dev/SwiftUI 2023. 4. 30. 17:04반응형
https://developer.apple.com/tutorials/swiftui/drawing-paths-and-shapes
Drawing Paths and Shapes | Apple Developer Documentation
Users receive a badge whenever they visit a landmark in their list. Of course, for a user to receive a badge, you’ll need to create one. This tutorial takes you through the process of creating a badge by combining paths and shapes, which you then overlay
developer.apple.com
이제 path와 shapes를 사용해서 다양한 뷰를 구성해보자.
Section 1. Create Drawing Data for a Badge View
Views/Badges 안에 HexagonParameters.swift 파일을 생성하자. 이번 챕터동안 생성되는 파일은 전부 Badges 그룹 안에 들어간다.
다음과 같이 HexagonParameters.swift 파일을 완성한다.
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) ) ] }
이렇게 코드로 쓰여있으면 이 도형이 어떤 도형인지 알기 어렵다. 이 코드가 의미하는 바를 그림으로 살짝 그려봤다.
먼저 하나의 segment는 line, curve, control로 이루어져 있다. 선은 line -> control -> curve (-> line -> control -> curve) 순서대로 지나간다. line까지는 직선이 그려지고, line부터 curve까지는 커브가 그려지는데, 곡선은 control을 지나가는 방식으로 구현된다. 이렇게 6개의 segment를 두어서 육각형을 표현했다.
중간에 adjustment는 위아래로 벌어지는 정도를 의미한다. (맨 아래와 맨 위 세그먼트는 제외)
Section 2. Draw the Badge Background
다음은 BadgeBackground.swift 파일을 생성한다.
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 ) ) HexagonParameters.segments.forEach { segment in path.addLine( to: CGPoint( x: width * segment.line.x, y: height * segment.line.y ) ) } } .fill(.black) } } struct BadgeBackground_Previews: PreviewProvider { static var previews: some View { BadgeBackground() } }
이렇게 생성해두고 preview를 보면,
저런 모양이 생긴다. 이상하다 생각할 수 있는데, 이전 그림에서 마지막 세그먼트의 커브 = 시작점이라고 했다. 그 시작점부터 각 세그먼트의 line 부분만 선으로 이으면 저런 모양이 된다.
이제 addQuadCurve 함수를 사용해서 curve와 control요소들을 사용하고, GeometryReader를 통해 뷰 사이즈를 동적으로 결정할 수 있도록 한다.
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)) / 2.0 width *= xScale path.move( to: CGPoint( x: width * 0.95 + xOffset, y: height * (0.20 + HexagonParameters.adjustment) ) ) HexagonParameters.segments.forEach { segment in path.addLine( to: CGPoint( x: width * segment.line.x + xOffset, y: height * segment.line.y ) ) path.addQuadCurve( to: CGPoint( x: width * segment.curve.x + xOffset, 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() } }
Geometry Reader는 무엇일까?
Geometry Reader를 사용하면 하위 뷰에서 해당 Geometry Reader 영역 정보를 알 수 있다. 현재 뷰 크기 등을 알려고할 때 유용하게 쓰인다.
/// A container view that defines its content as a function of its own size and /// coordinate space. /// /// This view returns a flexible preferred size to its parent layout. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @frozen public struct GeometryReader<Content> : View where Content : View { public var content: (GeometryProxy) -> Content @inlinable public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) /// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required ``View/body-swift.property`` property. public typealias Body = Never }
최상단에 GeometryReader는 자신의 사이즈와 coordinate space를 함수형으로 정의하는 컨테이너 뷰라고 써있다.
정의 부분을 보자. 처음 init될 때 @ViewBuilder로 content를 받고, 이는 @escaping 클로져로, GeometryProxy를 인자로 받아서 Content를 리턴하는 클로져이다. 즉, GeometryProxy를 입력받아서 Content를 리턴하면 된다.
GeometryProxy는 뭘까?
/// A proxy for access to the size and coordinate space (for anchor resolution) /// of the container view. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct GeometryProxy { /// The size of the container view. public var size: CGSize { get } /// Resolves the value of `anchor` to the container view. public subscript<T>(anchor: Anchor<T>) -> T { get } /// The safe area inset of the container view. public var safeAreaInsets: EdgeInsets { get } /// Returns the container view's bounds rectangle, converted to a defined /// coordinate space. public func frame(in coordinateSpace: CoordinateSpace) -> CGRect }
먼저, proxy는 대리인을 뜻한다. GeometryProxy는 단순하게 GeometryReader에서 사이즈와 coordinate space 등을 접근할 수 있게 해주는 대리인이다. 중간에 subscript는 [ ] (대괄호)를 의미한다. 여기 안에 Anchor를 집어넣으면 값을 알 수 있다.
CoordinateSpace를 활용하여 global, local, named(이름) 영역에 대한 frame을 알 수 있다.
Section 3. draw the Badge Symbol
이 파일의 압축을 풀어서 나온 폴더 자체를 Assets 안에 기존에 있던 AppIcon을 지우고 넣어준다.
다음, BadgeSymbol.swift 파일을 만든다.
import SwiftUI struct BadgeSymbol: View { 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) ]) } } } } struct BadgeSymbol_Previews: PreviewProvider { static var previews: some View { BadgeSymbol() } }
width : 전체 너비
heigth : 전체 높이
spacing : 가장 위쪽 (y가 0에 가까운 점) 점이 0에서부터 떨어진 거리 & 가장 아래쪽 점이 topHeight로부터 떨어진 거리
middle : x 중앙
topWidth : 가장 오른쪽 점 x좌표 (y가 가장 큰 점)
topHeight : 가장 아래 점 y좌표 (y가 가장 큰 점)이런 모양이 나온다. 아래쪽 모양도 마저 그리면,
import SwiftUI 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) ]) } .fill(Self.symbolColor) } } } struct BadgeSymbol_Previews: PreviewProvider { static var previews: some View { BadgeSymbol() } }
이렇게 된다.
RotatedBadgeSymbol.swift 파일을 새로 만들어서 다음 코드를 채운다.
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)) } }
주어진 각도만큼 살짝 틀어진 것을 볼 수 있다.
Section 4. Combine the Badge Foreground and Background
Badge.swift 파일을 하나 생성하자.
import SwiftUI struct Badge: View { var badgeSymbols: some View { ForEach(0..<8) { index in 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() } }
여기서 ZStack이 나온다. ZStack은 하위 뷰들을 쌓아올리는 뷰를 말한다.
/// A view that overlays its subviews, aligning them in both axes. /// /// The `ZStack` assigns each successive subview a higher z-axis value than /// the one before it, meaning later subviews appear "on top" of earlier ones. (중략) @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @frozen public struct ZStack<Content> : View where Content : View { /// Creates an instance with the given alignment. /// /// - Parameters: /// - alignment: The guide for aligning the subviews in this stack on both /// the x- and y-axes. /// - content: A view builder that creates the content of this stack. @inlinable public init(alignment: Alignment = .center, @ViewBuilder content: () -> Content) /// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required ``View/body-swift.property`` property. public typealias Body = Never }
순서대로 뷰에 쌓인다고 생각하면 되고, @ViewBuilder content에 들어오는 뷰에 대해 이전에 들어온 뷰가 나중에 들어온 뷰의 아래에 깔린다.
Check Your Understanding
답은 2 1 3
반응형'iOS Dev > SwiftUI' 카테고리의 다른 글
[SwiftUI] 6. Composing Complex Interfaces (0) 2023.05.04 [SwiftUI] 5. Animating Views and Transitions (0) 2023.05.04 [SwiftUI] 3. Handling User Input (0) 2023.03.30 [SwiftUI] 2. Building Lists and Navigation (0) 2023.03.28 [SwiftUI] 1. Creating and Combining Views (0) 2023.03.20