# App Extensions Share extensions, widgets, Quick Look previews, and Shortcuts for macOS. 1. File > New > Target > Share Extension 2. Configure activation rules in Info.plist 3. Implement share view controller **Info.plist activation rules**: ```xml NSExtension NSExtensionAttributes NSExtensionActivationRule NSExtensionActivationSupportsText NSExtensionActivationSupportsWebURLWithMaxCount 1 NSExtensionActivationSupportsImageWithMaxCount 10 NSExtensionPointIdentifier com.apple.share-services NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).ShareViewController ``` ```swift import Cocoa import Social class ShareViewController: SLComposeServiceViewController { override func loadView() { super.loadView() // Customize title title = "Save to MyApp" } override func didSelectPost() { // Get shared items guard let extensionContext = extensionContext else { return } for item in extensionContext.inputItems as? [NSExtensionItem] ?? [] { for provider in item.attachments ?? [] { if provider.hasItemConformingToTypeIdentifier("public.url") { provider.loadItem(forTypeIdentifier: "public.url") { [weak self] url, error in if let url = url as? URL { self?.saveURL(url) } } } if provider.hasItemConformingToTypeIdentifier("public.image") { provider.loadItem(forTypeIdentifier: "public.image") { [weak self] image, error in if let image = image as? NSImage { self?.saveImage(image) } } } } } extensionContext.completeRequest(returningItems: nil) } override func isContentValid() -> Bool { // Validate content before allowing post return !contentText.isEmpty } override func didSelectCancel() { extensionContext?.cancelRequest(withError: NSError(domain: "ShareExtension", code: 0)) } private func saveURL(_ url: URL) { // Save to shared container let sharedDefaults = UserDefaults(suiteName: "group.com.yourcompany.myapp") var urls = sharedDefaults?.array(forKey: "savedURLs") as? [String] ?? [] urls.append(url.absoluteString) sharedDefaults?.set(urls, forKey: "savedURLs") } private func saveImage(_ image: NSImage) { // Save to shared container guard let data = image.tiffRepresentation, let rep = NSBitmapImageRep(data: data), let pngData = rep.representation(using: .png, properties: [:]) else { return } let containerURL = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: "group.com.yourcompany.myapp" )! let imageURL = containerURL.appendingPathComponent(UUID().uuidString + ".png") try? pngData.write(to: imageURL) } } ``` Share data between app and extension: ```xml com.apple.security.application-groups group.com.yourcompany.myapp ``` ```swift // Shared UserDefaults let shared = UserDefaults(suiteName: "group.com.yourcompany.myapp") // Shared container let containerURL = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: "group.com.yourcompany.myapp" ) ``` 1. File > New > Target > Widget Extension 2. Define timeline provider 3. Create widget view ```swift import WidgetKit import SwiftUI // Timeline entry struct ItemEntry: TimelineEntry { let date: Date let items: [Item] } // Timeline provider struct ItemProvider: TimelineProvider { func placeholder(in context: Context) -> ItemEntry { ItemEntry(date: Date(), items: [.placeholder]) } func getSnapshot(in context: Context, completion: @escaping (ItemEntry) -> Void) { let entry = ItemEntry(date: Date(), items: loadItems()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let items = loadItems() let entry = ItemEntry(date: Date(), items: items) // Refresh every hour let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())! let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) completion(timeline) } private func loadItems() -> [Item] { // Load from shared container let shared = UserDefaults(suiteName: "group.com.yourcompany.myapp") // ... deserialize items return [] } } // Widget view struct ItemWidgetView: View { var entry: ItemEntry var body: some View { VStack(alignment: .leading) { Text("Recent Items") .font(.headline) ForEach(entry.items.prefix(3)) { item in HStack { Image(systemName: item.icon) Text(item.name) } .font(.caption) } } .padding() } } // Widget configuration @main struct ItemWidget: Widget { let kind = "ItemWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: ItemProvider()) { entry in ItemWidgetView(entry: entry) } .configurationDisplayName("Recent Items") .description("Shows your most recent items") .supportedFamilies([.systemSmall, .systemMedium]) } } ``` ```swift struct ItemWidgetView: View { var entry: ItemEntry var body: some View { VStack { ForEach(entry.items) { item in Link(destination: URL(string: "myapp://item/\(item.id)")!) { Text(item.name) } } } .widgetURL(URL(string: "myapp://widget")) } } // Handle in main app @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in handleURL(url) } } } func handleURL(_ url: URL) { // Parse myapp://item/123 if url.host == "item", let id = url.pathComponents.last { // Navigate to item } } } ``` ```swift // From main app, tell widget to refresh import WidgetKit func itemsChanged() { WidgetCenter.shared.reloadTimelines(ofKind: "ItemWidget") } // Reload all widgets WidgetCenter.shared.reloadAllTimelines() ``` 1. File > New > Target > Quick Look Preview Extension 2. Implement preview view controller ```swift import Cocoa import Quartz class PreviewViewController: NSViewController, QLPreviewingController { @IBOutlet var textView: NSTextView! func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) { do { let content = try loadDocument(at: url) textView.string = content.text handler(nil) } catch { handler(error) } } private func loadDocument(at url: URL) throws -> DocumentContent { let data = try Data(contentsOf: url) return try JSONDecoder().decode(DocumentContent.self, from: data) } } ``` 1. File > New > Target > Thumbnail Extension ```swift import QuickLookThumbnailing class ThumbnailProvider: QLThumbnailProvider { override func provideThumbnail( for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void ) { let size = request.maximumSize handler(QLThumbnailReply(contextSize: size) { context -> Bool in // Draw thumbnail let content = self.loadContent(at: request.fileURL) self.drawThumbnail(content, in: context, size: size) return true }, nil) } private func drawThumbnail(_ content: DocumentContent, in context: CGContext, size: CGSize) { // Draw background context.setFillColor(NSColor.white.cgColor) context.fill(CGRect(origin: .zero, size: size)) // Draw content preview // ... } } ``` ```swift import AppIntents // Define intent struct CreateItemIntent: AppIntent { static var title: LocalizedStringResource = "Create Item" static var description = IntentDescription("Creates a new item in MyApp") @Parameter(title: "Name") var name: String @Parameter(title: "Folder", optionsProvider: FolderOptionsProvider()) var folder: String? func perform() async throws -> some IntentResult & ProvidesDialog { let item = Item(name: name) if let folderName = folder { item.folder = findFolder(named: folderName) } try await DataService.shared.save(item) return .result(dialog: "Created \(name)") } } // Options provider struct FolderOptionsProvider: DynamicOptionsProvider { func results() async throws -> [String] { let folders = try await DataService.shared.fetchFolders() return folders.map { $0.name } } } // Register shortcuts struct MyAppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: CreateItemIntent(), phrases: [ "Create item in \(.applicationName)", "New \(.applicationName) item" ], shortTitle: "Create Item", systemImageName: "plus.circle" ) } } ``` ```swift // Define entity struct ItemEntity: AppEntity { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Item") var id: UUID var name: String var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") } static var defaultQuery = ItemQuery() } // Define query struct ItemQuery: EntityQuery { func entities(for identifiers: [UUID]) async throws -> [ItemEntity] { let items = try await DataService.shared.fetchItems(ids: identifiers) return items.map { ItemEntity(id: $0.id, name: $0.name) } } func suggestedEntities() async throws -> [ItemEntity] { let items = try await DataService.shared.recentItems(limit: 10) return items.map { ItemEntity(id: $0.id, name: $0.name) } } } // Use in intent struct OpenItemIntent: AppIntent { static var title: LocalizedStringResource = "Open Item" @Parameter(title: "Item") var item: ItemEntity func perform() async throws -> some IntentResult { // Open item in app NotificationCenter.default.post( name: .openItem, object: nil, userInfo: ["id": item.id] ) return .result() } } ``` ```swift import Cocoa class ActionViewController: NSViewController { @IBOutlet var textView: NSTextView! override func viewDidLoad() { super.viewDidLoad() // Get input items for item in extensionContext?.inputItems as? [NSExtensionItem] ?? [] { for provider in item.attachments ?? [] { if provider.hasItemConformingToTypeIdentifier("public.text") { provider.loadItem(forTypeIdentifier: "public.text") { [weak self] text, _ in DispatchQueue.main.async { self?.textView.string = text as? String ?? "" } } } } } } @IBAction func done(_ sender: Any) { // Return modified content let outputItem = NSExtensionItem() outputItem.attachments = [ NSItemProvider(item: textView.string as NSString, typeIdentifier: "public.text") ] extensionContext?.completeRequest(returningItems: [outputItem]) } @IBAction func cancel(_ sender: Any) { extensionContext?.cancelRequest(withError: NSError(domain: "ActionExtension", code: 0)) } } ``` - Share data via App Groups - Keep extensions lightweight (memory limits) - Handle errors gracefully - Test in all contexts (Finder, Safari, etc.) - Update Info.plist activation rules carefully - Use WidgetCenter.shared.reloadTimelines() to update widgets - Define clear App Intents with good phrases