반응형

Raywenderlich 사이트의 강좌 번역본입니다.

원문 : https://www.raywenderlich.com/27594491-using-timelineview-and-canvas-in-swiftui

Using TimelineView and Canvas in SwiftUI

자료 다운로드

호환성 버젼 : Swift 5.5, iOS 15, Xcode 13

모든 버젼의 SwiftUI 은 새로운 기능과 새로운 뷰를 제공합니다. 일부는 UIKit이나 AppKit의 추가 기능으로 SwiftUI를 확장하고 다른 부분은 새롭고 고유한 기능을 추가합니다. iOS 15와 해당 운영체제와 함께 제공된 세번째 버젼의 SwiftUI는 캔버스 뷰(canvas view)에 고성능 그리기를 도입했습니다. 또한 TimelineView라고 하는 뷰를 업데이트하는 새로운 시간 기반(time-based) 메소드를 도입했습니다.

이 튜토리얼에서, 다음을 배우게 될 것입니다.

  • Canvas와 TimelineView.
  • 시계 형식의 애니메이션 그래픽을 만들기 위해 이 둘을 조합

시작할 시간입니다.

시작하기(Getting Started)

튜토리얼의 자료 다운로드를 클릭해서 시작 프로젝트를 다운로드 하세요. Xcode에서 starter 디렉토리에 있는 프로젝트를 열어주세요. 빌드하고 실행하세요.

여러 위치를 추가하고 각 위치의 현재 시간을 보여주는 세계 시간 앱을 보게 될 것입니다. 목록에서 도시를 추가, 삭제, 이동할수 있는 뷰로 이동하기 위해 맵 아이콘을 탭합니다. 위치를 추가하기 위해서, 위치의 이름을 입력할 수 있는 검색 필드가 보일때까지 뷰를 아래로 내려줍니다. 메인 뷰로 가면, 각 도시의 시간대와 첫번째 도시와 다른 것들간의 시간 차이를 볼 수 있습니다.

각 도시 아래에 낮과 밤과 관련된 현지 시간을 보여주는 선(line)이 있습니다. 도시를 탭하면 해당 도시에 요약 정보를 보게 됩니다.

앱을 살펴보는 동안에, 버그를 발견할 수 있고, 버그를 해결할 것입니다. 앱을 실행하고 1분이 바뀔때까지 기다리세요. 앱에 보여지는 시간은 처음 뷰가 보인 시간 그대로 입니다. 도시를 탭해서 다른 뷰로 이동하세요. 앱은 이제 업데이트 합니다. 사용자가 뷰를 보는 동안에는 앱은 시간이 지남에 따라 뒤쳐집니다.

다음 섹션에서, 이 문제를 해결하기 위해 TimelineView를 사용할 것입니다.

TimelineView 사용하기(Using Timeline View)

ContentView.swift를 열고 NavigationView를 찾으세요, NavigationView의 전체 컨텐츠를 TimelineView의 안쪽에 넣으세요(embed).

TimelineView(.everyMinute) { context in
  // The entire content of NavigationView closure goes here.
}

위에서 시킨대로 하면, NavigationView는 다음과 같이 보입니다.

NavigationView {
  TimelineView(.everyMinute) { context in
    List(locations.locations) { ... }
    .onAppear { ... }
    .onChange(of: locations.locations) { ... }
    .navigationTitle("World Clock")
    .toolbar { ... }
  }
}

TimelineSchedule 프로토콜에서 구현하는 값을 사용해서 지정된 시간 스케쥴에 따라 TimelineView는 업데이트 합니다. 이 프로토콜에는 몇가지 정적(static) 타입을 기본으로 제공(built-in)하고, 그중에 하나가 everyMinute 매개변수 입니다. 이는 매 분마다 뷰를 업데이트 해야 하도록 지정합니다.

LocationView의 currentDate 매개변수를 다음에 오는 것으로 바꾸세요.

 currentDate: context.date,

TimelineView의 클로져 안으로 전달된 context는 2개의 프로퍼티를 포함하고 있습니다. 하나는 여기에서 사용한 date 프로퍼티이고 업데이트 날짜가 포함되어 있습니다. 또한, 타임라인 뷰를 업데이트 하는 속도(rate)를 정의하는 cadence프로퍼티를 포함하고 있습니다.

