Document Creation with UIDocumentBrowserViewController - ios

The documentation for documentBrowser(_:didRequestDocumentCreationWithHandler:) says, "Create a new document and save it to a temporary location. If you use a UIDocument subclass to create the document, you must close it before calling the importHandler block."
So I created a file URL by taking the URL for the user's temporary directory (FileManager.default.temporaryDirectory) and appending a name and extension (getting a path like "file:///private/var/mobile/Containers/Data/Application/C1DE454D-EA1E-4166-B137-5B43185169D8/tmp/Untitled.uti"). But when I call save(to:for:completionHandler:) passing this URL, the completion handler is never called back. I also tried using url(for:in:appropriateFor:create:) to pass a subdirectory in the user's temporary directory—the completion handler was still never called.
I understand the document browser view controller is managed by a separate process, which has its own read / write permissions. Beyond that though, I'm having a hard time understanding what the problem is. Where can new documents be temporarily saved so that the document browser process can move them?
Update: as of the current betas, I now see an error with domain NSFileProviderInternalErrorDomain and code 1 getting logged: "The reader is not permitted to access the URL." At least that's confirmation of what's happening…

So, to start with, if you're using a custom UTI, it's got to be set up correctly. Mine look like this…
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array>
<string>icon-file-name</string> // Can be excluded, but keep the array
</array>
<key>CFBundleTypeName</key>
<string>Your Document Name</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.custom-uti</string>
</array>
</dict>
</array>
and
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string> // My doc is saved as Data, not a file wrapper
</array>
<key>UTTypeDescription</key>
<string>Your Document Name</string>
<key>UTTypeIdentifier</key>
<string>com.custom-uti</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>doc-extension</string>
</array>
</dict>
</dict>
</array>
Also
<key>UISupportsDocumentBrowser</key>
<true/>
I subclass UIDocument as MyDocument and add the following method to create a new temp document…
static func create(completion: #escaping Result<MyDocument> -> Void) throws {
let targetURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Untitled").appendingPathExtension("doc-extension")
coordinationQueue.async {
let document = MyDocument(fileURL: targetURL)
var error: NSError? = nil
NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: targetURL, error: &error) { url in
document.save(to: url, for: .forCreating) { success in
DispatchQueue.main.async {
if success {
completion(.success(document))
} else {
completion(.failure(MyDocumentError.unableToSaveDocument))
}
}
}
}
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
Then init and display the DBVC as follows:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var documentBrowser: UIDocumentBrowserViewController = {
let utiDecs = Bundle.main.object(forInfoDictionaryKey: kUTExportedTypeDeclarationsKey as String) as! [[String: Any]]
let uti = utiDecs.first?[kUTTypeIdentifierKey as String] as! String
let dbvc = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes:[uti])
dbvc.delegate = self
return dbvc
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = documentBrowser
window?.makeKeyAndVisible()
return true
}
}
And my delegate methods are as follows:
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Swift.Void) {
do {
try MyDocument.create() { result in
switch result {
case let .success(document):
// .move as I'm moving a temp file, if you're using a template
// this will be .copy
importHandler(document.fileURL, .move)
case let .failure(error):
// Show error
importHandler(nil, .none)
}
}
} catch {
// Show error
importHandler(nil, .none)
}
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) {
let document = MyDocument(fileURL: destinationURL)
document.open { success in
if success {
// Modally present DocumentViewContoller for document
} else {
// Show error
}
}
}
And that's pretty much it. Let me know how you get on!

I had the same issue, but then I realized that the recommended way was to simply copy the package/folder from the Bundle, like so:
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
if let url = Bundle.main.url(forResource: "Your Already Created Package", withExtension: "your-package-extension") {
importHandler(url, .copy)
} else {
importHandler(nil, .none)
}
}
To clarify, this package is just a folder that you've created and plopped into Xcode.
This approach makes sense, if you think about it, for a few reasons:
Apple File System (AFS). The move towards AFS means copying is (almost) free.
Permissions. Copying from the Bundle is always permissible, and the user is specifying the location to copy to.
New document browser paradigm. Since we're using the new UIDocumentBrowserViewController paradigm (which is due to iOS11 and the new Files app), it is even handling the naming (c.f. Apple's Pages) and moving and arranging of files. We don't have to worry about which thread to run things on either.
So. Simpler, easier, and probably better. I can't think of a reason to manually create all the files (or use the temp folder, etc).

