TipKit
iOS 17+ 앱에 Apple의 TipKit 프레임워크를 사용하여 기능 발견 팁, 컨텍스트 힌트 및 온보딩 코치 마크를 추가하세요. TipKit은 표시 빈도, 자격 규칙 및 지속성을 관리하여 팁이 적시에 나타나고 사용자가 기능을 학습하면 사라지도록 합니다.
목차
설정
뷰가 렌더링되기 전에 App.init에서 Tips.configure()를 한 번 호출하세요. 이렇게 하면 팁 데이터 저장소가 초기화되고 규칙 평가가 시작됩니다. 나중에 호출하면 팁 뷰가 데이터 저장소가 준비되기 전에 표시되려고 하는 경합 상태가 발생할 수 있습니다.
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
try? Tips.configure([
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
DatastoreLocation 옵션
| 옵션 | 사용 사례 |
|---|---|
.applicationDefault | 기본 위치, 앱 샌드박스 (대부분의 앱) |
.groupContainer(identifier:) | 앱과 확장 프로그램 간 팁 상태 공유 |
.url(_:) | 저장 위치를 완전히 제어할 수 있는 사용자 정의 파일 URL |
CloudKit 동기화
사용자의 여러 기기 간에 팁 상태를 동기화하여 모든 기기에서 동일한 팁이 표시되지 않도록 합니다. 데이터 저장소 위치와 함께 CloudKit 컨테이너 옵션을 추가하세요.
try? Tips.configure([
.datastoreLocation(.applicationDefault),
.cloudKitContainer(.named("iCloud.com.example.app"))
])
팁 정의하기
Tip 프로토콜을 준수하는 구조체를 만드세요. 최소한 title을 제공해야 합니다. 지원 세부 정보를 위해 message를, 선행 아이콘을 위해 image를 추가하세요. 팁이 간결한 콜아웃으로 나타나므로 제목을 짧고 행동 지향적으로 유지하세요.
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("즐겨찾기 고정") }
var message: Text? { Text("하트 아이콘을 탭하여 빠르게 접근할 수 있도록 항목을 저장하세요.") }
var image: Image? { Image(systemName: "heart") }
}
속성: title (필수), message (선택적 세부 정보), image (선택적 선행 아이콘), actions (선택적 버튼), rules (선택적 자격 조건), options (표시 빈도, 최대 횟수).
수명 주기: 보류 중 (규칙 미충족) -> 자격 획득 (모든 규칙 통과) -> 무효화 (해제, 액션 실행 또는 프로그래매틱 방식으로 제거). 무효화되면 데이터 저장소가 재설정되지 않는 한 팁이 다시 나타나지 않습니다.
팁 표시하기
인라인 팁 (TipView 사용)
레이아웃에 TipView를 직접 포함시키세요. 애니메이션과 함께 둥근 카드 형태로 나타나고 사라집니다. 스크롤 가능한 콘텐츠 내의 팁에 사용하세요.
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
Popover 팁 (.popoverTip() 사용)
팁을 특정 뷰에 고정된 팝오버로 연결하세요. 프레임워크가 팝오버에서 앵커까지 화살표를 그립니다. 특정 컨트롤을 가리키는 팁에 사용하세요.
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// 화살표 방향 제어 (생략 시 시스템이 선택)
.popoverTip(favoriteTip, arrowEdge: .bottom)
사용자 정의 TipViewStyle
앱 전체에서 팁 모양을 제어하는 사용자 정의 스타일을 만드세요. TipViewStyle을 준수하고 makeBody(configuration:)을 구현하세요.
struct CustomTipStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 12) {
configuration.image?
.font(.title2)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 4) {
configuration.title
.font(.headline)
configuration.message?
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
// 전역 또는 뷰별로 적용
TipView(favoriteTip)
.tipViewStyle(CustomTipStyle())
팁 규칙
규칙은 팁이 자격을 획득하는 시점을 제어합니다. rules 배열의 모든 규칙이 통과되어야 팁이 표시됩니다. TipKit은 매개변수 기반 규칙과 이벤트 기반 규칙, 두 가지 유형을 지원합니다.
매개변수 기반 규칙
@Parameter를 사용하여 앱 상태를 추적하세요. 매개변수 값이 규칙 조건을 충족하면 팁이 자격을 획득합니다.
struct FavoriteTip: Tip {
@Parameter
static var hasSeenList: Bool = false
var title: Text { Text("즐겨찾기 고정") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
// 사용자가 목록에 도달하면 매개변수 설정
FavoriteTip.hasSeenList = true
이벤트 기반 규칙
Tips.Event를 사용하여 사용자 액션을 추적하세요. 액션이 발생할 때마다 이벤트에 기부(donate)하세요. 기부 횟수 또는 타이밍 조건이 충족되면 규칙이 실행됩니다. 이는 사용자가 관련 기능을 발견하지 않고 액션을 여러 번 수행한 후에 팁을 표시해야 하는 경우에 이상적입니다.
struct ShortcutTip: Tip {
static let appOpenedEvent = Tips.Event(id: "appOpened")
var title: Text { Text("빠른 액션 시도") }
var rules: [Rule] {
#Rule(Self.appOpenedEvent) { $0.donations.count >= 3 }
}
}
// 앱이 열릴 때마다 기부
ShortcutTip.appOpenedEvent.donate()
여러 규칙 결합
배열에 여러 규칙을 배치하세요. 모든 규칙이 통과되어야 합니다 (논리적 AND).
struct AdvancedTip: Tip {
@Parameter
static var isLoggedIn: Bool = false
static let featureUsedEvent = Tips.Event(id: "featureUsed")
var title: Text { Text("고급 모드 잠금 해제") }
var rules: [Rule] {
#Rule(Self.$isLoggedIn) { $0 == true }
#Rule(Self.featureUsedEvent) { $0.donations.count >= 5 }
}
}
표시 빈도 옵션
options 속성을 사용하여 팁이 표시되는 빈도를 제어하세요.
struct DailyTip: Tip {
var title: Text { Text("일일 알림") }
var options: [TipOption] {
MaxDisplayCount(3) // 최대 3회만 표시
IgnoresDisplayFrequency(true) // 전역 빈도 제한 무시
}
}
전역 표시 빈도는 구성 시점에 설정됩니다:
try? Tips.configure([
.displayFrequency(.daily) // .immediate, .hourly, .daily, .weekly, .monthly
])
.daily의 경우, 특정 팁이 IgnoresDisplayFrequency(true)를 설정하지 않는 한 시스템은 앱 전체에서 하루에 최대 하나의 팁만 표시합니다.
팁 액션
팁에 액션 버튼을 추가하여 직접 상호 작용을 가능하게 하세요. 각 액션에는 id와 레이블이 있습니다. 팁 뷰의 액션 핸들러에서 액션을 처리하세요.
struct FeatureTip: Tip {
var title: Text { Text("새 편집기 사용해보기") }
var message: Text? { Text("강력한 새 편집 모드를 추가했습니다.") }
var actions: [Action] {
Action(id: "open-editor", title: "편집기 열기")
Action(id: "learn-more", title: "더 알아보기")
}
}
뷰에서 액션 처리:
TipView(featureTip) { action in
switch action.id {
case "open-editor":
navigateToEditor()
featureTip.invalidate(reason: .actionPerformed)
case "learn-more":
showHelpSheet = true
default:
break
}
}
팁 그룹
TipGroup을 사용하여 단일 뷰 내에서 여러 팁을 조정하세요. TipGroup은 그룹에서 한 번에 하나의 팁만 표시되도록 하여 팁 과부하를 방지합니다. 팁은 우선 순위 순서로 표시됩니다.
struct OnboardingView: View {
let tipGroup = TipGroup(.ordered) {
WelcomeTip()
NavigationTip()
ProfileTip()
}
var body: some View {
VStack {
if let currentTip = tipGroup.currentTip {
TipView(currentTip)
}
Button("다음") {
tipGroup.currentTip?.invalidate(reason: .actionPerformed)
}
}
}
}
우선 순위 옵션
| 초기화 구문 | 동작 |
|---|---|
.ordered | 팁이 나열된 순서대로 표시됨 |
현재 팁이 무효화되면 그룹에서 다음 자격을 갖춘 팁이 currentTip이 됩니다.
프로그래매틱 제어
팁 무효화
사용자가 발견한 액션을 수행하거나 팁이 더 이상 관련이 없을 때 invalidate(reason:)을 호출하세요.
let tip = FavoriteTip()
tip.invalidate(reason: .actionPerformed)
| 이유 | 사용 시기 |
|---|---|
.actionPerformed | 사용자가 팁이 설명하는 액션을 수행함 |
.displayCountExceeded | 팁이 최대 표시 횟수에 도달함 |
.tipClosed | 사용자가 명시적으로 팁을 해제함 |
테스트 유틸리티
TipKit은 개발 및 테스트 중에 팁 가시성을 제어하는 정적 메서드를 제공합니다. #if DEBUG 또는 ProcessInfo 검사 뒤에 게이트(gate)하여 프로덕션 빌드에서 실행되지 않도록 하세요.
#if DEBUG
// 규칙에 관계없이 모든 팁 표시 (개발 중에 유용)
Tips.showAllTipsForTesting()
// 특정 팁만 표시
Tips.showTipsForTesting([FavoriteTip.self, ShortcutTip.self])
// 모든 팁 숨기기 (팁을 포함하지 않는 UI 테스트에 유용)
Tips.hideAllTipsForTesting()
// 데이터 저장소 재설정 (모든 팁 상태, 무효화 및 이벤트 지우기)
try? Tips.resetDatastore()
#endif
테스트 스킴을 위한 ProcessInfo 사용
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
개발 빌드의 Xcode 스킴에서 --show-all-tips를 시작 인수로 전달하세요.
일반적인 실수
DON'T: App.init 외부에서 Tips.configure() 호출
뷰의 onAppear 또는 task 수정자에서 Tips.configure()를 호출하면 팁 뷰가 데이터 저장소가 준비되기 전에 렌더링되려고 하는 경합 상태가 발생하여 팁이 누락되거나 깜빡일 수 있습니다.
// 틀림
struct ContentView: View {
var body: some View {
Text("Hello")
.task { try? Tips.configure() } // 너무 늦음, 뷰가 이미 렌더링됨
}
}
// 올바름
@main struct MyApp: App {
init() { try? Tips.configure() }
var body: some Scene { WindowGroup { ContentView() } }
}
DON'T: 한 번에 너무 많은 팁 표시
여러 팁을 동시에 표시하면 사용자를 압도하고 각 팁의 효과가 희석됩니다. 사용자는 팁을 무시하는 법을 배웁니다.
// 틀림: 동시에 세 개의 팁이 표시됨
VStack {
TipView(tipA)
TipView(tipB)
TipView(tipC)
}
// 올바름: TipGroup을 사용하여 순서 지정
let group = TipGroup(.ordered) { TipA(); TipB(); TipC() }
if let currentTip = group.currentTip {
TipView(currentTip)
}
DON'T: 사용자가 액션을 수행한 후 팁을 무효화하는 것을 잊음
팁에 "별표를 탭하여 즐겨찾기"라고 표시되어 있고 사용자가 별표를 탭했지만 팁이 계속 표시되면 UI에 대한 신뢰가 떨어집니다.
// 틀림: 사용자가 액션을 수행한 후에도 팁이 계속 표시됨
Button("Favorite") { toggleFavorite() }
.popoverTip(favoriteTip)
// 올바름: 액션 시 무효화
Button("Favorite") {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
}
.popoverTip(favoriteTip)
DON'T: 프로덕션에 테스트 팁을 활성화한 상태로 둠
Tips.showAllTipsForTesting()은 모든 규칙과 빈도 제한을 우회합니다. 이 상태로 프로덕션에 배포하면 모든 사용자가 모든 팁을 즉시 보게 됩니다.
// 틀림: 항상 활성화
Tips.showAllTipsForTesting()
// 올바름: DEBUG 뒤에 게이트
#if DEBUG
Tips.showAllTipsForTesting()
#endif
DON'T: 팁 제목을 너무 길게 만듦
긴 제목은 간결한 팁 콜아웃에서 잘리거나 어색하게 줄 바꿈됩니다. 핵심 액션은 제목에, 지원 컨텍스트는 메시지에 넣으세요.
// 틀림
var title: Text { Text("하트 버튼을 탭하여 이 항목을 즐겨찾기 목록에 저장할 수 있습니다") }
// 올바름
var title: Text { Text("즐겨찾기에 저장") }
var message: Text? { Text("하트 아이콘을 탭하여 빠르게 접근할 수 있도록 항목을 유지하세요.") }
DON'T: 중요한 정보에 팁 사용
사용자는 언제든지 팁을 해제할 수 있으며 팁은 다시 나타나지 않습니다. 필수 지침이나 안전 정보를 팁에 절대 넣지 마세요.
// 틀림: 해제 가능한 팁에 중요한 정보
struct DataLossTip: Tip {
var title: Text { Text("저장되지 않은 변경 사항이 손실됩니다") }
}
// 올바름: 중요한 정보에는 알림 또는 인라인 경고 사용
// 팁은 기능 발견 및 점진적 공개(progressive disclosure)를 위해 예약하세요
검토 체크리스트
-
Tips.configure()가 뷰가 렌더링되기 전에App.init에서 호출되었는지 확인 - 각 팁에 명확하고 간결한 제목이 있는지 확인 (행동 지향, 약 40자 미만)
- 사용자가 발견한 액션을 수행할 때 팁이 무효화되는지 확인
- 규칙이 설정되어 팁이 적시에 나타나는지 확인 (모든 팁이 첫 실행 시 즉시 나타나지 않음)
- 하나의 뷰에 여러 팁이 있을 때
TipGroup이 사용되었는지 확인 - 테스트 유틸리티 (
showAllTipsForTesting,resetDatastore)가#if DEBUG뒤에 게이트되었는지 확인 - 앱이 여러 기기를 지원하는 경우 CloudKit 동기화가 구성되었는지 확인
- 표시 빈도가 적절하게 설정되었는지 확인 (대부분의 앱의 경우
.daily또는.weekly) - 팁이 기능 발견에만 사용되고 중요한 정보에는 사용되지 않는지 확인
- 기본 스타일이 앱 디자인과 일치하지 않는 경우 사용자 정의
TipViewStyle이 일관되게 적용되었는지 확인 - 팁 액션이 처리되고 액션 핸들러에서 팁이 무효화되는지 확인
- 이벤트 기부가 올바른 사용자 액션 지점에 배치되었는지 확인
- 사용자 정의 Tip 유형이 Sendable인지 확인하고, @MainActor에서 Tips를 구성하세요.
참고 자료
- 완전한 구현 패턴 (사용자 정의 스타일, 이벤트 기반 규칙, 팁 그룹, 테스트 전략, 온보딩 플로우 및 SwiftUI 미리보기 구성 포함)은 references/tipkit-patterns.md를 참조하세요.