admin管理员组

文章数量:1391755

UPDATE: It appears that this behavior is not allowed in iOS, despite popular apps like ChatGPT being allowed to do it... To bypass this, I now just pass the URL over to the app when the app icon is clicked from the share sheet, and then I send a notification to the user that tells them to click it to continue. I think this is legal behavior under Apple's dev guidelines, but who knows anymore.

====================================================================

I've been trying to implement a simple feature: when a user shares a webpage via iOS's share sheet, clicking my app's Action Extension should launch the app and pass along the current URL for display. I set up a custom URL scheme ("myapp") in my URL Types. For example, when I type myapp://share?url=example in Safari, I get a prompt to open my app, and after I confirm, the URL is correctly passed and displayed in my app.

However, everytime I click the Action Extension, opening the app via the deeplink fails. For example, here is what is printed:

Action Extension: Deep Link formed: myapp://share?url=www.apple

Action Extension: Failed to open main app

Main App

import SwiftUI
import SwiftData

// You can keep NavigationManager here or move it to its own file.
class NavigationManager: ObservableObject {
    static let shared = NavigationManager()
    @Published var sharedURL: URL? = nil
    
    func navigateToURL(_ url: URL) {
        sharedURL = url
    }
}

// The AppDelegate that handles deep linking
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     open url: URL,
                     options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        print("AppDelegate triggered with URL: \(url)")
        // (Your URL parsing and handling code follows here)
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              components.scheme == "myapp",
              components.host == "share",
              let queryItem = components.queryItems?.first(where: { $0.name == "url" }),
              let sharedURLString = queryItem.value,
              let sharedURL = URL(string: sharedURLString) else {
            return false
        }
        
        NavigationManager.shared.navigateToURL(sharedURL)
        return true
    }

}

@main
struct MyApp: App {
    // Connect your AppDelegate to the SwiftUI lifecycle.
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @StateObject private var navManager = NavigationManager.shared
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(navManager)
                .onOpenURL { url in
                    // Optional fallback if needed; the AppDelegate should already handle it.
                    handleSharedURL(url)
                }
        }
    }

    private func handleSharedURL(_ url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              components.scheme == "myapp",
              components.host == "share",
              let queryItem = components.queryItems?.first(where: { $0.name == "url" }),
              let sharedURLString = queryItem.value,
              let sharedURL = URL(string: sharedURLString) else {
            return
        }
        navManager.navigateToURL(sharedURL)
    }
}

Action View Controller

import UIKit
import UniformTypeIdentifiers

class ActionViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Optionally hide the view if you want no visible UI
        view.isHidden = true
        processInput()
    }
    
    private func processInput() {
        print("Action Extension: processInput started")
        
        // Get the first input item
        guard let inputItem = extensionContext?.inputItems.first as? NSExtensionItem else {
            completeRequest()
            return
        }
        
        // Try to extract a URL from the item
        let urlType = UTType.url.identifier
        if let attachment = inputItem.attachments?.first,
           attachment.hasItemConformingToTypeIdentifier(urlType) {
            attachment.loadItem(forTypeIdentifier: urlType, options: nil) { [weak self] data, error in
                guard let self = self else { return }
                if let url = data as? URL {
                    self.handleURL(url)
                } else {
                    print("Action Extension: Failed to load URL from attachment")
                    selfpleteRequest()
                }
            }
        } else {
            print("Action Extension: No valid attachment found")
            completeRequest()
        }
    }
    
    private func handleURL(_ url: URL) {
        // Encode the URL to form a proper deep link
        guard let encodedURL = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
              let deepLink = URL(string: "myapp://share?url=\(encodedURL)") else {
            print("Action Extension: Error forming deep link")
            completeRequest()
            return
        }
        
        print("Action Extension: Deep Link formed: \(deepLink)")
        
        // Attempt to open the main app with the deep link
        extensionContext?.open(deepLink) { success in
            if success {
                print("Action Extension: Opened main app successfully")
            } else {
                print("Action Extension: Failed to open main app")
            }
            selfpleteRequest()
        }
    }
    
    private func completeRequest() {
        // Dismiss the extension
        extensionContext?pleteRequest(returningItems: nil, completionHandler: nil)
    }
}

Action Extension plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ".0.dtd">
<plist version="1.0">
<dict>
    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>myapp</string>
    </array>
    <key>NSExtension</key>
    <dict>
        <key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
            <string>TRUEPREDICATE</string>
        </dict>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.ui-services</string>
        <key>NSExtensionPrincipalClass</key>
        <string>$(PRODUCT_MODULE_NAME).ActionViewController</string>
    </dict>
</dict>
</plist>


Action Request Handler

import UIKit
import MobileCoreServices
import UniformTypeIdentifiers

class ActionRequestHandler: NSObject, NSExtensionRequestHandling {

    var extensionContext: NSExtensionContext?
    
    func beginRequest(with context: NSExtensionContext) {
        // Do not call super in an Action extension with no user interface
        self.extensionContext = context
        
        var found = false
        
        // Find the item containing the results from the JavaScript preprocessing.
        outer:
            for item in context.inputItems as! [NSExtensionItem] {
                if let attachments = item.attachments {
                    for itemProvider in attachments {
                        if itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) {
                            itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { (item, error) in
                                let dictionary = item as! [String: Any]
                                OperationQueue.main.addOperation {
                                    self.itemLoadCompletedWithPreprocessingResults(dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:])
                                }
                            })
                            found = true
                            break outer
                        }
                    }
                }
        }
        
        if !found {
            self.doneWithResults(nil)
        }
    }
    
    func itemLoadCompletedWithPreprocessingResults(_ javaScriptPreprocessingResults: [String: Any]) {
        // Here, do something, potentially asynchronously, with the preprocessing
        // results.
        
        // In this very simple example, the JavaScript will have passed us the
        // current background color style, if there is one. We will construct a
        // dictionary to send back with a desired new background color style.
        let bgColor: Any? = javaScriptPreprocessingResults["currentBackgroundColor"]
        if bgColor == nil ||  bgColor! as! String == "" {
            // No specific background color? Request setting the background to red.
            self.doneWithResults(["newBackgroundColor": "red"])
        } else {
            // Specific background color is set? Request replacing it with green.
            self.doneWithResults(["newBackgroundColor": "green"])
        }
    }
    
    func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) {
        if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg {
            // Construct an NSExtensionItem of the appropriate type to return our
            // results dictionary in.
            
            // These will be used as the arguments to the JavaScript finalize()
            // method.
            
            let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize]
            
            let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)
            
            let resultsItem = NSExtensionItem()
            resultsItem.attachments = [resultsProvider]
            
            // Signal that we're complete, returning our results.
            self.extensionContext!pleteRequest(returningItems: [resultsItem], completionHandler: nil)
        } else {
            // We still need to signal that we're done even if we have nothing to
            // pass back.
            self.extensionContext!pleteRequest(returningItems: [], completionHandler: nil)
        }
        
        // Don't hold on to this after we finished with it.
        self.extensionContext = nil
    }

}

Main App plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ".0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <string>com.elislothower.URLDisplayApp</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>myapp</string>
            </array>
        </dict>
    </array>
    <key>LSApplicationQueriesSchemes</key>
    <array/>
</dict>
</plist>

本文标签: swiftHow to pass a url to iOS app from share sheetStack Overflow