Test on the device, not in the Simulator. The minute I switched to testing on the device, everything just started working correctly. (NOTE: It may be that the Simulator failures occur only for Sierra users like myself.)

Related

Share extension on iOS won't open my react-native app

I have a React Native project and I am trying to build Share extension using Xcode and swift. I have tried using https://github.com/meedan/react-native-share-menu but it won't work for me. I have tried other lib as well but they are not maintained properly so I decided to build one of my own.
I only want to handle urls and text in my app.
I first create a Share extension and named it as Share
Following is my info.plist file for Share extension
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Share</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionPrincipalClass</key>
<string>Share.ShareViewController</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
Following is my code for ShareViewController
import UIKit
import Social
import CoreServices
class ShareViewController: UIViewController {
private let typeText = String(kUTTypeText)
private let typeURL = String(kUTTypeURL)
private let appURL = "leaaoShare://"
private let groupName = "group.leaaoShare"
private let urlDefaultName = "incomingURL"
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 1
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = extensionItem.attachments?.first else {
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
return
}
if itemProvider.hasItemConformingToTypeIdentifier(typeText) {
handleIncomingText(itemProvider: itemProvider)
} else if itemProvider.hasItemConformingToTypeIdentifier(typeURL) {
handleIncomingURL(itemProvider: itemProvider)
} else {
print("Error: No url or text found")
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
private func handleIncomingText(itemProvider: NSItemProvider) {
print("here22")
itemProvider.loadItem(forTypeIdentifier: typeText, options: nil) { (item, error) in
if let error = error { print("Text-Error: \(error.localizedDescription)") }
if let text = item as? String {
do {// 2.1
let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(
in: text,
options: [],
range: NSRange(location: 0, length: text.utf16.count)
)
// 2.2
if let firstMatch = matches.first, let range = Range(firstMatch.range, in: text) {
self.saveURLString(String(text[range]))
}
} catch let error {
print("Do-Try Error: \(error.localizedDescription)")
}
}
self.openMainApp()
}
}
private func handleIncomingURL(itemProvider: NSItemProvider) {
print("here")
itemProvider.loadItem(forTypeIdentifier: typeURL, options: nil) { (item, error) in
if let error = error { print("URL-Error: \(error.localizedDescription)") }
if let url = item as? NSURL, let urlString = url.absoluteString {
self.saveURLString(urlString)
}
self.openMainApp()
}
}
private func saveURLString(_ urlString: String) {
UserDefaults(suiteName: self.groupName)?.set(urlString, forKey: self.urlDefaultName)
}
private func openMainApp() {
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: { _ in
guard let url = URL(string: self.appURL) else { return }
_ = self.openURL(url)
})
}
// Courtesy: https://stackoverflow.com/a/44499222/13363449 👇🏾
// Function must be named exactly like this so a selector can be found by the compiler!
// Anyway - it's another selector in another instance that would be "performed" instead.
#objc private func openURL(_ url: URL) -> Bool {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
return application.perform(#selector(openURL(_:)), with: url) != nil
}
responder = responder?.next
}
return false
}
}
Following is my AppDelegate code
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if let scheme = url.scheme,
scheme.caseInsensitiveCompare("leaaoShare") == .orderedSame,
let page = url.host {
var parameters: [String: String] = [:]
URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.forEach {
parameters[$0.name] = $0.value
}
print("redirect(to: \(page), with: \(parameters))")
// for parameter in parameters where parameter.key.caseInsensitiveCompare("url") == .orderedSame {
// UserDefaults().set(parameter.value, forKey: "incomingURL")
// }
}
return true
}
I have also created App groups for my main and share extension project
When I try to share url from safari, I am able to see my app in the share sheet but when I click on my app nothing happens. It feels like my app crashes or something. I don't see anything in Xcode console as well. Also when I click my app from share sheet , I cannot see the post dialog which iOS shows when sharing urls. Same issue happens when sharing text
I created a small native iOS app with Swift and it is available at https://github.com/PritishSawant/iosDevShare. I am following this article https://medium.com/#damisipikuda/how-to-receive-a-shared-content-in-an-ios-application-4d5964229701 to create extension
I figured out the issue. I forgot to add following in my main app's info plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>leaaoShare</string>
</array>
</dict>
</array>

How to set root view controller in Today extension programmatically