앱을 실행하고 초기 뷰의 분(minute)이 바뀔때까지 기다립니다. 그 시간에, 핸드폰의 시계와 동기화된 업데이트한 뷰를 보게 될 것입니다.

다음 섹션에서, SwiftUI 3.0에서 가능한 새로운 그리기를 살펴볼것입니다. - canvas

캔버스 뷰 소개(Introducing the Canvas View)

새로운 캔버스 뷰는 SwiftUI에서 고성능의 즉시 그리기 모드를 지원합니다. 이 뷰는 GraphicsContext 타입의 그래픽 컨텍스트(context)와 캔버스의 크기에 맞게 클로져(closure)로 포함된 캔버스의 치수인 CGSize 프로퍼티 모두 제공합니다.

DaytimeGraphicsView.swift를 여세요. 이 뷰는 SwiftUI의 그리기 뷰를 사용해서, 제공된 날짜와 위치에 대한 낮과 밤 이미지를 만듭니다. 이번 섹션에서, 캔버스를 대신 사용하도록 수정할 것입니다. GeometryReader를 사용해서
포함한 뷰의 크기에 맞도록 조정합니다. 캔버스는 클로져에 매개변수중 하나로 이런 정보를 제공하고 있씁니다. GeometryReader를 다음에 오는 것으로 교체하세요.

