diff --git a/Xcode/BluetoothAccessory.xcodeproj/project.pbxproj b/Xcode/BluetoothAccessory.xcodeproj/project.pbxproj index e3d03ae..e67693c 100644 --- a/Xcode/BluetoothAccessory.xcodeproj/project.pbxproj +++ b/Xcode/BluetoothAccessory.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 6E2C9BD329CF6341002F1B28 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 6E2C9BD229CF6341002F1B28 /* CodeScanner */; }; 6E2C9BD629CF637D002F1B28 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E2C9BD529CF637D002F1B28 /* DarwinGATT */; }; 6E2C9BD829CF637D002F1B28 /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E2C9BD729CF637D002F1B28 /* GATT */; }; + 6E38D1522B96B68600A46D7D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6E38D1512B96B68600A46D7D /* Assets.xcassets */; }; 6E5B4A5E2AC4BFED00F41D5A /* SubtitleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5B4A5D2AC4BFED00F41D5A /* SubtitleRow.swift */; }; 6E5B4A602AC4C00500F41D5A /* DetailRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5B4A5F2AC4C00500F41D5A /* DetailRow.swift */; }; 6E5B4A662AC4C5AC00F41D5A /* AccessoryAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5B4A642AC4C59500F41D5A /* AccessoryAppEntity.swift */; }; @@ -68,7 +69,7 @@ 6EA074812B95E72D00D31F54 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA074802B95E72D00D31F54 /* ContentView.swift */; }; 6EA074832B95E72E00D31F54 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA074822B95E72E00D31F54 /* Assets.xcassets */; }; 6EA074862B95E72E00D31F54 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA074852B95E72E00D31F54 /* Preview Assets.xcassets */; }; - 6EA0748B2B95E72E00D31F54 /* Clip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = 6EA0747C2B95E72D00D31F54 /* Clip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6EA0748B2B95E72E00D31F54 /* Clip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = 6EA0747C2B95E72D00D31F54 /* Clip.app */; platformFilters = (ios, xros, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6EA074912B95E82700D31F54 /* BluetoothAccessory in Frameworks */ = {isa = PBXBuildFile; productRef = 6EA074902B95E82700D31F54 /* BluetoothAccessory */; }; 6EA074952B95E82700D31F54 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 6EA074942B95E82700D31F54 /* KeychainAccess */; }; 6EA074972B95E82700D31F54 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6EA074962B95E82700D31F54 /* DarwinGATT */; }; @@ -201,6 +202,7 @@ 6E2C9BBA29CF5C47002F1B28 /* BluetoothAccessoryKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothAccessoryKit.swift; sourceTree = ""; }; 6E2C9BC129CF5DBF002F1B28 /* BluetoothAccessory */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BluetoothAccessory; path = ..; sourceTree = ""; }; 6E2C9BC529CF6075002F1B28 /* BluetoothAccessoryApp-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "BluetoothAccessoryApp-Info.plist"; sourceTree = ""; }; + 6E38D1512B96B68600A46D7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6E5B4A5D2AC4BFED00F41D5A /* SubtitleRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubtitleRow.swift; sourceTree = ""; }; 6E5B4A5F2AC4C00500F41D5A /* DetailRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailRow.swift; sourceTree = ""; }; 6E5B4A642AC4C59500F41D5A /* AccessoryAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryAppEntity.swift; sourceTree = ""; }; @@ -366,6 +368,7 @@ isa = PBXGroup; children = ( 6E2C9BBA29CF5C47002F1B28 /* BluetoothAccessoryKit.swift */, + 6E38D1512B96B68600A46D7D /* Assets.xcassets */, 6EFCFFCA29D05F23004B6DE8 /* Extensions */, 6EFCFFCF29D061B3004B6DE8 /* Model */, 6EFCFFD029D078AF004B6DE8 /* Views */, @@ -721,6 +724,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E38D1522B96B68600A46D7D /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -885,6 +889,10 @@ }; 6EA0748A2B95E72E00D31F54 /* PBXTargetDependency */ = { isa = PBXTargetDependency; + platformFilters = ( + ios, + xros, + ); target = 6EA0747B2B95E72D00D31F54 /* Clip */; targetProxy = 6EA074892B95E72E00D31F54 /* PBXContainerItemProxy */; }; diff --git a/Xcode/BluetoothAccessoryKit/Assets.xcassets/AccentColor.colorset/Contents.json b/Xcode/BluetoothAccessoryKit/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..2eb0655 --- /dev/null +++ b/Xcode/BluetoothAccessoryKit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemCyanColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/BluetoothAccessoryKit/Assets.xcassets/Contents.json b/Xcode/BluetoothAccessoryKit/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Xcode/BluetoothAccessoryKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/BluetoothAccessoryKit/Model/AccessoryManagerSpotlight.swift b/Xcode/BluetoothAccessoryKit/Model/AccessoryManagerSpotlight.swift index 9a19826..7d5b708 100644 --- a/Xcode/BluetoothAccessoryKit/Model/AccessoryManagerSpotlight.swift +++ b/Xcode/BluetoothAccessoryKit/Model/AccessoryManagerSpotlight.swift @@ -6,15 +6,19 @@ // import Foundation -import BluetoothAccessory import CoreSpotlight +import SwiftUI +import BluetoothAccessory +#if canImport(SFSafeSymbols) && !APPCLIP +import SFSafeSymbols +#endif #if canImport(CoreSpotlight) && os(iOS) || os(macOS) internal extension AccessoryManager { func loadSpotlight() -> SpotlightController { - let spotlight = SpotlightController(index: .default()) - spotlight.log = { [unowned self] in self.log("🔦 Spotlight: " + $0) } + let log = { [unowned self] in self.log("🔦 Spotlight: " + $0) } + let spotlight = SpotlightController(index: .default(), log: log) return spotlight } @@ -39,12 +43,11 @@ extension PairedAccessory: CoreSpotlightSearchable { AccessoryURL.accessory(accessory).rawValue } - public func searchableAttributeSet() -> CSSearchableItemAttributeSet { + public func searchableAttributeSet() async -> CSSearchableItemAttributeSet { let attributeSet = CSSearchableItemAttributeSet(itemContentType: Swift.type(of: self).itemContentType) attributeSet.displayName = name attributeSet.contentDescription = information.type.description attributeSet.version = information.softwareVersion.description - //attributeSet.thumbnailData = Data() attributeSet.keywords = [ information.id.uuidString, information.service.description, @@ -53,8 +56,40 @@ extension PairedAccessory: CoreSpotlightSearchable { information.model, key.permission.type.localizedText ] + + // add image + #if os(iOS) + if #available(iOS 16.0, *) { + do { + let imageData = try await renderSpotlightImage() + attributeSet.thumbnailData = imageData + } + catch { + assertionFailure("\(error)") + } + } + #endif + return attributeSet } + + #if os(iOS) + @MainActor + @available(iOS 16.0, *) + private func renderSpotlightImage() throws -> Data { + let symbol = information.type.symbol + let view = Image(systemSymbol: symbol) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.accent) + .frame(width: 250, height: 250) + let renderer = ImageRenderer(content: view) + guard let pngData = renderer.uiImage?.pngData() else { + throw CocoaError(.featureUnsupported) + } + return pngData + } + #endif } #endif diff --git a/Xcode/BluetoothAccessoryKit/Model/CoreSpotlight.swift b/Xcode/BluetoothAccessoryKit/Model/CoreSpotlight.swift index 11e517f..8c6c73e 100644 --- a/Xcode/BluetoothAccessoryKit/Model/CoreSpotlight.swift +++ b/Xcode/BluetoothAccessoryKit/Model/CoreSpotlight.swift @@ -21,22 +21,26 @@ import AppKit #endif /// Manage the Spotlight index. -public final class SpotlightController { +public actor SpotlightController { // MARK: - Initialization - - public init(index: CSSearchableIndex = .default()) { + + public init( + index: CSSearchableIndex = .default(), + log: ((String) -> ())? + ) { self.index = index + self.log = log } // MARK: - Properties internal let index: CSSearchableIndex - public var log: ((String) -> ())? + public let log: ((String) -> ())? /// Returns a Boolean value that indicates whether indexing is available on the current device. - public static var isSupported: Bool { + public nonisolated static var isSupported: Bool { return CSSearchableIndex.isIndexingAvailable() } @@ -46,8 +50,18 @@ public final class SpotlightController { _ items: [T] ) async throws { - let searchableItems = items - .map { $0.searchableItem() } + let searchableItems = await withTaskGroup(of: CSSearchableItem.self, returning: [CSSearchableItem].self) { taskGroup in + for item in items { + taskGroup.addTask { + await item.searchableItem() + } + } + var results = [CSSearchableItem]() + for await result in taskGroup { + results.append(result) + } + return results + } try await index.deleteSearchableItems(withDomainIdentifiers: [T.searchDomain]) log?("Deleted all old items") @@ -65,18 +79,16 @@ public protocol CoreSpotlightSearchable { static var searchDomain: String { get } var searchIdentifier: String { get } - - func searchableItem() -> CSSearchableItem - - func searchableAttributeSet() -> CSSearchableItemAttributeSet + + func searchableAttributeSet() async -> CSSearchableItemAttributeSet } public extension CoreSpotlightSearchable { static var itemContentType: String { return UTType.text.identifier } - func searchableItem() -> CSSearchableItem { - let attributeSet = searchableAttributeSet() + func searchableItem() async -> CSSearchableItem { + let attributeSet = await searchableAttributeSet() return CSSearchableItem( uniqueIdentifier: searchIdentifier, domainIdentifier: type(of: self).searchDomain, diff --git a/Xcode/BluetoothAccessoryKit/Views/Detail/AccessoryDetailView.swift b/Xcode/BluetoothAccessoryKit/Views/Detail/AccessoryDetailView.swift index 64a918a..d309d35 100644 --- a/Xcode/BluetoothAccessoryKit/Views/Detail/AccessoryDetailView.swift +++ b/Xcode/BluetoothAccessoryKit/Views/Detail/AccessoryDetailView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI import Bluetooth import BluetoothAccessory -#if canImport(SFSafeSymbols) +#if canImport(SFSafeSymbols) && !APPCLIP import SFSafeSymbols #endif @@ -136,7 +136,7 @@ internal extension AccessoryDetailView { Image(systemSymbol: accessory.information.type.symbol) .resizable() .aspectRatio(contentMode: .fit) - .foregroundColor(.gray) + .foregroundColor(.accentColor) Spacer() } Spacer() diff --git a/Xcode/BluetoothAccessoryKit/Views/Detail/SetupAccessoryView.swift b/Xcode/BluetoothAccessoryKit/Views/Detail/SetupAccessoryView.swift index d6c15d8..54332c5 100644 --- a/Xcode/BluetoothAccessoryKit/Views/Detail/SetupAccessoryView.swift +++ b/Xcode/BluetoothAccessoryKit/Views/Detail/SetupAccessoryView.swift @@ -10,7 +10,7 @@ import SwiftUI import Bluetooth import BluetoothAccessory -#if canImport(SFSafeSymbols) +#if canImport(SFSafeSymbols) && !APPCLIP import SFSafeSymbols #endif