-
SwiftUI - CanvasSwiftUI 2026. 9. 12. 16:11
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SwiftUI의 Canvas 컴포넌트에 대해 애플 공식 문서를 기준으로 정리해보겠습니다 🙋🏻
Intro
프레임워크: SwiftUI
지원 버전: iOS 15 / macOS 12 / watchOS 8 이상
Canvas?
일반 SwiftUI 뷰(
Circle,Rectangle,Path)를 여러 개 겹쳐 그리면, SwiftUI는 그 뷰들을 실제 뷰 트리 노드로 유지해요(retained mode).상태가 바뀌면 diffing해서 필요한 부분만 다시 그리죠.
Canvas는 이 방식을 버려요.대신 클로저 하나를 받아서, 매 프레임 그 클로저를 통째로 다시 실행해요.
클로저 안에서
GraphicsContext라는 그리기 도구에게 순서대로 명령을 내리는 방식이라 즉시 모드(immediate mode)라고 불러요.Canvas 뷰는 매 프레임 renderer 클로저 (inout GraphicsContext, CGSize) -> Void를 실행해 GraphicsContext를 통해 픽셀을 직접 그립니다.
개별 도형은 뷰 트리에 남지 않아요.
기본적인 사용법 — stroke
애플 공식 문서의 첫 예제가 아래와 같습니다.
Canvas { context, size in context.stroke( Path(ellipseIn: CGRect(origin: .zero, size: size)), with: .color(.green), lineWidth: 4) } .frame(width: 300, height: 200) .border(Color.blue)context:GraphicsContext— 여기다 대고 "그려라" 명령을 내림size: 캔버스가 차지한 실제CGSize— 이 크기에 맞춰 좌표를 계산Path(ellipseIn:): 캔버스 크기에 딱 맞는 타원 경로.stroke(_:with:lineWidth:): 경로의 윤곽선만 그림 (채우기 아님)
GraphicsContext의 핵심 규칙 — "순서가 상태다"
GraphicsContext는 그냥 그리기 함수 모음이 아니라, 자기 자신이 상태(state)를 갖는 객체예요.opacity,blendMode, clip, transform 같은 값을 바꾸면 그 이후에 실행되는 그리기 명령에만 영향을 줘요. 이미 그려진 건 안 바뀝니다.애플 공식 문서가 정확히 이걸로 예제를 들어요.
let halfSize = size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5)) context.clip(to: Path(CGRect(origin: .zero, size: halfSize))) // 1. 먼저 클립 context.fill( Path(ellipseIn: CGRect(origin: .zero, size: size)), with: .color(.green)) // 2. 그 다음 채우기절반 크기의 사각형으로 먼저 클립을 걸어두면, 그 뒤에 그리는 타원은 클립 영역 밖으로 나간 부분이 안 보여요.
반대로 fill을 먼저 하고 clip을 나중에 걸면, 클립은 "이후 연산에만" 적용되는 규칙 때문에 이미 그려진 타원엔 아무 영향이 없습니다.
context는 값 타입 — 복사해서 독립적으로 쓸 수 있다
GraphicsContext는struct예요.그래서 복사하면 상태가 통째로 복제돼요.
원본과 복사본은 서로 다른 state(clip, opacity 등)를 가질 수 있지만, 같은 레이어에 그림을 그려요. 즉, "복사본에만 클립을 걸고 그리기"가 가능해요.
var maskedContext = context // 상태를 통째로 복사 let halfSize = size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5)) maskedContext.clip(to: Path(CGRect(origin: .zero, size: halfSize))) maskedContext.fill( Path(ellipseIn: CGRect(origin: .zero, size: size)), with: .color(.green)) // 이 fill만 클립의 영향을 받음 // 원본 context는 클립이 안 걸린 상태 그대로 context.fill( Path(CGRect(origin: origin, size: halfSize)), with: .color(.blue)) // 클립 영향 없이 파란 사각형 그림이 패턴은 "특정 도형에만 마스크/필터를 걸고 싶은데, 나머지 그리기엔 영향을 주고 싶지 않을 때" 자주 씁니다.
좌표계 변환 — transform
GraphicsContext는 내부에CGAffineTransform을 갖고 있고,scaleBy/rotate/translateBy/concatenate메서드는 전부 이transform을 갱신하는 편의 함수예요.context.translateBy(x: size.width / 2, y: size.height / 2) // 원점을 중앙으로 context.rotate(by: .degrees(45)) // 45도 회전 context.scaleBy(x: 1.5, y: 1.5) // 1.5배 확대공식 문서 표현 그대로,
rotate(by:)를 호출하는 건 사실 아래 코드를 대신 해주는 슈가 문법이에요.transform = transform.rotated(by: angle.radians)이 변환들도 순서가 중요해요.
translate 후 rotate와 rotate 후 translate는 결과가 다릅니다.
앞서 설명한 "이후 연산에만 적용" 규칙이 여기서도 그대로 적용돼요.
블렌드 모드와 필터
opacity, blendMode도 같은 원리로 동작해요.
context.blendMode = .destinationOut // 이후 그리는 도형으로 배경을 "지운다" context.opacity = 0.5 // 이후 그리는 것들이 반투명해짐addFilter(_:options:)로는 이미지 처리 필터도 이후 그리기에 적용할 수 있어요.예를 들어 블러 효과는 다음처럼 씁니다.
context.addFilter(.blur(radius: 8))GraphicsContext.Filter에는 이 외에도alphaThreshold, 그리고ShadowOptions를 활용하는 그림자 필터 등 다양한 케이스가 존재합니다.SwiftUI 뷰를 통째로 그리기 — symbols
Canvas가 효율적인 이유 중 하나는, 진짜 SwiftUI 뷰를 "드로잉 재료"로 등록해서 원하는 위치에 여러 번 찍어낼 수 있다는 거예요.Canvas { context, size in for i in 0..<20 { if let symbol = context.resolveSymbol(id: i) { let point = CGPoint(x: Double(i) * 15, y: 50) context.draw(symbol, at: point, anchor: .center) } } } symbols: { ForEach(0..<20) { i in Circle() .fill(.blue) .frame(width: 10, height: 10) .tag(i) // resolveSymbol(id:)에서 이 id로 찾음 } }symbols클로저에 등록된 뷰들은.tag(_:)로 붙인 id를 통해resolveSymbol(id:)로 꺼내 쓸 수 있어요.이렇게 꺼낸
ResolvedSymbol은 해상도 독립적이라, 같은 심볼을 수십·수백 번 다른 위치에 찍어도 성능이 안정적이에요.파티클, 포인트 클라우드, 커스텀 차트 마커 같은 걸 그릴 때 정확히 이 패턴을 씁니다.
텍스트와 이미지 — resolve
Text나 Image도 바로는 못 그리고, 먼저 환경(폰트, 색상 스킴 등)에 맞춰 "고정"시켜야 해요.
Canvas { context, size in let resolvedText = context.resolve(Text("Hello").font(.title)) context.draw(resolvedText, at: CGPoint(x: size.width/2, y: 20), anchor: .top) }resolve(_:)가Text/Image를 각각ResolvedText/ResolvedImage로 바꿔주는데, 이 과정에서context.environment(디스플레이 해상도, 컬러 스킴 등)를 참조해요.withCGContext
정말 세밀한 제어가 필요하면
withCGContext(content:)로 진짜 Core GraphicsCGContext를 빌려서 그릴 수도 있어요.GraphicsContext가 지원 안 하는 연산이 필요할 때 쓰는 탈출구예요.언제 Canvas를 써야 하나 — 공식 문서의 트레이드오프
애플 문서가 명시적으로 경고하는 부분이에요.
Canvas doesn't offer interactivity or accessibility for individual elements, including for views that you pass in as symbols.
즉,
symbols로 진짜Button을 넣어도 탭 이벤트가 안 먹고, VoiceOver도 개별 요소를 인식 못 해요.대신 다음 기준으로 판단하면 됩니다.
- 동적 데이터가 많고, 도형 개수가 유동적인 복잡한 드로잉일수록 유리 (뷰 트리 오버헤드가 없으니까)
- 텍스트 위주이거나 사용자와 상호작용해야 하는 UI는 Canvas가 아니라 일반 SwiftUI 뷰로
Conclusion
Canvas는 SwiftUI 뷰 트리를 포기하는 대신, 매 프레임GraphicsContext에 명령을 순서대로 내려서 즉시 픽셀을 그리는 고성능 드로잉 도구예요.상태(clip/opacity/transform/filter)는 항상 "이후 연산에만" 적용된다는 규칙 하나만 기억하면, 나머지 API는 다 이 규칙의 응용이라고 볼 수 있습니다 🙌
References
Canvas | Apple Developer Documentation
A view type that supports immediate mode drawing.
developer.apple.com
GraphicsContext | Apple Developer Documentation
An immediate mode drawing destination, and its current state.
developer.apple.com
'SwiftUI' 카테고리의 다른 글
SwiftUI - TipKit (1) 2026.10.03 What's new in SwiftUI (feat. WWDC 2026) (1) 2026.06.29 SwiftUI View hierarchy Debugging (1) 2025.12.13 SwiftUI Preview - Thunk (1) 2025.11.29 SwiftUI's diffing (5) 2025.09.27