I have a project without storyboard. I've set root view controller in AppDelegate. Now I've added a today extension. I got a new storyboard MainInterface.storyboard with root view controller TodayViewController. How can I delete MainInterface.storyboard and set root view controller programmatically?
Xcode 11.0
Open Info.plist from your Extension and change
<dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
...
</dict>
to:
<dict>
<key>NSExtensionPrincipalClass</key>
<string>NameOfYourExtension.NameOfYourViewController</string>
...
</dict>
Delete MainInterface.storyboard and it should work without any faults.
Using the URL Scheme to achieve your goal, See this code.
Write the following code in TodayViewController (Today extension swift file)
#IBAction func buttonTapped(_ sender: UIButton) {
let url = URL(string: "MyAppTodayExtension://")!
self.extensionContext?.open(url, completionHandler: { (success) in
if (!success) {
print("error: failed to open app from Today Extension")
}
})
}
Add URL Scheme in your info.plist file
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>MyAppTodayExtension</string>
</array>
</dict>
</array>
When you tap in today extension then get call following method in AppDelegate.swift file.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
//Write code here for navigating to a specific controller OR set root view controller
return true
}

Open uber from my app with pickup and dropoff location prefilled without using sdk

I want to open uber from click of button in my app with pickup and dropoff location prefilled without using uber sdk. I followed the link which suggest deep linking but its not working : https://developer.uber.com/docs/riders/ride-requests/tutorials/deep-links/introduction
Below given is my code
Firstly added uber as in here:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>uber</string>
</array>
Then added this piece of code in button action:
let url = NSURL(string:
"uber://?client_id=oR5_kM9B8Hsxf9BKAXZl7Pm6IcL38n9w&action=setPickup&pickup[latitude]=37.775818&pickup[longitude]=-122.418028&pickup[nickname]=UberHQ&pickup[formatted_address]=1455%20Market%20St%2C%20San%20Francisco%2C%20CA%2094103&dropoff[latitude]=37.802374&dropoff[longitude]=-122.405818&dropoff[nickname]=Coit%20Tower&dropoff[formatted_address]=1%20Telegraph%20Hill%20Blvd%2C%20San%20Francisco%2C%20CA%2094133&product_id=a1111c8c-c720-46c3-8534-2fcdd730040d&link_text=View%20team%20roster&partner_deeplink=partner%3A%2F%2Fteam%2F9383")
if UIApplication.shared.canOpenURL(url! as URL){
UIApplication.shared.openURL(url! as URL)
}
I also just simply tried to open uber from my app, even that is not working. Please suggest some solution. Thanks in advance!
Try this:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>uber</string>
</array>
let url = URL(string:
"uber://?client_id=oR5_kM9B8Hsxf9BKAXZl7Pm6IcL38n9w&action=setPickup&pickup[latitude]=37.775818&pickup[longitude]=-122.418028&pickup[nickname]=UberHQ&pickup[formatted_address]=1455%20Market%20St%2C%20San%20Francisco%2C%20CA%2094103&dropoff[latitude]=37.802374&dropoff[longitude]=-122.405818&dropoff[nickname]=Coit%20Tower&dropoff[formatted_address]=1%20Telegraph%20Hill%20Blvd%2C%20San%20Francisco%2C%20CA%2094133&product_id=a1111c8c-c720-46c3-8534-2fcdd730040d&link_text=View%20team%20roster&partner_deeplink=partner%3A%2F%2Fteam%2F9383")
if UIApplication.shared.canOpenURL(url!){
if #available(iOS 10.0, *) {
UIApplication.shared.open(url!, options: [:], completionHandler: { (success) in
if success {
print("Successfully open uber")
}
})
} else {
// Fallback on earlier versions
}
} else{
print("app not found")
}

Export UIDocument with custom file package UTI

