๐ŸŒ KO

๐ŸŽฏ 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

๐ŸŽฏ Practical Applications

๐Ÿ“š Learn More

โšก๏ธ Performance Tips: Don't update relevance scores too frequently. Only update on meaningful context changes to optimize battery and performance.

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐ŸŽฌ WWDC Sessions