π‘ TipKit
μ± κΈ°λ₯μ λ°κ²¬νκ² λλ μΈμ± ν
β¨ TipKitμ΄λ?
TipKitμ iOS 17μμ λμ λ νλ μμν¬λ‘, μ¬μ©μκ° μ±μ μλ‘μ΄ κΈ°λ₯μ΄λ μ¨κ²¨μ§ κΈ°λ₯μ μμ°μ€λ½κ² λ°κ²¬ν μ μλλ‘ μΈμ± νμ νμν©λλ€. Appleμ λ€μ΄ν°λΈ μ€νμΌμ λ°λ₯΄λ©°, μ¬μ©μ νλμ λ°λΌ μ§λ₯μ μΌλ‘ νμ νμνκ±°λ μ¨κΉλλ€.
π― 1. κΈ°λ³Έ μ€μ
μ± μμ μ TipKitμ μ΄κΈ°νν©λλ€.
import SwiftUI import TipKit @main struct MyApp: App { init() { // TipKit μ΄κΈ°ν try? Tips.configure([ // κ°λ° μ€: μ± μ€ν μλ§λ€ ν νμ .displayFrequency(.immediate), // νλ‘λμ : κΈ°λ³Έ μ μ± μ¬μ© // .displayFrequency(.daily) // λ°μ΄ν° μ μ₯ μμΉ .datastoreLocation(.applicationDefault) ]) } var body: some Scene { WindowGroup { ContentView() } } }
π¬ 2. κΈ°λ³Έ ν μμ±
Tip νλ‘ν μ½μ μ±ννμ¬ νμ μ μν©λλ€.
import TipKit struct FavoriteTip: Tip { // ν μ λͺ© var title: Text { Text("μ¦κ²¨μ°ΎκΈ°μ μΆκ°νμΈμ") } // ν μ€λͺ var message: Text? { Text("λ³ μμ΄μ½μ ννλ©΄ μμ£Ό μ¬μ©νλ νλͺ©μ λΉ λ₯΄κ² μ°Ύμ μ μμ΄μ") } // ν μμ΄μ½ var image: Image? { Image(systemName: "star") } } // SwiftUI Viewμμ μ¬μ© struct ContentView: View { let favoriteTip = FavoriteTip() var body: some View { VStack { // μΈλΌμΈ ν (λ·°μ μ§μ μ½μ ) TipView(favoriteTip) Button("μ¦κ²¨μ°ΎκΈ° μΆκ°") { // μ‘μ } } .padding() } }
π¨ 3. ν μ€νμΌ
μΈλΌμΈ νκ³Ό νμ€λ² ν λ κ°μ§ μ€νμΌμ μ 곡ν©λλ€.
import SwiftUI import TipKit struct TipStylesView: View { let inlineTip = FavoriteTip() let popoverTip = SearchTip() var body: some View { VStack(spacing: 30) { // 1. μΈλΌμΈ ν (λ·° κ³μΈ΅μ μ§μ μ½μ ) TipView(inlineTip, arrowEdge: .top) Divider() // 2. νμ€λ² ν (νΉμ μμμ νμ΄νλ‘ μ°κ²°) Button("κ²μ") { // κ²μ μ‘μ } .popoverTip(popoverTip) // 3. 컀μ€ν μ€νμΌ μΈλΌμΈ ν TipView(inlineTip) { action in // μ‘μ λ²νΌ 컀μ€ν°λ§μ΄μ¦ if let action { Button(action: action.handler) { Text(action.label) .foregroundStyle(.white) .padding(8) .background(Color.blue) .cornerRadius(8) } } } } .padding() } } // κ²μ ν μ μ struct SearchTip: Tip { var title: Text { Text("λΉ λ₯Έ κ²μ") } var message: Text? { Text("β + Fλ‘ λΉ λ₯΄κ² κ²μν μ μμ΄μ") } var image: Image? { Image(systemName: "magnifyingglass") } }
π― 4. κ·μΉ κΈ°λ° ν (Rules)
νΉμ μ‘°κ±΄μ΄ λ§μ‘±λ λλ§ νμ νμν©λλ€.
import TipKit struct EditTip: Tip { // μ¬μ©μκ° 3κ° νλͺ©μ μμ±νλμ§ μΆμ @Parameter static var itemsCreated: Int = 0 // νΈμ§ κΈ°λ₯μ μ¬μ©νλμ§ μΆμ static let hasEditedItem = Event(id: "hasEditedItem") var title: Text { Text("νλͺ© νΈμ§νκΈ°") } var message: Text? { Text("νλͺ©μ κΈΈκ² λλ¬μ νΈμ§ν μ μμ΄μ") } var image: Image? { Image(systemName: "pencil") } // ν νμ κ·μΉ var rules: [Rule] { [ // κ·μΉ 1: 3κ° μ΄μ νλͺ©μ μμ±νμ λ #Rule(Self.$itemsCreated) { $0 >= 3 }, // κ·μΉ 2: μμ§ νΈμ§μ ν λ²λ μ νμ λ #Rule(Self.hasEditedItem) { $0.donations.count == 0 } ] } // μ΅μ : νμ ν μ‘μ var actions: [Action] { [ Action(id: "try-edit", title: "μ§κΈ ν΄λ³΄κΈ°") ] } } // μ¬μ© μμ struct ItemListView: View { let editTip = EditTip() @State private var items: [String] = [] var body: some View { VStack { TipView(editTip) { action in if action.id == "try-edit" { Button("μ§κΈ ν΄λ³΄κΈ°") { // νΈμ§ λͺ¨λλ‘ μ ν } } } List(items, id: \.self) { item in Text(item) .onLongPressGesture { // νΈμ§ μ‘μ Task { await EditTip.hasEditedItem.donate() } } } Button("νλͺ© μΆκ°") { items.append("μ νλͺ©") // νλΌλ―Έν° μ λ°μ΄νΈ EditTip.itemsCreated += 1 } } } }
π 5. μ΄λ²€νΈ μΆμ
μ¬μ©μ νλμ μΆμ νμ¬ μ μ ν μμ μ νμ νμν©λλ€.
import TipKit struct ShareTip: Tip { // μ¬λ¬ μ΄λ²€νΈ μ μ static let didViewItem = Event(id: "didViewItem") static let didCreateItem = Event(id: "didCreateItem") static let didShareItem = Event(id: "didShareItem") var title: Text { Text("μΉκ΅¬μκ² κ³΅μ νκΈ°") } var message: Text? { Text("곡μ λ²νΌμΌλ‘ μΉκ΅¬λ€κ³Ό νλͺ©μ 곡μ ν΄λ³΄μΈμ") } var rules: [Rule] { [ // 5λ² μ΄μ νλͺ©μ λ΄€μ λ #Rule(Self.didViewItem) { $0.donations.count >= 5 }, // 2κ° μ΄μ νλͺ©μ λ§λ€μμ λ #Rule(Self.didCreateItem) { $0.donations.count >= 2 }, // μμ§ κ³΅μ λ₯Ό μ νμ λ #Rule(Self.didShareItem) { $0.donations.count == 0 } ] } } // μ΄λ²€νΈ κΈ°λ‘ μμ struct DetailView: View { let shareTip = ShareTip() var body: some View { VStack { Text("μμΈ λ΄μ©") Button("곡μ ") { // 곡μ μ‘μ Task { await ShareTip.didShareItem.donate() } } .popoverTip(shareTip) } .task { // λ·° μ§μ μ μ΄λ²€νΈ κΈ°λ‘ await ShareTip.didViewItem.donate() } } }
βοΈ 6. ν κ΄λ¦¬
νμ μνλ₯Ό νλ‘κ·Έλλ° λ°©μμΌλ‘ μ μ΄ν©λλ€.
import TipKit struct TipManagementView: View { let tip = FavoriteTip() var body: some View { VStack(spacing: 20) { TipView(tip) // ν 무ν¨ν (λ€μλ νμ μ ν¨) Button("μ΄ ν 무ν¨ν") { tip.invalidate(reason: .actionPerformed) } // λͺ¨λ ν 리μ (κ°λ°/ν μ€νΈμ©) Button("λͺ¨λ ν 리μ ") { Task { try? await Tips.resetDatastore() } } // λͺ¨λ μ΄λ²€νΈμ νλΌλ―Έν° μ΄κΈ°ν Button("λ°μ΄ν° μ΄κΈ°ν") { Task { try? await Tips.configureTips { Tips.showAllTipsForTesting() } } } } .padding() } } // ν 무ν¨ν μ΄μ extension Tip.InvalidationReason { // .actionPerformed - μ¬μ©μκ° νμ μ‘μ μ μνν¨ // .tipClosed - μ¬μ©μκ° νμ μ§μ λ«μ // .displayCountExceeded - μ΅λ νμ νμ μ΄κ³Ό }
π 7. κ³ κΈ κ·μΉ
볡μ‘ν 쑰건μ μ‘°ν©νμ¬ μ§λ₯μ μΌλ‘ νμ νμν©λλ€.
import TipKit struct ProTip: Tip { // νλΌλ―Έν°λ€ @Parameter static var userLevel: Int = 0 @Parameter static var isPremiumUser: Bool = false // μ΄λ²€νΈ static let usedBasicFeature = Event(id: "usedBasicFeature") static let usedProFeature = Event(id: "usedProFeature") var title: Text { Text("νλ‘ κΈ°λ₯ μ¬μ©νκΈ°") } var message: Text? { Text("ν리미μ κΈ°λ₯μΌλ‘ λ λ§μ μμ μ ν μ μμ΄μ") } var rules: [Rule] { [ // λ 벨 5 μ΄μ #Rule(Self.$userLevel) { $0 >= 5 }, // ν리미μ μ μ κ° μλ #Rule(Self.$isPremiumUser) { $0 == false }, // κΈ°λ³Έ κΈ°λ₯μ 10λ² μ΄μ μ¬μ© #Rule(Self.usedBasicFeature) { $0.donations.count >= 10 }, // νλ‘ κΈ°λ₯μ μμ§ μ μ¨λ΄ #Rule(Self.usedProFeature) { $0.donations.count == 0 }, // μ΅κ·Ό 7μΌ μ΄λ΄μ κΈ°λ³Έ κΈ°λ₯ μ¬μ© #Rule(Self.usedBasicFeature) { $0.donations.last7Days.count > 0 } ] } var options: [Option] { [ // μ΅λ 3λ²κΉμ§λ§ νμ .maxDisplayCount(3), // 무μνλ©΄ λ μ΄μ νμ μ ν¨ .ignoresDisplayFrequency(false) ] } }
π± μ’ ν© μμ
import SwiftUI import TipKit struct TipKitDemoView: View { @State private var items: [String] = [] @State private var favorites: Set<String> = [] let favoriteTip = FavoriteTip() let editTip = EditTip() var body: some View { NavigationStack { VStack { // μΈλΌμΈ ν TipView(favoriteTip) .padding() List { ForEach(items, id: \.self) { item in HStack { Text(item) Spacer() Button { toggleFavorite(item) } label: { Image(systemName: favorites.contains(item) ? "star.fill" : "star") } } .contentShape(Rectangle()) .onLongPressGesture { Task { await EditTip.hasEditedItem.donate() } } } } // νμ€λ² ν Button("νλͺ© μΆκ°") { items.append("νλͺ© \(items.count + 1)") EditTip.itemsCreated += 1 } .buttonStyle(.borderedProminent) .padding() .popoverTip(editTip) } .navigationTitle("TipKit λ°λͺ¨") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("리μ ") { Task { try? await Tips.resetDatastore() } } } } } } func toggleFavorite(_ item: String) { if favorites.contains(item) { favorites.remove(item) } else { favorites.insert(item) // μ¦κ²¨μ°ΎκΈ° μΆκ° μ ν 무ν¨ν favoriteTip.invalidate(reason: .actionPerformed) } } }
π‘ HIG κ°μ΄λλΌμΈ
- μ μ ν νμ΄λ°: μ¬μ©μκ° κΈ°λ₯μ μ¬μ©ν μ€λΉκ° λμμ λ ν νμ
- κ°κ²°μ±: ν λ΄μ©μ μ§§κ³ λͺ ννκ² μμ± (1-2 λ¬Έμ₯)
- λΉμΉ¨μ΅μ±: νμ΄ μ£Όμ μμ μ λ°©ν΄νμ§ μλλ‘ λ°°μΉ
- λ§₯λ½μ±: κ΄λ ¨ UI μμ κ·Όμ²μ ν νμ
- μ μ§μ 곡κ°: ν λ²μ νλμ νλ§ νμ
- ν΄μ κ°λ₯: μ¬μ©μκ° μΈμ λ μ§ νμ λ«μ μ μμ΄μΌ ν¨
- μ§λ₯μ±: μ¬μ©μκ° μ΄λ―Έ μκ³ μλ λ΄μ©μ νμνμ§ μμ
π― μ€μ νμ©
- μ¨λ³΄λ©: μ κ· μ¬μ©μμκ² ν΅μ¬ κΈ°λ₯ μκ°
- κΈ°λ₯ λ°κ²¬: μ¨κ²¨μ§ μ μ€μ²λ λ¨μΆν€ μλ €μ£ΌκΈ°
- μ μ λ§: ν리미μ κΈ°λ₯ ν보
- 컨ν μ€νΈ λμλ§: μν©μ λ§λ μ¬μ© ν μ 곡
- μ κΈ°λ₯ μκ°: μ λ°μ΄νΈ ν μ κΈ°λ₯ μλ΄
π λ μμ보기
.displayFrequency(.immediate)λ₯Ό μ¬μ©νμ¬ μ± μ€ν μλ§λ€ νμ ν
μ€νΈν μ μμ΅λλ€. νλ‘λμ
μμλ κΈ°λ³Έκ° μ¬μ©μ κΆμ₯ν©λλ€.