๐ฏ RelevanceKit
โญ Difficulty: โญโญโญ
โฑ๏ธ Est. Time: 2h
๐ iOS 26
์ปจํ ์คํธ ์ธ์ ๊ธฐ๋ฐ ์ฑ ์ ์ ๋ฐ ์ฐ์ ์์ ๊ด๋ฆฌ
iOS 18+๐ 2024
โจ RelevanceKit is?
RelevanceKit is a framework introduced in iOS 18 that lets your app communicate relevance to the system based on the user's current context (location, time, activity, etc.). This ensures your app surfaces at the right time in Spotlight search, Live Activity priority, Siri Suggestions and more.
๐ก Key Features: Context Relevance Signals ยท Live Activity Priority ยท Spotlight Integration ยท User Intent Prediction ยท Background Updates ยท Privacy Protection
๐ฏ 1. Basic Setup
Define your app's relevance context using RelevanceKit.
RelevanceManager.swift โ Basic Setup
import RelevanceKit import Foundation @Observable class RelevanceManager { var currentRelevance: Double = 0.5 private let relevanceContext = RKRelevanceContext() // ๊ด๋ จ์ฑ ์ ๋ฐ์ดํธ func updateRelevance(score: Double, reason: String) async { currentRelevance = score // ์์คํ ์ ๊ด๋ จ์ฑ ์ ํธ ์ ๋ฌ await relevanceContext.updateRelevance( score: score, metadata: [ "reason": reason, "timestamp": Date().timeIntervalSince1970 ] ) } // ์ปจํ ์คํธ ๊ธฐ๋ฐ ๊ด๋ จ์ฑ ๊ณ์ฐ func calculateContextualRelevance( location: String?, timeOfDay: String, userActivity: String ) -> Double { var score = 0.0 // ์์น ๊ธฐ๋ฐ ์ ์ if location == "gym" { score += 0.3 } // ์๊ฐ ๊ธฐ๋ฐ ์ ์ if timeOfDay == "morning" { score += 0.2 } // ํ๋ ๊ธฐ๋ฐ ์ ์ if userActivity == "workout" { score += 0.5 } return min(score, 1.0) } }
๐ 2. Live Activity ์ฐ์ ์์
Dynamically adjust Live Activity priority.
LiveActivityRelevance.swift โ Live Activity Priority
import ActivityKit import RelevanceKit struct DeliveryAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var status: String var estimatedTime: Date var relevanceScore: Double // ๐ ๊ด๋ จ์ฑ ์ ์ } var orderId: String } @Observable class LiveActivityRelevanceManager { private var activity: Activity<DeliveryAttributes>? // Live Activity ์์ (๊ด๋ จ์ฑ ํฌํจ) func startActivity(orderId: String) async throws { let attributes = DeliveryAttributes(orderId: orderId) let initialState = DeliveryAttributes.ContentState( status: "์ค๋น ์ค", estimatedTime: Date().addingTimeInterval(1800), relevanceScore: 0.5 ) activity = try Activity.request( attributes: attributes, content: .init(state: initialState, staleDate: nil), pushType: nil ) } // ๋ฐฐ๋ฌ ์๋ฐ ์ ์ฐ์ ์์ ์ฆ๊ฐ func updateForIncomingDelivery() async { guard let activity else { return } let updatedState = DeliveryAttributes.ContentState( status: "๋ฐฐ๋ฌ ์ค - 5๋ถ ๋จ์", estimatedTime: Date().addingTimeInterval(300), relevanceScore: 0.95 // ๐ ๋์ ์ฐ์ ์์ ) await activity.update(using: updatedState) } // ๋ฐฐ๋ฌ ์๋ฃ ์ ์ฐ์ ์์ ๊ฐ์ func updateForCompletion() async { guard let activity else { return } let finalState = DeliveryAttributes.ContentState( status: "๋ฐฐ๋ฌ ์๋ฃ", estimatedTime: Date(), relevanceScore: 0.1 // ๐ ๋ฎ์ ์ฐ์ ์์ ) await activity.update(using: finalState) // 5์ด ํ ์ข ๋ฃ try? await Task.sleep(for: .seconds(5)) await activity.end(nil, dismissalPolicy: .immediate) } }
๐ 3. Spotlight ํตํฉ
Adjust context-based ranking in Spotlight search results.
SpotlightRelevance.swift โ Spotlight Integration
import CoreSpotlight import RelevanceKit @Observable class SpotlightRelevanceManager { // ์ปจํ ์คํธ ๊ธฐ๋ฐ ๊ฒ์ ํญ๋ชฉ ์์ธ func indexWithRelevance( title: String, description: String, contextScore: Double ) async { let attributeSet = CSSearchableItemAttributeSet( contentType: .init(rawValue: "public.content") ) attributeSet.title = title attributeSet.contentDescription = description // ๐ ๊ด๋ จ์ฑ ์ ์ ์ค์ attributeSet.rankingHint = contextScore // ์ปจํ ์คํธ ๋ฉํ๋ฐ์ดํฐ attributeSet.keywords = [ "relevant", "priority-\(Int(contextScore * 10))" ] let item = CSSearchableItem( uniqueIdentifier: UUID().uuidString, domainIdentifier: "com.app.relevance", attributeSet: attributeSet ) try? await CSSearchableIndex.default().indexSearchableItems([item]) } // ์ด๋ ์๊ฐ์ ํผํธ๋์ค ์ฝํ ์ธ ์ฐ์ ์์ ์ฆ๊ฐ func indexWorkoutContent() async { let currentHour = Calendar.current.component(.hour, from: Date()) let isWorkoutTime = (6...9).contains(currentHour) || (17...20).contains(currentHour) await indexWithRelevance( title: "์ค๋์ ์ด๋ ๋ฃจํด", description: "๋ง์ถคํ ์ด๋ ๊ณํ", contextScore: isWorkoutTime ? 0.9 : 0.3 ) } // ์์น ๊ธฐ๋ฐ ๊ด๋ จ์ฑ func indexLocationBasedContent(nearGym: Bool) async { await indexWithRelevance( title: "์ฒด์ก๊ด ํ์๊ถ", description: "ํ์๊ถ ์ ๋ณด ํ์ธ", contextScore: nearGym ? 0.95 : 0.2 ) } }
๐ง 4. ์ฌ์ฉ์ ์๋ ์์ธก
Learn user patterns to optimize when to suggest the app.
IntentPrediction.swift โ Intent Prediction
import RelevanceKit import CoreLocation @Observable class IntentPredictionManager { private var userPatterns: [String: [Date]] = [:] private let relevanceContext = RKRelevanceContext() // ์ฌ์ฉ์ ํ๋ ํจํด ๊ธฐ๋ก func recordActivity(_ activity: String) { if userPatterns[activity] == nil { userPatterns[activity] = [] } userPatterns[activity]?.append(Date()) } // ํจํด ๊ธฐ๋ฐ ๊ด๋ จ์ฑ ์์ธก func predictRelevance(for activity: String) -> Double { guard let history = userPatterns[activity] else { return 0.1 } let now = Date() let calendar = Calendar.current let currentHour = calendar.component(.hour, from: now) // ๊ฐ์ ์๊ฐ๋ ํ๋ ๋น๋ ๊ณ์ฐ let sameHourActivities = history.filter { calendar.component(.hour, from: $0) == currentHour } let frequency = Double(sameHourActivities.count) / Double(history.count) return min(frequency * 2.0, 1.0) } // ์์คํ ์ ์์ธก ์ ๋ฌ func updatePredictedRelevance(for activity: String) async { let score = predictRelevance(for: activity) await relevanceContext.updateRelevance( score: score, metadata: [ "activity": activity, "prediction_confidence": score, "timestamp": Date().timeIntervalSince1970 ] ) } } // ์ค์ฌ์ฉ ์์ struct WorkoutPredictionView: View { @State private var predictor = IntentPredictionManager() @State private var relevanceScore = 0.0 var body: some View { VStack { Text("์ด๋ ๊ด๋ จ์ฑ: \(Int(relevanceScore * 100))%") .font(.title) Button("์ด๋ ์์") { predictor.recordActivity("workout") Task { await predictor.updatePredictedRelevance(for: "workout") } } } .task { relevanceScore = predictor.predictRelevance(for: "workout") } } }
๐ 5. ์์น ๊ธฐ๋ฐ ๊ด๋ จ์ฑ
์์น ์ปจํ ์คํธ๋ฅผ ํ์ฉํ ๊ด๋ จ์ฑ ๊ด๋ฆฌ.
LocationRelevance.swift โ Location-Based Relevance
import RelevanceKit import CoreLocation @Observable class LocationRelevanceManager: NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() private let relevanceContext = RKRelevanceContext() var currentRelevance = 0.0 // ๊ด์ฌ ์์น ์ ์ private let gymLocation = CLLocationCoordinate2D( latitude: 37.5665, longitude: 126.9780 ) override init() { super.init() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters } // ์์น ๋ชจ๋ํฐ๋ง ์์ func startMonitoring() { locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } // ์์น ์ ๋ฐ์ดํธ ์ฒ๋ฆฌ func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { guard let location = locations.last else { return } // ์ฒด์ก๊ด๊ณผ์ ๊ฑฐ๋ฆฌ ๊ณ์ฐ let gymCLLocation = CLLocation( latitude: gymLocation.latitude, longitude: gymLocation.longitude ) let distance = location.distance(from: gymCLLocation) // ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ๊ด๋ จ์ฑ ๊ณ์ฐ let relevance: Double if distance < 100 { relevance = 1.0 // ๋งค์ฐ ๊ฐ๊น์ } else if distance < 500 { relevance = 0.7 // ๊ฐ๊น์ } else if distance < 1000 { relevance = 0.4 // ์ค๊ฐ } else { relevance = 0.1 // ๋ฉ์ } currentRelevance = relevance // ์์คํ ์ ์ ๋ฌ Task { await relevanceContext.updateRelevance( score: relevance, metadata: [ "distance": distance, "location": "gym" ] ) } } }
๐ฑ SwiftUI Integration
RelevanceApp.swift โ Complete Example
import SwiftUI import RelevanceKit struct RelevanceApp: View { @State private var relevanceManager = RelevanceManager() @State private var liveActivityManager = LiveActivityRelevanceManager() @State private var locationManager = LocationRelevanceManager() var body: some View { NavigationStack { List { Section("ํ์ฌ ๊ด๋ จ์ฑ") { HStack { Text("์์คํ ๊ด๋ จ์ฑ") Spacer() Text("\(Int(relevanceManager.currentRelevance * 100))%") .foregroundStyle(.secondary) } HStack { Text("์์น ๊ธฐ๋ฐ") Spacer() Text("\(Int(locationManager.currentRelevance * 100))%") .foregroundStyle(.secondary) } } Section("์ก์ ") { Button("์ด๋ ์์") { Task { await relevanceManager.updateRelevance( score: 0.9, reason: "workout_started" ) } } Button("๋ฐฐ๋ฌ ์์") { Task { try? await liveActivityManager.startActivity( orderId: "ORDER123" ) } } Button("์์น ์ถ์ ์์") { locationManager.startMonitoring() } } } .navigationTitle("RelevanceKit") } } }
๐ก HIG Guidelines
- Privacy: ์์น ๋ฑ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ฌ์ฉ ์ ๋ช ํํ ์ด์ ์ค๋ช
- ๋ฐฐํฐ๋ฆฌ: ๋ฐฑ๊ทธ๋ผ์ด๋ ์ ๋ฐ์ดํธ ์ต์ํ, ํจ์จ์ ์ธ ๊ด๋ จ์ฑ ๊ณ์ฐ
- ์ ํ์ฑ: ๊ด๋ จ์ฑ ์ ์๋ ์ค์ ์ฌ์ฉ์ ์ปจํ ์คํธ์ ์ผ์นํด์ผ ํจ
- ํฌ๋ช ์ฑ: ์ ์ฑ์ด ์ ์๋๋์ง ์ฌ์ฉ์๊ฐ ์ดํดํ ์ ์์ด์ผ ํจ
- ๊ถํ: In Info.plist, ์์น ๊ถํ ์ค๋ช ์ถ๊ฐ
๐ฏ Practical Applications
- Fitness Apps: ์ฒด์ก๊ด ๊ทผ์ฒ์์ ์ด๋ ๋ฃจํด ์ ์
- ๋ฐฐ๋ฌ ์ฑ: ๋ฐฐ๋ฌ ์๋ฐ ์ Live Activity ์ฐ์ ํ์
- ๊ตํต ์ฑ: ์ถํด๊ทผ ์๊ฐ์ ๊ฒฝ๋ก ์ ์ ์ฐ์ ์์ ์ฆ๊ฐ
- Music Apps: ์ด๋ ์๊ฐ์ ์ํฌ์์ ํ๋ ์ด๋ฆฌ์คํธ ์ ์
- ์ผํ ์ฑ: ๋งค์ฅ ๊ทผ์ฒ์์ ์ฟ ํฐ ๋ฐ ํ ์ธ ์ ๋ณด ๊ฐ์กฐ
๐ Learn More
โก๏ธ Performance Tips: Don't update relevance scores too frequently. Only update on meaningful context changes to optimize battery and performance.