๊ธฐ๋ฅ ํ ๋ฐ ์จ๋ณด๋ฉ ๊ฐ์ด๋. ์ด ๋ฌธ์๋ฅผ ์ฝ๊ณ TipKit ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
TipKit์ ์ฑ์ ๊ธฐ๋ฅ์ ์ฌ์ฉ์์๊ฒ ์ ์ ํ ์์ ์ ์๋ดํ๋ ํ๋ ์์ํฌ์ ๋๋ค. ํ ํ์ ์กฐ๊ฑด, ๋น๋, ์ฐ์ ์์๋ฅผ ์์คํ ์ด ์๋์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค.
import TipKit@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? Tips.configure([
.displayFrequency(.immediate), // ๋๋ .daily, .weekly, .monthly
.datastoreLocation(.applicationDefault)
])
}
}
}
}struct FavoriteTip: Tip {
var title: Text {
Text("์ฆ๊ฒจ์ฐพ๊ธฐ ์ถ๊ฐ")
}
var message: Text? {
Text("ํํธ๋ฅผ ํญํด์ ์ฆ๊ฒจ์ฐพ๊ธฐ์ ์ถ๊ฐํ์ธ์")
}
var image: Image? {
Image(systemName: "heart")
}
}struct ContentView: View {
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
// ์ธ๋ผ์ธ ํ
TipView(favoriteTip)
Button {
// ์ก์
} label: {
Image(systemName: "heart")
}
// ํ์ค๋ฒ ํ
.popoverTip(favoriteTip)
}
}
}struct FavoriteTip: Tip {
// ...
}
// ์ฌ์ฉ์๊ฐ ๊ธฐ๋ฅ ์ฌ์ฉ ์ ํ ๋ซ๊ธฐ
Button("์ฆ๊ฒจ์ฐพ๊ธฐ") {
FavoriteTip().invalidate(reason: .actionPerformed)
}
// ๋ฌดํจํ ์ด์
// .actionPerformed: ์ฌ์ฉ์๊ฐ ๊ธฐ๋ฅ ์ฌ์ฉ
// .displayCountExceeded: ํ์ ํ์ ์ด๊ณผ
// .tipClosed: ์ฌ์ฉ์๊ฐ ํ ๋ซ์import SwiftUI
import TipKit
// MARK: - Tips ์ ์
struct SearchTip: Tip {
var title: Text {
Text("๊ฒ์ ๊ธฐ๋ฅ")
}
var message: Text? {
Text("์ํ๋ ํญ๋ชฉ์ ๋น ๋ฅด๊ฒ ์ฐพ์๋ณด์ธ์")
}
var image: Image? {
Image(systemName: "magnifyingglass")
}
}
struct FilterTip: Tip {
// ํ๋ผ๋ฏธํฐ๋ก ์กฐ๊ฑด ์ค์
@Parameter
static var hasUsedSearch: Bool = false
var title: Text {
Text("ํํฐ ๊ธฐ๋ฅ")
}
var message: Text? {
Text("์นดํ
๊ณ ๋ฆฌ๋ณ๋ก ํํฐ๋งํ ์ ์์ด์")
}
var image: Image? {
Image(systemName: "line.3.horizontal.decrease.circle")
}
// ํ์ ์กฐ๊ฑด: ๊ฒ์์ ์ฌ์ฉํ ํ์๋ง
var rules: [Rule] {
#Rule(Self.$hasUsedSearch) { $0 == true }
}
}
struct ShareTip: Tip {
// ์ด๋ฒคํธ ๊ธฐ๋ฐ ์กฐ๊ฑด
static let itemViewed = Event(id: "itemViewed")
var title: Text {
Text("๊ณต์ ํ๊ธฐ")
}
var message: Text? {
Text("์น๊ตฌ์๊ฒ ๊ณต์ ํด๋ณด์ธ์")
}
var image: Image? {
Image(systemName: "square.and.arrow.up")
}
// 3๋ฒ ์ด์ ์์ดํ
์ ๋ณธ ํ์๋ง
var rules: [Rule] {
#Rule(Self.itemViewed) { $0.donations.count >= 3 }
}
// ํ์ ์ต์
var options: [TipOption] {
MaxDisplayCount(3) // ์ต๋ 3๋ฒ๋ง ํ์
}
}
struct ProTip: Tip {
var title: Text {
Text("Pro ๊ธฐ๋ฅ โจ")
}
var message: Text? {
Text("๋ ๋ง์ ๊ธฐ๋ฅ์ ์ฌ์ฉํด๋ณด์ธ์")
}
// ์ก์
๋ฒํผ
var actions: [Action] {
Action(id: "learn-more", title: "์์ธํ ๋ณด๊ธฐ")
Action(id: "dismiss", title: "๋์ค์", role: .cancel)
}
}
// MARK: - App
@main
struct TipDemoApp: App {
var body: some Scene {
WindowGroup {
TipDemoView()
.task {
try? Tips.configure([
.displayFrequency(.immediate)
])
}
}
}
}
// MARK: - Views
struct TipDemoView: View {
let searchTip = SearchTip()
let filterTip = FilterTip()
let shareTip = ShareTip()
let proTip = ProTip()
@State private var searchText = ""
@State private var items = ["์ฌ๊ณผ", "๋ฐ๋๋", "์ค๋ ์ง", "ํฌ๋", "์๋ฐ"]
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// ์ธ๋ผ์ธ ํ (์๋จ)
TipView(proTip) { action in
if action.id == "learn-more" {
// Pro ํ์ด์ง๋ก ์ด๋
}
}
.tipBackground(Color.blue.opacity(0.1))
.padding()
List {
ForEach(filteredItems, id: \.self) { item in
Text(item)
.onTapGesture {
// ์์ดํ
์กฐํ ์ด๋ฒคํธ ๊ธฐ๋ก
ShareTip.itemViewed.sendDonation()
}
}
}
}
.navigationTitle("TipKit ๋ฐ๋ชจ")
.searchable(text: $searchText, prompt: "๊ฒ์")
.onChange(of: searchText) { _, newValue in
if !newValue.isEmpty {
// ๊ฒ์ ์ฌ์ฉ ๊ธฐ๋ก
FilterTip.hasUsedSearch = true
searchTip.invalidate(reason: .actionPerformed)
}
}
.toolbar {
// ๊ฒ์ ๋ฒํผ + ํ์ค๋ฒ ํ
Button {
// ๊ฒ์ ํฌ์ปค์ค
} label: {
Image(systemName: "magnifyingglass")
}
.popoverTip(searchTip)
// ํํฐ ๋ฒํผ + ํ์ค๋ฒ ํ
Button {
// ํํฐ ์ํธ
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
}
.popoverTip(filterTip)
// ๊ณต์ ๋ฒํผ + ํ์ค๋ฒ ํ
Button {
shareTip.invalidate(reason: .actionPerformed)
} label: {
Image(systemName: "square.and.arrow.up")
}
.popoverTip(shareTip)
}
}
}
var filteredItems: [String] {
if searchText.isEmpty {
return items
}
return items.filter { $0.contains(searchText) }
}
}struct AdvancedTip: Tip {
@Parameter
static var isLoggedIn: Bool = false
@Parameter
static var hasCompletedOnboarding: Bool = false
static let featureUsed = Event(id: "featureUsed")
var title: Text { Text("๊ณ ๊ธ ๊ธฐ๋ฅ") }
var rules: [Rule] {
// ๋ก๊ทธ์ธ AND ์จ๋ณด๋ฉ ์๋ฃ AND ๊ธฐ๋ฅ 2๋ฒ ์ด์ ์ฌ์ฉ
#Rule(Self.$isLoggedIn) { $0 == true }
#Rule(Self.$hasCompletedOnboarding) { $0 == true }
#Rule(Self.featureUsed) { $0.donations.count >= 2 }
}
}struct DailyTip: Tip {
static let appOpened = Event(id: "appOpened")
var title: Text { Text("์ค๋์ ํ") }
var rules: [Rule] {
// ์ค๋ ์ฑ์ ์ด์์ ๋๋ง
#Rule(Self.appOpened) {
$0.donations.filter {
Calendar.current.isDateInToday($0.date)
}.count >= 1
}
}
}struct StyledTipView: View {
let tip: some Tip
var body: some View {
TipView(tip)
.tipBackground(
LinearGradient(
colors: [.blue.opacity(0.2), .purple.opacity(0.2)],
startPoint: .leading,
endPoint: .trailing
)
)
.tipImageSize(CGSize(width: 40, height: 40))
.tipCornerRadius(16)
}
}// ๋ชจ๋ ํ ๋ฆฌ์
(๊ฐ๋ฐ์ฉ)
try? Tips.resetDatastore()
// ํน์ ํ ํ์ ๊ฐ์
Tips.showAllTipsForTesting()
// ํ ์จ๊ธฐ๊ธฐ
Tips.hideAllTipsForTesting()
// ํ ์ํ ํ์ธ
if myTip.shouldDisplay {
// ํ์ด ํ์๋์ด์ผ ํจ
}struct HighPriorityTip: Tip {
var title: Text { Text("์ค์ํ ํ") }
var options: [TipOption] {
IgnoresDisplayFrequency(true) // ๋น๋ ์ ํ ๋ฌด์
}
}
struct LowPriorityTip: Tip {
var title: Text { Text("์ผ๋ฐ ํ") }
var options: [TipOption] {
MaxDisplayCount(1) // 1๋ฒ๋ง ํ์
}
}-
Tips.configure() ํ์
- ์ฑ ์์ ์ ํ ๋ฒ ํธ์ถ
- ๋ฏธํธ์ถ ์ ํ์ด ํ์๋์ง ์์
-
displayFrequency ์ค์
.immediate: ์กฐ๊ฑด ์ถฉ์กฑ ์ ์ฆ์.daily: ํ๋ฃจ 1ํ.weekly: ์ฃผ 1ํ.monthly: ์ 1ํ
-
๋ฐ์ดํฐ ์ ์ฅ ์์น
.datastoreLocation(.applicationDefault) // ๊ธฐ๋ณธ .datastoreLocation(.groupContainer(identifier: "group.com.app")) // App Group
-
iOS 17+ ์ ์ฉ
- iOS 16 ์ดํ๋ ์ฌ์ฉ ๋ถ๊ฐ
- ์กฐ๊ฑด๋ถ import ๋๋
@available์ฌ์ฉ