I'm trying to export my UIDocument subclass with a UIDocumentPickerViewController. The subclass writes data to a FileWrapper and its UTI conforms to com.apple.package.
But the presented document picker shows "Documents in iCloud Drive are not available because the iCloud Drive setting is disabled."
The document is successfully written to the cache, as I can see from the exported container package.
When I change the document subclass and custom UTI to conform to a single file (e.g. public.plain-text), the document picker works fine and I can export the file. So the problem seems to be with the Document Type or Exported UTI.
Am I doing something wrong or is this a bug?
Info.plist
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Custom Doc</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.zxzxlch.documentsandbox.customdoc</string>
</array>
<key>LSTypeIsPackage</key>
<true/>
</dict>
</array>
...
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
</array>
<key>UTTypeDescription</key>
<string>Custom Doc File</string>
<key>UTTypeIdentifier</key>
<string>com.zxzxlch.documentsandbox.customdoc</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>zzz</string>
</array>
</dict>
</dict>
</array>
CustomDocument.swift
private let textFilename = "contents.txt"
class CustomDocument: UIDocument {
var content = "Test"
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let topFileWrapper = contents as? FileWrapper,
let textData = topFileWrapper.fileWrappers?[textFilename]?.regularFileContents else {
return
}
content = String(data: textData, encoding: .utf8)!
}
override func contents(forType typeName: String) throws -> Any {
let textFileWrapper = FileWrapper(regularFileWithContents: content.data(using: .utf8)!)
textFileWrapper.preferredFilename = textFilename
return FileWrapper(directoryWithFileWrappers: [textFilename: textFileWrapper])
}
}
ViewController.swift
func exportDocument() {
// Write to cache
let cachesDir = FileManager.default.urls(for: FileManager.SearchPathDirectory.cachesDirectory, in: .allDomainsMask).first!
let dataDir = cachesDir.appendingPathComponent("export", isDirectory: true)
try! FileManager.default.createDirectory(at: dataDir, withIntermediateDirectories: true, attributes: nil)
let fileURL = dataDir.appendingPathComponent("cookie").appendingPathExtension("zzz")
let archive = CustomDocument(fileURL: fileURL)
archive.content = "Cookie cat"
archive.save(to: archive.fileURL, for: .forCreating) { success in
guard success else {
let alertController = UIAlertController.notice(title: "Cannot export data", message: nil)
self.present(alertController, animated: true, completion: nil)
return
}
let documentPicker = UIDocumentPickerViewController(url: archive.fileURL, in: .exportToService)
documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
}
}
This solves my problem: make the UTI also conform to public.composite-content, i.e.
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
<string>public.composite-content</string>
</array>
I'm not sure why though.

How does one force the "openURL url: NSURL!" version of AppDelegate.application() to be called in a Swift 1.2 app?

Question re-edited following some research, problem now as stated in title (specifically this is for use with the Swifter framework). I have:
a. a single-page XCode 6.3 Beta iPhone project;
b. in which I have added a URL Type to CFBundleURLTypes in Info.plist (via the Info tab in the project's settings), which looks like this:
...
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.example.SwifterTest4</string>
<key>CFBundleURLSchemes</key>
<array>
<string>swifter</string>
</array>
</dict>
</array>
...
c. AppDelegate.swift coded thus:
import SwifteriOS
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
func application ( application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject? ) -> Bool
{
println( "application() openURL: " + url.absoluteString! );
Swifter.handleOpenURL( url )
return true
}
}
d. and ViewController.swift coded thus:
import SwifteriOS
import UIKit
class ViewController: UIViewController
{
var swifter: Swifter
required init ( coder aDecoder: NSCoder )
{
let oauthToken: String = NSUserDefaults.standardUserDefaults().valueForKey("oauth_token") as! String
let oauthTokenSecret: String = NSUserDefaults.standardUserDefaults().valueForKey("oauth_secret") as! String
// these two actually contain useful values
let consumerKey = "my-consumer-key"
let consumerSecret = "my-consumer-secret"
self.swifter = Swifter( consumerKey: consumerKey, consumerSecret: consumerSecret, oauthToken: oauthToken, oauthTokenSecret: oauthTokenSecret )
super.init( coder: aDecoder )
}
override func viewDidLoad ()
{
super.viewDidLoad()
swifter.authorizeWithCallbackURL( NSURL( string: "swifter://success" )!,
success:
{
( accessToken: SwifterCredential.OAuthAccessToken?, response: NSURLResponse ) in
println( "SUCCESS" )
},
failure:
{
( error: NSError ) in
println( "FAILURE" )
})
}
}
The application( ... ) func in AppDelegate is not being called, whereas the Registering Custom URL Schemes section of this official Apple documentation page, and this SO post suggest that the addition of a CFBundleTypeRole in Info.plist is sufficient to force the call. It seems not though.
With reference to #rmaddy's comment below, I have also tried this is stable release XCode 6.2 to no avail either, although even if that did work it wouldn't be that useful, because the Swifter framework has been updated to Swift 1.2 in the past week. Without the call to application( ... ) [presumably], a fatal error: unexpectedly found nil while unwrapping an Optional value is thrown at let oauthToken: String in ViewController.init(). Alternatively, if oauthToken and oauthTokenSecret are simply omitted, FAILURE is printed to the console.
I would be grateful of any help, thank you.

Resources