๐ฏ RelevanceKit
์ปจํ ์คํธ ์ธ์ ๊ธฐ๋ฐ ์ฑ ์ ์ ๋ฐ ์ฐ์ ์์ ๊ด๋ฆฌ
iOS 18+๐ 2024
โจ RelevanceKit์ด๋?
RelevanceKit์ iOS 18์์ ๋์ ๋ ํ๋ ์์ํฌ๋ก, ์ฑ์ด ์ฌ์ฉ์์ ํ์ฌ ์ปจํ ์คํธ(์์น, ์๊ฐ, ํ๋ ๋ฑ)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์คํ ์ ๊ด๋ จ์ฑ์ ์ ๋ฌํ ์ ์๊ฒ ํฉ๋๋ค. ์ด๋ฅผ ํตํด Spotlight ๊ฒ์, Live Activity ์ฐ์ ์์, Siri ์ ์ ๋ฑ์์ ์ฑ์ด ์ ์ ํ ์์ ์ ๋ ธ์ถ๋ฉ๋๋ค.
๐ก ํต์ฌ ๊ธฐ๋ฅ: ์ปจํ
์คํธ ๊ด๋ จ์ฑ ์ ํธ ยท Live Activity ์ฐ์ ์์ ยท Spotlight ํตํฉ ยท ์ฌ์ฉ์ ์๋ ์์ธก ยท ๋ฐฑ๊ทธ๋ผ์ด๋ ์
๋ฐ์ดํธ ยท ํ๋ผ์ด๋ฒ์ ๋ณดํธ
๐ฏ 1. ๊ธฐ๋ณธ ์ค์
RelevanceKit์ ์ฌ์ฉํ์ฌ ์ฑ์ ๊ด๋ จ์ฑ ์ปจํ ์คํธ๋ฅผ ์ ์ํฉ๋๋ค.
RelevanceManager.swift โ ๊ธฐ๋ณธ ์ค์
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 ์ฐ์ ์์
Live Activity์ ์ฐ์ ์์๋ฅผ ๋์ ์ผ๋ก ์กฐ์ ํฉ๋๋ค.
LiveActivityRelevance.swift โ Live Activity ์ฐ์ ์์
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 ํตํฉ
Spotlight ๊ฒ์ ๊ฒฐ๊ณผ์์ ์ปจํ ์คํธ ๊ธฐ๋ฐ ์์๋ฅผ ์กฐ์ ํฉ๋๋ค.
SpotlightRelevance.swift โ Spotlight ํตํฉ
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. ์ฌ์ฉ์ ์๋ ์์ธก
์ฌ์ฉ์ ํจํด์ ํ์ตํ์ฌ ์ฑ ์ ์ ์์ ์ ์ต์ ํํฉ๋๋ค.
IntentPrediction.swift โ ์๋ ์์ธก
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 โ ์์น ๊ธฐ๋ฐ ๊ด๋ จ์ฑ
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 ํตํฉ
RelevanceApp.swift โ ์ข
ํฉ ์์
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 ๊ฐ์ด๋๋ผ์ธ
- ํ๋ผ์ด๋ฒ์: ์์น ๋ฑ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ฌ์ฉ ์ ๋ช ํํ ์ด์ ์ค๋ช
- ๋ฐฐํฐ๋ฆฌ: ๋ฐฑ๊ทธ๋ผ์ด๋ ์ ๋ฐ์ดํธ ์ต์ํ, ํจ์จ์ ์ธ ๊ด๋ จ์ฑ ๊ณ์ฐ
- ์ ํ์ฑ: ๊ด๋ จ์ฑ ์ ์๋ ์ค์ ์ฌ์ฉ์ ์ปจํ ์คํธ์ ์ผ์นํด์ผ ํจ
- ํฌ๋ช ์ฑ: ์ ์ฑ์ด ์ ์๋๋์ง ์ฌ์ฉ์๊ฐ ์ดํดํ ์ ์์ด์ผ ํจ
- ๊ถํ: Info.plist์ ์์น ๊ถํ ์ค๋ช ์ถ๊ฐ
๐ฏ ์ค๋ฌด ํ์ฉ
- ํผํธ๋์ค ์ฑ: ์ฒด์ก๊ด ๊ทผ์ฒ์์ ์ด๋ ๋ฃจํด ์ ์
- ๋ฐฐ๋ฌ ์ฑ: ๋ฐฐ๋ฌ ์๋ฐ ์ Live Activity ์ฐ์ ํ์
- ๊ตํต ์ฑ: ์ถํด๊ทผ ์๊ฐ์ ๊ฒฝ๋ก ์ ์ ์ฐ์ ์์ ์ฆ๊ฐ
- ์์ ์ฑ: ์ด๋ ์๊ฐ์ ์ํฌ์์ ํ๋ ์ด๋ฆฌ์คํธ ์ ์
- ์ผํ ์ฑ: ๋งค์ฅ ๊ทผ์ฒ์์ ์ฟ ํฐ ๋ฐ ํ ์ธ ์ ๋ณด ๊ฐ์กฐ
๐ ๋ ์์๋ณด๊ธฐ
โก๏ธ ์ฑ๋ฅ ํ: ๊ด๋ จ์ฑ ์ ์๋ ์์ฃผ ์
๋ฐ์ดํธํ์ง ๋ง์ธ์. ์๋ฏธ ์๋ ์ปจํ
์คํธ ๋ณํ๊ฐ ์์ ๋๋ง ์
๋ฐ์ดํธํ์ฌ ๋ฐฐํฐ๋ฆฌ์ ์ฑ๋ฅ์ ์ต์ ํํ์ธ์.