스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
TipKit으로 기능 소개 팁을 맞춤화하기
TipKit 프레임워크의 주 목적은 사용자에게 기능을 소개하는 것으로, TipKit 프레임워크를 사용하면 앱에서 사용 팁을 손쉽게 표시할 수 있습니다. 이제 여러 기능을 적절한 순서대로 표시하기 위해 팁을 그룹화할 수 있으며, 맞춤형 팁 식별자로 재사용 가능한 팁을 만들 수 있습니다. 또한 CloudKit을 사용하여 팁의 디자인과 느낌을 앱에 맞게 설정하고 팁을 동기화할 수 있습니다. TipKit의 최신 기능을 사용하여 사용자에게 앱의 유용한 기능을 소개하는 방법을 알아보세요.
챕터
- 0:00 - Introduction
- 1:18 - Tip groups
- 5:12 - Reusable tips with custom identifiers
- 8:25 - Custom tip styles
- 10:48 - Sync tips with CloudKit
리소스
관련 비디오
WWDC23
-
다운로드
안녕하세요 Jake입니다 TipKit의 새로운 맞춤화 방법으로 앱의 새 기능이나 알려지지 않은 기능을 사용자에게 알리는 법을 소개하게 되어 기쁩니다
TipKit은 앱에 팁을 쉽게 표시 가능한 프레임워크입니다 새로운 기능을 알려 주거나 작업을 완료하는 더 빠른 방법을 보여 줄 수 있습니다 TipKit으로 팁을 쉽게 생성하고 표시 상태와 기록을 자동 관리하여 적절한 순간에만 팁이 표시되게 할 수 있습니다 TipKit으로 자격 규칙과 표시 빈도를 통해 누가 언제 팁을 볼 수 있는지 제어할 수 있습니다 TipKit은 앱의 UI에 가장 적합한 여러 프레젠테이션 스타일을 제공하며 모든 플랫폼에서 사용할 수 있습니다 이제 더 많은 작업을 통해 기능 소개를 맞춤화하여 팁이 통합되고 매끄럽게 느껴질 수 있습니다 이 비디오에서는 팁을 그룹화하여 기능이 최적의 순서로 검색되는 방법 맞춤화 팁 식별자로 팁을 다시 사용할 수 있게 만드는 방법 TipViewStyle로 팁을 앱의 모습, 느낌에 맞추는 법 CloudKit으로 팁 표시 상태가 기기 간에 공유되도록 TipKit 데이터 저장소의 동기화 방법을 설명합니다
먼저 팁 그룹입니다 팁 그룹으로 여러 팁을 지정하여 특정 순서대로 또는 표시 가능한 첫 번째 팁을 사용하여 한 번에 하나씩 표시할 수 있습니다 오지 등산객이 새 등산로를 찾고 탐색할 수 있게 돕는 앱을 업데이트해 보겠습니다 최근 지도에 나침반 제어기를 추가했는데 팁 사용에 대해 알려 줄 두 가지 기능을 포함합니다 첫 번째 팁은 나침반을 탭해 지도에 현재 위치를 표시하는 것입니다 기능을 설명하는 제목, 메시지 이미지가 포함된 새 팁 구조를 만듭니다
두 번째 팁은 좀 더 숨겨진 제스처를 위한 팁으로 나침반 제어기를 길게 누르면 지도를 북위 0도로 다시 회전시키는데 이 기능에 대한 팁도 추가하겠습니다
이를 표시하기 위해 나침반에서 TipKit의 popoverTip 보기 한정자를 두 번 호출합니다 showLocationTip용과
rotateMapTip용입니다
이 팝오버는 잘 작동하지만 문제가 하나 있습니다 현재는 팁의 표시 순서를 제어할 수 없고, 나침반을 길게 누르라는 팁을 표시하기 전에 사용자에게 탭 나침반 기능에 대해 먼저 알려주고 싶습니다 이제 이 코드를 업데이트해 TipGroup을 사용합니다
표시를 제어하려면 두 나침반 팁을 TipGroup에 추가합니다 우선 순위에 따라 초기화합니다 정렬된 우선 순위를 사용하면 ShowLocationTip 뷰가 해제되거나 위치 표시 탭 제스처로 ShowLocationTip이 무효화될 때까지 RotateMapTip이 표시되지 않도록 할 수 있습니다 이제 그룹의 사용 가능 팁을 표시하기 위해 currentTip 속성을 사용하도록 나침반의 popoverTip 뷰 한정자를 업데이트합니다
TipGroup의 currentTip 속성을 특정 유형으로 캐스트해 팁 표시 위치를 맞춤화할 수도 있습니다 두 나침반 기능에 별도의 버튼이 있다면 currentTip을 ShowLocationTip으로 사용하여 해당 팁의 팝오버가 첫 제어기에만 표시되도록 할 수 있습니다 RotateMapTip 팝오버는 두 번째부터만 표시됩니다
이제 두 나침반 팁이 모두 올바른 순서로 표시되므로 하나만 추가하면 됩니다 설명하는 기능을 사용할 때 두 팁을 다 무효화해야 합니다 팁을 무효화하면 이미 해당 기능을 발견한 사람에게 해당 팁이 표시되지 않습니다 정렬된 우선 순위를 사용하는 TipGroup의 경우 앞의 모든 팁이 무효화된 경우에만 팁이 표시될 수 있습니다 TipGroup은 순서나 firstAvailable 우선 순위로 구성 가능합니다 compassTip에 사용된 정렬된 우선 순위는 사람들에게 관련 기능을 점진적으로 가르치는 데 유용합니다
firstAvailable 우선 순위는 표시 규칙을 충족하는 첫 번째 팁을 표시합니다 이는 서로 관련이 없는 여러 팁이 있는 뷰가 있지만 한 번에 하나만 표시하려는 경우에 유용합니다 TipGroup은 displayFrequency와 잘 작동하기도 합니다 오지 등산로 앱은 주간 displayFrequency로 팁을 구성합니다 이로써 등산로를 찾는 사람들이 팁 표시 전에 스스로 나침반의 기능을 파악하는 시간을 가질 수 있습니다 TipKit의 displayFrequency는 앱을 처음 실행할 때 너무 많은 팁으로 사람들이 지치는 것을 방지하는 좋은 방법입니다 팁 그룹은 한 번에 하나씩 팁을 원하는 순서대로 표시할 수 있는 완벽한 방법입니다
표시 규칙 및 표시 빈도와 팁 그룹을 사용하면 너무 많은 팁으로 앱에 부담을 주지 않으면서 점진적으로 기능을 호출할 수 있습니다
이제 맞춤 식별자로 팁을 재사용하는 방법을 알아봅니다 팁의 상태 및 규칙은 식별자에 따라 고유합니다 팁의 기본 식별자를 재정의하면 콘텐츠별 동일한 팁 구조를 재사용할 수 있습니다 최근 오지 등산로 앱이 업데이트되어 새 등산로 관련 지원이 추가되었으며 팁 사용에 대해 알려 드리려 합니다 먼저 새로 추가된 버틀러 포크 등산로 기점 관련 팁과 위치를 알려 주는 메시지를 만듭니다 지도에서 새 등산로를 쉽게 찾을 수 있도록 작업 버튼도 추가할 예정입니다 이 팁이 가장 유용하게 사용될 등산객에게만 표시되도록 등산로 지역을 세 번 이상 방문한 경우에만 팁이 표시되도록 이벤트 규칙을 추가할 것입니다
팁을 표시하기 위해 인스턴스를 TrailList에 추가하고 TipView로 표시하면 됩니다 Go there now 버튼을 탭하면 새 등산로가 강조 표시되도록 동작 핸들러도 추가합니다
하지만 이후 더 많은 새 등산로에 대한 지원을 추가한다면 얼마나 잘 작동할까요? 이런 앱에 새 등산로를 계속 추가하면 TrailList 코드는 대부분 팁이 될 것입니다 여러 팁 뷰가 동시에 나타날 수도 있으므로 사람들이 실제 등산로 목록에 접근하기 어려울 수 있습니다 이 접근 방식은 확장성이 떨어지므로 코드를 업데이트해 맞춤 식별자로 재사용 가능 팁을 만듭니다
특정 등산로 개체로 생성된 새 팁을 정의하고 해당 등산로의 이름과 지역을 기반으로 메시지를 제공하겠습니다 초기화에 사용된 등산로에 따라 팁의 맞춤 ID를 추가합니다 해당 ID를 맞춤화하여 NewTrailTip의 각 인스턴스는 설명하는 등산로에 따라 고유한 상태와 규칙을 갖습니다 이러면 이전 다른 등산로에 팁이 무효화되었더라도 팁이 새 등산로로 다시 표시될 수 있습니다 이러한 팁이 설명하는 영역에 관심 있는 등산객에게만 표시되도록 didVisit 표시 규칙을 업데이트해 새로 추가된 등산로 지역을 기반으로 할 것입니다
이제 최신 등산로 기반 새 팁을 생성하기 위해 TrailList 코드를 변경하면 됩니다 이를 통해 앱에 새 등산로를 추가할 때마다 자동으로 팁을 표시할 수 있습니다 팁의 단일 인스턴스만 생성하므로 여러 NewTrailTip이 동시에 나타나는 걸 걱정할 필요가 없습니다 모든 팁에는 표시되지 않더라도 식별자 기반인 지속적 기록이 있습니다 이로써 TipKit은 앱의 여러 실행에서 발생하는 이벤트를 기반으로 적합한 팁을 만들 수 있습니다 그러므로 맞춤화 식별자를 지정할 때 사용자 ID나 등산로 이름 같은 구체적인 식별자를 기반으로 하는 것이 중요합니다
기본적으로 팁의 식별자는 초기화에 사용된 유형 이름이며 해당 ID를 재정의해 콘텐츠에 따라 팁을 재사용 가능합니다
맞춤형 식별자는 TipKit에서 여러 팁에 동일 팁 모델을 재사용할 수 있는 좋은 방법입니다
이제 팁 뷰 모습을 맞춤화하는 방법을 살펴보겠습니다
팁에는 멋진 기본 프레젠테이션이 있지만 일부 경우에 앱의 UI와 더 잘 일치하도록 더 세밀하게 맞춤화해야 할 수 있습니다 이러한 팁의 경우 TipViewStyle로 모양과 동작을 맞춤화할 수 있습니다 등산 앱에 추가하는 모든 등산로 관련 멋진 사진이 있고 NewTrailTip으로 이를 보여 줘야 한다고 생각합니다 맞춤형 TipViewStyle을 만들고 각 등산로의 이미지를 배경으로 사용하고 팁의 제목과 메시지를 오버레이로 표시합니다
제목과 메시지에는 팁의 인스턴스 값 대신 makeBody 함수 구성 인수의 속성을 사용하겠습니다 이제 TipView에 적용하는 모든 한정자가 맞춤형 스타일의 메시지 및 제목과 함께 작동할 수 있습니다
이를 적용하려면 tipViewStyle 한정자를 호출하면 됩니다 이제 팁이 등산로의 멋진 사진 배경으로 표시됩니다 NewTrailTip에는 지도에서 등산로를 빠르게 강조 표시하는 작업도 포함되어 있으므로 해당 사진 위에 버튼을 추가하고 싶진 않습니다
대신 전체 팁 뷰를 탭하도록 맞춤형 스타일을 업데이트합니다
구성 인수에서 NewTrailTip의 작업을 가져오는 것부터 시작합니다 이제 팁 뷰를 탭할 때 동작 핸들러를 호출하면 됩니다 구성 인수의 actions 속성을 사용하면 TipView의 일부로 만든 핸들러가 작업이 수행될 때 계속 호출됩니다
맞춤형 TipViewStyle을 만들 때 가능하면 팁의 인스턴스 값보다 구성 인수의 속성을 선호하는 것이 중요합니다
이로써 맞춤형 스타일 사용 시 TipView에 적용된 클로저와 한정자를 계속 평가할 수 있습니다
맞춤형 TipViewStyle은 tipCornerRadius와 tipBackground 등 다른 팁 뷰 한정자와 잘 작동합니다 UIKit나 AppKit을 사용하는 앱의 경우 TipUIView 및 TipNSView 스타일을 변경하도록 설정 가능한 viewStyle 속성이 있습니다 TipViewStyle을 생성하면 TipKit의 규칙 엔진이 표시, 해제 처리를 허용하면서 맞춤형 모양 및 동작으로 팁을 쉽게 표시할 수 있습니다
이제 CloudKit 동기화입니다 CloudKit은 팁의 표시 상태를 동기화하고 둘 이상의 기기에서 동일한 팁을 닫을 필요가 없도록 하여 앱의 사용자 경험을 개선합니다 이제 오지 등산로 앱에 유용한 팁을 추가했으므로 팁의 상태와 규칙을 공유할 수 있게 CloudKit 동기화를 설정해야 합니다 먼저 Xcode 프로젝트의 Signing 및 Capabilities에 iCloud를 추가합니다 iCloud 서비스 아래 CloudKit을 켜고 팁 동기화를 위한 새 컨테이너를 만듭니다 백그라운드 모드를 추가하고 원격 알림 기능도 활성화해야 합니다 이로써 TipKit이 백그라운드에서 원격 변화를 처리해 앱의 등산로 팁이 항상 올바른 상태와 표시 상태를 유지하게 할 수 있습니다
마지막 단계로 cloudKitContainer 옵션을 포함하고 새 컨테이너의 ID를 전달하도록 Tips 구성 호출을 업데이트합니다
이게 전부입니다 이제 등산로 팁이 기기 간에 동기화되어 동일한 팁을 여러 번 닫을 필요가 없습니다 또한 TipKit은 이벤트 및 매개변수 값도 동기화하므로 NewTrailTip이 한 기기에 표시되도록 허용하는 정보가 공유되어 다른 기기에도 표시될 수 있습니다
앱의 팁은 무효화되기 전에 몇 번만 나타날 수 있습니다 팁을 유지하여 TipKit은 표시될 준비가 될 때까지 모델을 메모리에 로드하는 것을 방지합니다 CloudKit 동기화를 통해 팁의 상태와 규칙이 공유되므로 팁이 다른 기기에서 닫힌 후 한 기기에 재표시되지 않습니다
TipKit은 이벤트와 매개변수를 동기화하여 여러 기기 간 이벤트 기반의 표시 규칙을 만들 수 있습니다 표시 횟수와 기간 값도 동기화되므로 MaxDisplayCount를 지정하는 팁은 공유된 총 표시 횟수에 따라 무효화될 수 있습니다
경우에 따라 CloudKit 동기화를 사용하고 싶지만 다른 플랫폼에 고유한 특정 팁이 있을 수 있습니다 이러한 경우 UIDevice를 사용해 같은 팁을 여러 기기에 다시 표시 가능한 플랫폼별 팁 ID를 만들 수 있습니다
테스트를 위해 TipKit의 resetDatastore 함수는 모든 팁에 대한 CloudKit 기록과 로컬 데이터 저장소도 지웁니다
TipKit은 SwiftData의 강력한 지속성 기반입니다 이를 통해 앱 실행 시 팁 상태, 규칙 매개변수, 이벤트 값을 유지할 수 있습니다 CloudKit 동기화로 이 값을 기기 간 쉽게 공유 가능합니다
TipKit에는 앱의 팁이 가장 유용한 대상에게만 최적의 시간에 표시되도록 하는 강력한 도구가 있습니다 TipGroup을 사용해 기능을 최적의 순서로 한 번에 하나씩 검색할 수 있습니다 팁 그룹은 앱의 기능 검색 맞춤화를 위해 표시 규칙 및 표시 빈도와 효과적으로 작동합니다 표시 규칙 생성에 대해 자세히 알아보려면 작년 WWDC 비디오 ‘TipKit으로 기능 노출하기‘를 확인하세요
맞춤형 식별자는 재사용 가능 팁 모델을 만드는 쉬운 방법을 제공하여 팁이 콘텐츠에 따라 다시 표시될 수 있습니다 TipViewStyle은 팁의 맞춤형 레이아웃과 상호 작용을 생성하여 항상 앱 UI와 일치하도록 합니다 CloudKit으로 기기 간 TipKit의 데이터 저장소를 동기화해 팁이 불필요하게 다시 표시되지 않게 할 수 있습니다
전체 TipKit 팀을 대표하여 오늘 시청해 주셔서 감사하다는 말씀 전합니다 TipKit이 사람들에게 앱의 멋진 새 기능을 노출하는 데 도움이 되면 좋겠네요
-
-
1:43 - Create new tips
// Create new tips struct ShowLocationTip: Tip { var title: Text { Text("Show your location") } var message: Text? { Text("Tap the compass to highlight your current location on the map.") } var image: Image? { Image(systemName: "location.circle") } }
-
1:54 - Create new tips
// Create new tips struct ShowLocationTip: Tip { var title: Text { Text("Show your location") } var message: Text? { Text("Tap the compass to highlight your current location on the map.") } var image: Image? { Image(systemName: "location.circle") } } struct RotateMapTip: Tip { var title: Text { Text("Reorient the map") } var message: Text? { Text("Tap and hold on the compass to rotate the map back to 0° North.") } var image: Image? { Image(systemName: "hand.tap") } }
-
2:09 - Show popover tips
// Show popover tips struct MapCompassControl: View { let showLocationTip = ShowLocationTip() let rotateMapTip = RotateMapTip() var body: some View { CompassDial() .popoverTip(showLocationTip) .popoverTip(rotateMapTip) .onTapGesture { showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { reorientMapHeading() } } }
-
2:41 - Create a TipGroup
// Create a TipGroup struct MapCompassControl: View { @State var compassTips: TipGroup(.ordered) { ShowLocationTip() RotateMapTip() } var body: some View { CompassDial() .popoverTip(compassTips.currentTip) .onTapGesture { showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { reorientMapHeading() } } }
-
3:15 - Show TipGroup tips on different views
// Show TipGroup tips on different views struct MapControlsStack: View { @State var compassTips: TipGroup(.ordered) { ShowLocationTip() RotateMapTip() } var body: some View { VStack { ShowLocationButton() .popoverTip(compassTips.currentTip as? ShowLocationTip) RotateMapButton() .popoverTip(compassTips.currentTip as? RotateMapTip) } } }
-
3:50 - Invalidate tips
// Invalidate tips struct MapCompassControl: View { @State var compassTips: TipGroup(.ordered) { showLocationTip rotateMapTip } var body: some View { CompassDial() .popoverTip(compassTips.currentTip) .onTapGesture { showLocationTip.invalidate(reason: .actionPerformed) showCurrentLocation() } .onLongPressGesture(minimumDuration: 0.1) { rotateMapTip.invalidate(reason: .actionPerformed) reorientMapHeading() } } }
-
5:37 - Create a tip
// Create a tip struct ButlerForkTip: Tip { var title: Text { Text("Butler Fork is now available") } var message: Text? { Text("To see key trail info, tap Big Cottonwood Canyon on the map.") } var actions: [Action] { Action(title: "Go there now") } var rules: [Rule] { #Rule(Region.bigCottonwoodCanyon.didVisitEvent) { $0.donations.count > 3 } } }
-
6:01 - Show a TipView
// Show a TipView struct ButlerForkTip: Tip { var title: Text { Text("Butler Fork is now available") } var message: Text? { Text("To see key trail info, tap Big Cottonwood Canyon on the map.") } var actions: [Action] { Action(title: "Go there now") } var rules: [Rule] { #Rule(Region.bigCottonwoodCanyon.didVisitEvent) { $0.donations.count > 3 } } } struct TrailList: View { var trails: [Trail] var body: some View { ScrollView { let butlerForkTip = ButlerForkTip() TipView(butlerForkTip) { _ in highlightButlerForkTrail() } ListSection(title: "Trails", trails: trails) } } }
-
6:45 - Create a reusable tip
// Create a reusable tip struct NewTrailTip: Tip { let newTrail: Trail var title: Text { Text("\(newTrail.name) is now available") } var message: Text? { Text("To see key trail info, tap \(newTrail.region) on the map.") } var actions: [Action] { Action(title: "Go there now") } var id: String { "NewTrailTip-\(newTrail.id)" } var rules: [Rule] { #Rule(newTrail.region.didVisitEvent) { $0.donations.count > 3 } } }
-
7:26 - Show a TipView
// Show a TipView struct NewTrailTip: Tip { let newTrail: Trail var title: Text { Text("\(newTrail.name) is now available") } var message: Text? { Text("To see key trail info, tap \(newTrail.region) on the map.") } var actions: [Action] { Action(title: "Go there now") } var id: String { "NewTrailTip-\(newTrail.id)" } var rules: [Rule] { #Rule(newTrail.region.didVisitEvent) { $0.donations.count > 3 } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } ListSection(title: "Trails", trails: trails) } } }
-
8:55 - Create a custom TipViewStyle
// Create a custom TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .overlay { VStack { configuration.title.font(.title) configuration.message.font(.subheadline) } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } }
-
9:20 - Apply a TipViewStyle
// Apply a TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .overlay { VStack { configuration.title.font(.title) configuration.message.font(.subheadline) } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } .tipViewStyle(NewTrailTipViewStyle()) ListSection(title: "Trails", trails: trails) } } }
-
9:45 - Add the tip's action handler
// Apply a TipViewStyle struct NewTrailTipViewStyle: TipViewStyle { func makeBody(configuration: Configuration) -> some View { let tip = configuration.tip as! NewTrailTip let highlightTrailAction = configuration.actions.first! TrailImage(imageName: tip.newTrail.heroImage) .frame(maxHeight: 150) .onTapGesture { highlightTrailAction.handler() } .overlay { VStack { configuration.title.font(.title) HStack { configuration.message.font(.subheadline) Spacer() Image(systemName: "chevron.forward.circle") .foregroundStyle(.white) } } } } } extension NewTrailTipViewStyle { struct TrailImage: View { let imageName: String var body: some View { Image(imageName) .resizable() .aspectRatio(contentMode: .fill) } } } struct TrailList: View { var trails: [Trail] let newTrail: Trail var body: some View { ScrollView { let newTrailTip = NewTrailTip(newTrail: newTrail) TipView(newTrailTip) { _ in highlightTrail(newTrailTip) } .tipViewStyle(NewTrailTipViewStyle()) ListSection(title: "Trails", trails: trails) } } }
-
11:38 - Add CloudKit sync for tips
// Add CloudKit sync for tips @main struct TipKitTrails: App { var body: some Scene { WindowGroup { ContentView() .task { await configureTips() } } } func configureTips() async { do { try Tips.configure([ .cloudKitContainer(.named("iCloud.com.apple.TipKitTrails.tips")), .displayFrequency(.weekly) ]) } catch { print("Unable to configure tips: \(error)") } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.