๐ŸŽฏ 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 ๊ฐ€์ด๋“œ๋ผ์ธ

๐ŸŽฏ ์‹ค๋ฌด ํ™œ์šฉ

๐Ÿ“š ๋” ์•Œ์•„๋ณด๊ธฐ

โšก๏ธ ์„ฑ๋Šฅ ํŒ: ๊ด€๋ จ์„ฑ ์ ์ˆ˜๋Š” ์ž์ฃผ ์—…๋ฐ์ดํŠธํ•˜์ง€ ๋งˆ์„ธ์š”. ์˜๋ฏธ ์žˆ๋Š” ์ปจํ…์ŠคํŠธ ๋ณ€ํ™”๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ๋ฐฐํ„ฐ๋ฆฌ์™€ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜์„ธ์š”.