Canvas { context, size in

캔버스는 GeometryReader에 대한 대체품이 아니지만, 이번 경우에는, 같은 정보를 제공합니다. Canvas에서의 size 매개변수는 GeometryReader에서 얻은 것과 같은 수치를 가집니다.

클로져 안쪽의 proxy.size.width를 참조하는 4개를 size.width으로, proxy.size.height를 참조하는 하나를 size.height로 변경하세요.

아직 끝나지 않았습니다.

그 뷰는 다음에 대한 각 3개의 사각형을 그립니다.

  • 검정색의 해 뜨기전(Pre-dawn)
  • 파란색의 낮 시간(Daytime)
  • 다시 검정색의 해가 진 후(After-sunset)

context는 캔버스의 그리는 영역을 나타냅니다. Core Graphics가 익숙하다면 쉽게 느껴질 것입니다. 캔버스 그리기 모델은 Core Graphics을 기반으로 하고 같은 메소드와 구조체를 많이 사용합니다.

첫번째 Rectangle 뷰를 다음에 오는 코드로 교체하세요.

// 1
let preDawnRect = CGRect(
  x: 0,
  y: 0,
  width: sunrisePosition,
  height: size.height)
// 2
context.fill(
  // 3
  Path(preDawnRect),
  // 4
  with: .color(.black))

이 코드는 다음 단계에서 해 뜨기 전(pre-dawn) 부분(portion)을 그립니다.

  1. 캔버스에서 사각형을 그리기 위해, 사각형의 위치와 크기를 정의한 CGRect 구조체를 만듭니다. SwiftUI 뷰와는 다르게, 반드시 모든 수치를 지정해야하고 사각형이 뷰를 가득채울것이라 가정할 수 없습니다. Canvas내부의 수치와 축은 Core Graphics와 같은 규칙을 따릅니다. 원점이 x 와 y 모두 0이면 좌측 상단 모서리 입니다. x 값이 증가하면 오른쪽으로 이동하고, y 값이 증가하면 아래로 이동합니다. 여기에서 좌표는 직사각형의 좌측 상단 모서리를 그리기 화면의 좌측 상단 모서리에 배치합니다. 이전에 Rectangle 뷰에 적용된 프레임의 넓이는 사각형의 넓이와 같고 size.height는 높이 매개변수로 사용합니다. 이렇게 하면 직사각형이 캔버스의 전체 높이를 채우게 됩니다.
  2. 가득찬 도형을 그리이 위해서 context에서fill(_:width)을 호출합니다.
  3. 그리기 위한 객체를 지정하기 위해서, 사각형을 첫번째 매개변수로 사용해서 경로(path)를 만듭니다. 채울 영역을 정의합니다.
  4. 채워진 도형을 그리기 위해 검정색을 전달합니다. SwiftUI 색상 정의를 사용하는 것을 주의하세요.

앱을 실행해서, 현재 나오는 경고는 무시하고, 해 뜨기 전(pre-dawn) 부분만 그려지는 것을 보게 될 입니다.

다음은, 경고를 처리 할 것입니다.

캔버스 안쪽 그리기(Drawing Inside a Canvas)

캔버스에 대한 클로져가 뷰 빌더(view builder)가 아니기 때문에, 캔버스 안쪽의 Rectangle 뷰는 그려지지 않습니다. 이는 대부분의 SwiftUI 클로져와 다릅니다. Canvas안에서 직접 SwiftUI를 사용하는 능력을 읽은 대신에, SwiftUI와 혼합해서 사용할 수 있는 강력한 Core Graphics API를 사용할 수 있습니다.

주의
Core Graphics에 대해서 배우려면 Core Graphics Tutorial: Getting Started Core Graphics Tutorial: Lines, Rectangels and Gradients를 보세요.

남은 2개의 사각형 뷰를 다음에 오는 코드로 교체하세요.

let dayRect = CGRect(
  x: sunrisePosition,
  y: 0,
  width: sunsetPosition - sunrisePosition,
  height: size.height)
context.fill(
  Path(dayRect),
  with: .color(.blue))

let eveningRect = CGRect(
  x: sunsetPosition,
  y: 0,
  width: size.width - sunsetPosition,
  height: size.height)
context.fill(
  Path(eveningRect),
  with: .color(.black))

이전 처럼, 사각형은 캔버스의 수직공간을 가득 채웁니다. 이전에 Rectangel 뷰에 적용된 offset을 사각형의 x좌표로 이동합니다. 이전 처럼, Rectangle에 적용된 frame의 넓이는 각 사각형의 넓이가 됩니다.

이 뷰의 마지막 코드는 그래프에서 자정과 정오에 노란색 선을 그리는 것입니다. 현재 ForEach 뷰를 다음으로 교체합니다.

// 1
for hour in [0, 12] {
  // 2
  var hourPath = Path()
  // 3
  let position = Double(hour) / 24.0 * size.width
  // 4
  hourPath.move(to: CGPoint(x: position, y: 0))
  // 5
  hourPath.addLine(to: CGPoint(x: position, y: size.height))
  // 6
  context.stroke(
    hourPath,
    with: .color(.yellow),
    lineWidth: 3.0)
}

코드 동작은 다음과 같습니다.

  1. 뷰 빌더(view builder) 안에 있지 않으므로, 자정을 0으로 표현하고 정오를 12로 표현하는 정수형 모음(collection)으로 for-in 반복문을 사용합니다.
  2. 캔버스 안쪽에 추가할 빈 경로(path)를 만듭니다.
  3. 선의 수평 좌표를 결정하기 위해 시간을 Double로 변환하고나서 해당 시간을 하루의 일부를 표현하기 위해 24.0으로 나눕니다. 그리고나서 해당 시간을 표현하는 수평 위치를 얻기 위해 캔버스의 넓이 부분을 곱해줍니다.
  4. move(to:)는 경로(path)를 추가하지 않고 현재 위치의 경로를 이동합니다. 3단계에서 현재 위치를 수평 위치로 이동하고 뷰의 맨 위로 이동합니다.
  5. 현재 위치에서 경로(path)에 지정된 위치로 addLine(to:)은 선을 추가합니다. 이 위치는 뷰 하단의 수평 좌표와 같습니다.
  6. context에서 stroke(_:with:lineWidth:)를 사용해서 경로(path)를 채우지 않고 그려줍니다. 선이 돋보이도록 노란색과 넓이를 3으로 지정합니다.

빌드하고 실행하세요. 이전과 같은 뷰를 보게될것이지만, SwiftUI 도형 뷰 대신에 Canvas를 사용합니다.

캔버스를 사용하는 주된 이유는 성능입니다. 그라데이션(gradients) 또는 그릴것이 많은 복잡한 그리기 할때에는 SwiftUI 뷰를 사용하는 것보다 훨씬 더 나은 성능을 보일것입니다. 또한, 캔버스 뷰는 Core-Graphics-enabled wrapper 사용하는 것을 포함해서 Core Graphics와의 호환성을 제공합니다. UIView용으로 작성했고 draw(_:)으로 랜더링된(rendered) 사용자정의 컨트롤 처럼 Core Graphics를 사용해서 만든 기존코드가 있는 경우에, 수정없이 캔버스의 안쪽에 넣을 수 있습니다.

캔버스 뷰에서 무언가 잊은게 있나요? 예제에서 봤던 것처럼, 캔버스는 좀 더 자세한(verbose) 코드가 필요합니다. 캔버스는 단일 요소처럼 존재하고, SwiftUI 뷰 처럼 구성요소를 개별적으로 지정하고 수정할 수 없습니다. 캔버스에 onTapGesture(count:perform:)을 추가할 수 있지만, 캔버스에서 경로(path)는 추가할 수 없습니다.

또한 캔버스는 하나 이상의 함수를 제공합니다. TimelineView과 조합해서 애니메이션을 실행할 수 있습니다. 이 튜토리얼의 나머지에서 앱의 아날로그 시계를 만드는 것을 살펴볼 것입니다.

시계 표면 그리기(Drawing a Clock Face)

TimelineView는 뷰를 정기적으로 업데이트 하는 방법을 제공하는 반면 캔버스 뷰는 고성능의 그래픽을 만드는 방법을 제공합니다. 이번 섹션에서, 상세 페이지에서 선택된 도시의 시간을 애니메이션으로 보여주는 아날로그 시계를 만들 것입니다.

AnalogClock.swift를 여세요. 뷰의 본문(body)을 다음에 오는 코드로 교체하세요.

Canvas { gContext, size in
  // 1
  let clockSize = min(size.width, size.height) * 0.9
  // 2
  let centerOffset = min(size.width, size.height) * 0.05
  // 3
  let clockCenter = min(size.width, size.height) / 2.0
  // 4
  let frameRect = CGRect(
    x: centerOffset,
    y: centerOffset,
    width: clockSize,
    height: clockSize)
}

이 코드는 Canvas 뷰를 정의하고 뷰의 크기를 기반으로 시계 표면의 크기를 계산합니다.

  1. 먼저 캔버스의 넓이와 높이 간에 더 작은 치수를 결정합니다. 이 값에 0.9를 곱해서 작은 치수의 90%를 채우도록 표면의 크기를 설정합니다.
  2. 캔버스 중심에 시계를 놓고, 더 작은 치수를 결정하고 0.05를 곱해서 1단계에서 남은 10%의 절반을 결정합니다. 이 값은 시계 표면을 포함하는 직사각형의 좌측 상단 모서리가 될 것입니다.
  3. 더 작은 치수를 2로 나누어서 시계의 중심 좌표를 결정합니다. 시계가 대칭적(symmetrical)이기에 수평과 수직 중심 위치 모두를 얻을 수 있습니다. 이 튜토리얼 뒤에서 이 값을 사용할 것입니다.
  4. 2단계의 offset과 1단계의 크기를 사용해서 사각형을 정의 합니다. 이 사각형은 시계 표면을 감싸고 있습니다.

이제, 시계 표면을 그릴것입니다. 다음 코드를 사용해서 캔버스의 클로져를 계속 합니다.

// 1
gContext.withCGContext { cgContext in
  // 2
  cgContext.setStrokeColor(
    location.isDaytime(at: time) ? 
      UIColor.black.cgColor : UIColor.white.cgColor)
  // 3
  cgContext.setFillColor(location.isDaytime(at: time) ? dayColor : nightColor)
  cgContext.setLineWidth(2.0)
  // 4
  cgContext.addEllipse(in: frameRect)
  // 5
  cgContext.drawPath(using: .fillStroke)
}

코드 동작방식은 다음과 같습니다.

  1. 앞에서 언급했던 것 처럼, Canvas 뷰는 Core Graphics 그리기를 지원합니다. 하지만, 캔버스 클로져 안쪽에 있는 gContext 매개변수는 여전히 Core Graphics를 감싼 래퍼(wrapper)입니다. Core Graphics를 사용하려면 GraphicsContext.withCGContext(content:)를 호출합니다. 이는 Core Graphics context를 만들고 해당 클로져에 전달하며, 모든 Core Graphics 코드에서 사용할 수 있습니다. 캔버스 또는 Core Graphics context에서 만든 그래픽 상태의 변경사항은 클로져가 끝날때까지 유지됩니다.
  2. 지정된 시간에 하루를 기준으로 선의 색상을 설정하기 위해서 Core Graphics의 setStrokeColor(_:)를 사용합니다. 낮(daytime)을 검정색으로 설정하고, 밤(night)은 흰색을 설정합니다. Core Graphics를 호출하므로 CGColor을 사용합니다.
  3. 그리고나서, dayColor와 nightColor 프로퍼티를 사용해서 채우기 색을 설정합니다. 또한, 선의 넓이를 2포인트로 설정합니다.
  4. 시계 표면을 그리기 위해서, 타원의 모서리를 정의하는 이전 사각형을 사용한 Core Graphics context에서 addEllipse(in:)을 호출합니다.
  5. 마지막으로, 4단계의 타원으로 구성된 경로(path)로 뷰에 그립니다.

시계를 보기 위해서, LocationDetilsView.swift를 여세요. 다음과 같이 TimelineView 안쪽으로 VStack으로 감쌉니다.

TimelineView(.animation) { context in
  // Existing VStack
}

뷰를 빨르게 업데이트 하는것이 가능한 animation 정적 식별자를 사용해서 TimelineView를 만듭니다. 두번째 Text 뷰에서 Date() 참조하는 부분을 context.date으로 변경합니다.

Text(timeInLocalTimeZone(context.date, showSeconds: showSeconds))

이제, 기존 텍스트 필드 뒷부분, 공간 앞에 다음에 오는 코드를 추가하세요.

AnalogClock(time: context.date, location: location)

뷰에서 새로운 아날로그 시계가 보일것입니다. 빌드하고 실행하고, 상세 페이지를 보기 위해 도시들 중 하나를 탭하세요. 새로운 시계 표면을 다음꽈 같이 보일것입니다.

검정색이나 파란색인 단순한 원을 보게 될 것입니다. 다음으로, 표시된 시간을 알수 있도록 정적 눈금(tick)을 추가할 것입니다.

눈금 그리기(Drawing Tick Marks)

눈금 표시는 시계 표면의 주위를 12개의 동일한 간격으로 표시하고 5분 간격을 나타냅니다. 이는 시계에 표시되는 시간을 더 잘 알 수 있도록 도와줍니다.

여기에서 삼각법(trigonometry)가 많이 필요하지만, 걱정하지 마세요! 필요한 모든 단계를 할 것입니다.

AnalogClock.swift로 돌아가서 뷰의 본문 위에 다음에 오는 새로운 메소드를 추가하세요.

func drawTickMarks(context: CGContext, size: Double, offset: Double) {
  // 1
  let clockCenter = size / 2.0 + offset
  let clockRadius = size / 2.0
  // 2
  for hourMark in 0..<12 {
    // 3
    let angle = Double(hourMark) / 12.0 * 2.0 * Double.pi
    // 4
    let startX = cos(angle) * clockRadius + clockCenter
    let startY = sin(angle) * clockRadius + clockCenter
    // 5
    let endX = cos(angle) * clockRadius * 0.9 + clockCenter
    let endY = sin(angle) * clockRadius * 0.9 + clockCenter
    // 6
    context.move(to: CGPoint(x: startX, y: startY))
    // 7
    context.addLine(to: CGPoint(x: endX, y: endY))
    // 8
    context.strokePath()
  }
}

시계의 구성요소를 여러가지 메소드로 분리하면 어수선한 것을 줄이는데 도움이 됩니다. 뷰의 본문에서 계산된 크기(size)와 간격(offset) 을 Core Graphics context에서 메소드로 전달할 것입니다. 다음은 메소드를 자세히 살펴본 것입니다.

  1. 시계 표면의 크기를 2로 나누어서 시계 표면의 중심 위치를 계산하고 간격을 더해줍니다.
  2. 다음으로, 각 눈금에 대해서 0에서 11까지 정수를 사용해서 반복합니다. 다시한번 강조하지만, 뷰 빌더(view builder)에 있는 것이 아니기에 ForEach 대신에 for-in 반복문을 사용합니다.
  3. 시계 표면을 12개의 같은 조각(segments)으로 나눕니다. 각 조각은 현재 시간(hourMark)를 표시하는 전체 원의 지름의 비율(fraction)을 계산합니다. Swift에서 삼각법(Trigonometric) 계산은 라디안(radians)을 사용합니다. 일반적으로(conventional) 360도는 2π 라디안(radians)과 같습니다. 원의 현재 비율과 동일한 라디안 수를 결정하기 위해서, 비율(fraction)에 2을 곱하고 Double.pi 상수를 곱해줍니다.

주의
기술적으로, 반시계 방향으로 1/4각도로 이동(shift)하기 위해서 π/2를 빼야(subtract)합니다. 이러한 조정이 없으면, 0도는 위쪽이 아니라 오른쪽을 향하게 될 것입니다. 이 눈금의 경우에, 차이가 없지만, 숫자를 표시하기 위해 변경하면 잘못된 위치에 나타납니다.

  1. 여기에서 삼각법(trigonometry)를 사용하지만, 당황하지 마세요. 알아야할 각도는 코사인(cosine)이 주어진 각도에서 점을 기준으로 전체 반지름의 수평 부분의 위치를 제공한다는 것입니다. 수직 위치에 대해서 동일한 정보를 제공합니다. 시계 표면의 중심를 중심으로 같은 거리에 점(points)을 위치하길 원하기에, 1 단계에서 계산된 간격(offset)을 더해줍니다. 3단계에서 계산된 각도에 대한 x 와 y 포인트(points)를 제공합니다.
  2. 시계 표면의 안쪽으로 점(points)을 가져오기 위해서 반지름에 0.8를 곱하는 것을 제외하면 4 단계와 동일합니다. 눈금 표시는 표면 안쪽에서 이 위치(point)까지 이어집니다.
  3. 계산된 점(points)을 사용해서 4 단계의 시작 점(point)으로 context를 이동합니다.
  4. 다음으로, 5 단계의 종료 점(point)까지 선을 추가합니다.
  5. 캔버스에 경로(path)를 따라 선을 그려줍니다.

이제, Core Graphics context가 있는 클로져의 하단에 있는 메소드에 대한 호출을 추가합니다.

drawTickMarks(
  context: cgContext,
  size: clockSize,
  offset: centerOffset)

앱을 실행하고 눈금이 있는 시계 표면을 보기 위해서 아무 도시나 탭 합니다.

눈금 표시가 있으면, 이제 시계 바늘을 추가할 수 있습니다.

시계 바늘 그리기(Drawing Clock Hands)

먼저 3개의 시계 바늘 모두를 그리는 재사용가능한 메소드를 만들 것입니다. drawTickMarks(context:size:offset:) 뒤에 다음에 오는 코드를 추가하세요.

func drawClockHand(
  context: CGContext,
  angle: Double,
  width: Double,
  length: Double
) {
  // 1
  context.saveGState()
  // 2
  context.rotate(by: angle)
  // 3
  context.move(to: CGPoint(x: 0, y: 0))
  context.addLine(to: CGPoint(x: -width, y: -length * 0.67))
  context.addLine(to: CGPoint(x: 0, y: -length))
  context.addLine(to: CGPoint(x: width, y: -length * 0.67))
  context.closePath()
  // 4
  context.fillPath()
  // 5
  context.restoreGState()
}

이 메소드는 지정된 각도, 넓이, 길이로 시계 바늘(clock hand)을 그려줍니다. 넓이와 길이를 변경해서 시침(hour), 분침(minute), 초침(second)을 다르게 만듭니다. 다음은 이 메소드의 동작 방법입니다.

  1. saveGState() 는 현재 그래픽 상태의 복사본을 스택에 넣습니다(pushes). 나중에 스택에서 현재 상태를 복원할 수 있습니다. 상태를 저장해서 이 메소드를 사용하는 동안에 변경한 사항을 쉽게 복원할 수 있습니다.
  2. 눈금을 만들때, 삼각법(trigonometry)를 사용해서 선의 위치를 계산했었습니다. 여러개의 선이나 도형을 보여주기 위한 상황이 지루할 수 있습니다. rotate(by:)은 라디안 단위로 지정된 각도만큼 다음에 오는 모든 경로(path)를 회전합니다. 이 메소드를 사용해서, 시계 바늘을 수직으로, 원하는 각도로 나타나도록 연산(math) 처리를 할 수 있습니다. 어려운 일은 컴퓨터에게 맡기세요!
  3. 캔버스의 중심로 이동합니다 - 질문은 잠깐 멈추세요. 지정된 넓이의 선을 왼쪽과 위쪽으로 전체 길이의 2/3까지 그립니다. 중심의 오른쪽으로 첫번째 선을 대칭하기 전에 위쪽으로 전체 길이만큼 중심으로 계속됩니다. closePath()은 중심에 있는 초기 점에 선을 추가합니다.
  4. 방금 정의한 모양(shape)을 현재 채우기 색으로 채워줍니다.
  5. 1 단계에서 저장했던 그래픽 상태를 복원합니다. 2 단계에서 회전했던 각도 변경한 것을 되돌립니다.

이제 시계 바늘을 그리는 메소드가 있고, 시계 바늘을 그릴수 있습니다. drawTickMarks(context:size:offset:) 호출하는 바로 뒤의 Core Graphics 클로져의 끝에 다음에 오는 코드를 추가하세요.

// 1
cgContext.setFillColor(location.isDaytime(at: time) ?
  UIColor.black.cgColor : UIColor.white.cgColor)
// 2
cgContext.translateBy(x: clockCenter, y: clockCenter)
// 3
let angle = clockDecimalHourInLocalTz / 12.0 * 2 * Double.pi
let hourRadius = clockSize * 0.65 / 2.0
// 4
drawClockHand(
  context: cgContext,
  angle: angle,
  width: 7.5,
  length: hourRadius)

채우기 색을 바꾸고 시계 바늘에 필요한 정보를 계산합니다. 자세한 내용은 다음과 같습니다.

  1. 현재 채우기 색상을 선의 색상과 일치하도록 바꿔줍니다 - 낮은 검정색이고 밤은 흰색.
  2. 캔버스의 중심에 위치하기 위해, 이전에 그릴때의 눈금 표시와 시계 표면에 대한 간격(offset)을 추가합니다. 위의 rotate(by:)처럼 그래픽 상태를 변경할 수 있습니다. translateBy(x:y:)는 그리는 표면의 원점을 시계의 중심이 되는 곳으로 이동시켜줍니다. 이러한 변화는 다음에 오는 모든 그리기 동작에 영향을 미칩니다. 이러한 이동으로 drawClockHand(context:angle:width:length:)에서 원점을 사용할 수 있습니다.
  3. 주어진 시간에 대한 각도를 계산합니다. clockDecimalHourInLocalTz는 분수(fraction)를 포함하므로, 1:30은 1.5가 됩니다. 분수(fractions)를 포함해서 시계 바늘의 부드러운 움직임을 지원합니다. 회전하기 전에 시계 바늘을 수직으로 그리면서 rotate(by:)를 사용하면 수동으로 각도를 계산할때 처럼 π/2만큼 이동할 필요가 없습니다.
  4. 시계 바늘을 그려주는 메소드를 호출합니다.

앱을 실행하고 시계 표면에 시계 바늘이 표시됩니다.

이제, 다른 시계 바늘을 그리기 위해 같은 과정을 사용합니다. 시계바늘 그리는 코드 위에 다음을 추가하세요.

let minuteRadius = clockSize * 0.75 / 2.0
let minuteAngle = clockMinuteInLocalTz / 60.0 * 2 * Double.pi
drawClockHand(
  context: cgContext,
  angle: minuteAngle,
  width: 5.0,
  length: minuteRadius)

cgContext.saveGState()
cgContext.setFillColor(UIColor.red.cgColor)
let secondRadius = clockSize * 0.85 / 2.0
let secondAngle = clockSecondInLocalTz / 60.0 * 2 * Double.pi
drawClockHand(
  context: cgContext,
  angle: secondAngle,
  width: 2.0,
  length: secondRadius)
cgContext.restoreGState()

더 큰 반지름을 곱하고 좁은 넓이를 사용해서 분침을 그립니다. 그리고나서 채우기 색상을 빨간색으로 바꾸고 더 길고 좁은 초침을 그립니다. 원래 채우기 색상을 복원하기 위해 초침 그리는 주변의 그래픽 상태를 저장하고 복원합니다.

앱을 실행하고 시침과 함께 분침, 초침을 볼 수 있을 것입니다.

중심에 버튼 추가하기(Adding a Center Button)

다음으로, 시계 바늘들이 만나는 곳인 시계 표면의 중심에 작은 원을 추가할 것입니다. 초침을 그리는 코드 뒤에 다음을 추가해 주세요.

let buttonDiameter = clockSize * 0.05
let buttonOffset = buttonDiameter / 2.0
let buttonRect = CGRect(
  x: -buttonOffset,
  y: -buttonOffset,
  width: buttonDiameter,
  height: buttonDiameter)
cgContext.addEllipse(in: buttonRect)
cgContext.fillPath()

시계 표면의 5%의 지름을 계산합니다. 그리고 나서 지름의 절반인 사각형의 모서리 좌측 상단으로 이동합니다. 다음으로, 사각형으로 정의된 타원을 추가하고 채워줍니다.

업데이트된 시계 표면을 보기 위해 앱을 실행하세요.

스티브 잡스(Steve Jobs) 말했듯이, 한가지 더(One more thing)! SwiftUI와 캔버스를 통합하는 방법을 보기 위해 선택된 도시의 날짜를 보여주는 것을 추가할 것입니다.

캔버스에 SwiftUI 뷰를 함께 사용하기(Mixing SwiftUI Views Into a Canvas)

캔버스는 ViewBuilder가 아니고, 이는 SwiftUI 뷰를 직접 포함할수 없다는 것을 의미합니다. DaytimeGraphicsView.swift에서 사각형 뷰가 캔버스 메소드 호출로 변환될때까지 표시되지 않을때 알았습니다. 대신, 캔버스에 SwiftUI 뷰를 전달하고 그릴때 참조할 수 있습니다.

AnalogClock.swift를 열고 Canvas의 닫는 중괄호가 있는 줄에 다음 코드를 추가하세요.

symbols: {
  ClockDayView(time: time, location: location)
    .tag(0)
}

캔버스 뷰 초기화의 symbols 매개변수를 사용해서 SwiftUI 뷰를 전달합니다. tag(_:)를 사용해서 고유한 식별자를 각 뷰에 태그(tag)를 지정해야 합니다.

다음으로, 뷰를 참조하려면 해당 태그를 사용해야 합니다. 캔버스 클로져의 상단에 다음에 오는 코드를 추가하세요.

let dayView = gContext.resolveSymbol(id: 0)

이 코드는 식별자를 전달해서 태그된 SwiftUI 뷰를 찾습니다. 존재하는 경우에, dayView에 저장됩니다. 그렇지 않다면, 해당 메소드는 nil을 반환합니다.

뷰의 끝부분에, withCGContext(content:)의 끝난 뒤에, 시계 표면에 SwiftUI 뷰를 보여주기 위해서 다음에 오는 코드를 추가하세요.

if let dayView = dayView {
  gContext.draw(
    dayView,
    at: CGPoint(x: clockCenter * 1.6, y: clockCenter))
}

dayView를 해제(unwrap)하려고 합니다. 만약 성공한다면, 심볼(symbol)을 사용해서 캔버스에 뷰를 그리기 위해 GraphicsContext의 draw(_:at:) 메소드를 사용합니다. 원점을 변경하는 클로져를 그대로 두더라도 시계 중심의 새 위치는 유지됩니다. 따라서(Hence), 이전에 계산한 clockCenter를 사용해서 오른쪽으로 수평 위치를 이동합니다. 마지막 시계 표면을 보기 위해 앱을 실행하세요.

여기에서 어디로 가야하나요?(Where to Go From Here?)

이 튜토리얼의 자료 다운로드를 클릭해서 완성된 프로젝트를 다운로드 할 수 있습니다.

시간을 동기화한 것만이 아니라 처음부터 만든 아날로그 시계에 시간을 보여주는 앱을 만들었습니다. - 잘 했습니다!

시계 표면을 그리기 위해 사용하는 회전과 삼각법(trigonometry)의 배경지식을 더 알고 싶다면, Trigonometry for Game Programming — SpriteKit and Swift Tutorial: Part ½ Trigonometry for Game Programming — SpriteKit and Swift Tutorial: Part 2/2을 보세요.

Beginning Core Graphics 비디오 코스는 또 다른 훌륭한 자료입니다. 추가로, raywenderlich.com에는 더 많은 Core Graphics 튜토리얼이 있습니다. 그 중 몇가지는 다음과 같습니다.

반응형
Posted by 까칠코더
,