Import file from iCloud


#1

First I would like to make any reader aware of my limited knowledge of iOS and Objective-C. The involved code is a combination of Apple Developer documentation examples and experimenting with the API available from tns-platform-declarations. I was lead here by https://github.com/NativeScript/NativeScript/issues/4029, which I have based this Topic on.


My goal is to able to open a document picker in order to select an image/pdf-file from the user’s iCloud Drive. I then want to retrieve this file and store it in another system.

I am currently able to use the UIDocumentPickerViewController to navigate and select the relevant files as shown below.

The following code currently allows me to display iCloud files, as mentioned. However, the problem appears post-selection as I am not able to retrieve the location of the file.

According to the Apple Developer Documentation: “The document picker calls the delegate’s documentPicker:didPickDocumentAtURL: method when the user selects a file. The URL points to a temporary file in the app’s sandbox. The file remains available until the app closes.”

Currently, the DocumentPickerDelegate’s documentPickerDidPickDocumentAtUrl fails to notify that a file has been selected.

class DocumentPickerDelegate extends NSObject implements UIDocumentPickerDelegate {
    private owner: WeakRef<DocumentPicker>;

    static initWithOwner(owner: WeakRef<DocumentPicker>): DocumentPickerDelegate {
        const delegate = <DocumentPickerDelegate>DocumentPickerDelegate.new();
        delegate.owner = owner;

        return delegate;
    }

    documentPickerDidPickDocumentAtURL(controller: UIDocumentPickerViewController, url: NSURL) {
        console.log("documentPickerDidPickDocumentAtURL");
        this.owner.get().addFileUrl(url);
    }
    documentPickerWasCancelled?(controller: UIDocumentPickerViewController) {
        console.log("documentPickerWasCancelled");
    }
}

export class DocumentPicker {
    private documentPicker: UIDocumentPickerViewController;

    fileUrl: NSURL;

    constructor() {}

    show(vc: UIViewController) {
        const documentTypes = utils.ios.collections.jsArrayToNSArray([kUTTypeImage]);
        this.documentPicker = UIDocumentPickerViewController.alloc().initWithDocumentTypesInMode(documentTypes, UIDocumentPickerMode.Import);
        this.documentPicker.delegate = DocumentPickerDelegate.initWithOwner(new WeakRef(this));

        vc.presentViewControllerAnimatedCompletion(this.documentPicker, true, null);
    }

    addFileUrl(fileUrl: NSURL) {
        console.log(fileUrl.path);
        this.fileUrl = fileUrl;
    }
}

In order to be able to reach my goal I have two questions.

1. Is my current code incomplete/incorrect, or is there something I am not getting here?
2. Optionally, is there another way to achieve my stated goal in a similar fashion?


Why there's no File Picker for NativeScript?
#2

I was able to do it by writing the delegate in javascript.


DocumentPicker:

export class DocumentPicker {
    private documentPicker: UIDocumentPickerViewController;

    fileUrl: string;
    fileExt: string;

    constructor() {}

    show(vc: UIViewController) {
        const documentTypes = utils.ios.collections.jsArrayToNSArray([kUTTypeImage, kUTTypePDF]);
        this.documentPicker = UIDocumentPickerViewController.alloc().initWithDocumentTypesInMode(documentTypes, UIDocumentPickerMode.Import);
        this.documentPicker.delegate = delegate.DocumentPickerDelegate.initWithOwner(new WeakRef(this));

        vc.presentViewControllerAnimatedCompletion(this.documentPicker, true, null);
    }

    addFileUrl(fileUrl: NSURL) {
        console.log(fileUrl.path);
        this.fileUrl = fileUrl.path;
        this.fileExt = fileUrl.pathExtension.toLowerCase();
    }
}

DocumentPickerDelegate:

var DocumentPickerDelegate = (function (_super) {
    __extends(DocumentPickerDelegate, _super);
    function DocumentPickerDelegate() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    DocumentPickerDelegate.initWithOwner = function(owner) {
        const delegate = DocumentPickerDelegate.new();
        delegate._owner = owner;
        return delegate;
    };
    DocumentPickerDelegate.prototype.documentPickerDidPickDocumentAtURL = function(controller, url) {
        console.log("documentPickerDidPickDocumentAtURL");
        this._owner.get().addFileUrl(url);
    };
    DocumentPickerDelegate.prototype.documentPickerWasCancelled = function(controller) {
        console.log("documentPickerWasCancelled");
    };
    DocumentPickerDelegate.ObjCProtocols = [UIDocumentPickerDelegate];
    return DocumentPickerDelegate;
}(NSObject));
exports.DocumentPickerDelegate = DocumentPickerDelegate;

Ios upload and import files in icloud
#3

Hello @Svettis2k,

Thank you for your example.
I have a similar situation for my app.
My scenario is this:
User gets an email with document attachment. Saves the attachment on iCloud.
Opens my app, picks the document from iCloud and use my app to upload it to my server.

I am using Nativescript with Angular, and pasting your code gives me some errors in the IDE (Visual Studio Code) and compiler.

app/job/docs/docs.component.ts(260,31): error TS2304: Cannot find name 'utils'.

app/job/docs/docs.component.ts(262,40): error TS2552: Cannot find name 'delegate'. Did you mean 'tdelete'?

app/job/docs/docs.component.ts(275,5): error TS2304: Cannot find name '__extends'.

app/job/docs/docs.component.ts(279,28): error TS2339: Property 'initWithOwner' does not exist on type '() => any'.

app/job/docs/docs.component.ts(280,49): error TS2339: Property 'new' does not exist on type '() => any'.
app/job/docs/docs.component.ts(291,28): error TS2339: Property 'ObjCProtocols' does not exist on type '() => any'.

I guess I’m missing some javascript imports here.
Can you please post some more code where I can see how you integrated this into your app?
You say that you solved the problem finding the file in temporary app file storage, but I don’t see how you used the delegate.
Are you keeping these two snippets in separate files?

Sorry for a lot of noob questions. Hope you can help me.
Thank you very much.


#4

Hi.

You can import the utils functionality for the DatePicker like this:

import * as utils from "tns-core-modules/utils/utils";

As the delegate is written in javascript it is better to put it in a .js-file and require it when needed. So to answer your question: Yes, the two snippets are kept in separate files (one .ts for the picker itself, and one .js for the delegate). The delegate is responsible for providing you with the fileUrl which you can then use to retrieve the selected file. You should be good to go as long as the delegate is provided for the document picker:

const delegate = require("./document-picker-delegate");
this.documentPicker.delegate = delegate.DocumentPickerDelegate.initWithOwner(new WeakRef(this));

How you implement the two callback functions of the delegate (documentPickerDidPickDocumentAtUrl and documentPickerWasCancelled) determines the behaviour of the picker. In this case, the picker’s fileUrl will be updated every time a new file is selected.

If you are importing an image you could use the following snippet to retrieve it as an ImageSource (or similarly for another datatype):

import { fromFile } from "image-source";
const image = fromFile(this.fileUrl);

Alternatively, you can use the file system module to create a File from the fileUrl:

import * as fs from "tns-core-modules/file-system";
const file = fs.File.fromPath(this.fileUrl);

#5

Thanks @Svettis2k for your quick reply,

However, meanwhile, with a little research I got your original example to work, using the class you first posted.
Meanwhile, I still didn’t find a way to get the url back to my Angular component, because I’m writing this as soon as I got the file picker to display.

So, for posterity, here’s my modified version of your code.

First of all, I had to edit my Info.plist:

<key>NSUbiquitousContainers</key>
		<dict>
			<key>iCloud.com.mydomain.MyApp</key>
			<dict>
				<key>NSUbiquitousContainerIsDocumentScopePublic</key>
				<true/>
				<key>NSUbiquitousContainerSupportedFolderLevels</key>
				<string>Any</string>
				<key>NSUbiquitousContainerName</key>
				<string>MyApp</string>
			</dict>
		</dict>

In the above snippet, mydomain is my actual domain and MyApp is my application’s name.
This was also registered on my Apple Developer’s account under App ID and iCloud containers, as shown on screenshots below. I’m not sure this step on Apple’s site is necessary.

First I made an Angular Service: document-picker.service.ts

import { Injectable } from '@angular/core';
import * as utils from "utils/utils";
import { ios as iosApp } from "application";

@Injectable()
export class DocumentPickerService {
    private documentPicker: UIDocumentPickerViewController;

    fileUrl: string;
    fileExt: string;

    constructor() { }

    show(vc: UIViewController) {
        const documentTypes = utils.ios.collections.jsArrayToNSArray([kUTTypeImage, kUTTypePDF]);
        this.documentPicker = UIDocumentPickerViewController.alloc().initWithDocumentTypesInMode(documentTypes, UIDocumentPickerMode.Import);
        this.documentPicker.delegate = DocumentPickerDelegate.initWithOwner(new WeakRef(this));

        vc.presentViewControllerAnimatedCompletion(this.documentPicker, true, null);
    }

    addFileUrl(fileUrl: NSURL) {
        console.log(fileUrl.path);
        this.fileUrl = fileUrl.path;
        this.fileExt = fileUrl.pathExtension.toLowerCase();
    }
}

class DocumentPickerDelegate extends NSObject implements UIDocumentPickerDelegate {
    private owner: WeakRef<DocumentPickerService>;
    public static ObjCProtocols = [UIDocumentPickerDelegate];

    static initWithOwner(owner: WeakRef<DocumentPickerService>): DocumentPickerDelegate {
        const delegate = <DocumentPickerDelegate>DocumentPickerDelegate.new();
        delegate.owner = owner;

        return delegate;
    }

    documentPickerDidPickDocumentAtURL(controller: UIDocumentPickerViewController, url: NSURL) {
        console.log("documentPickerDidPickDocumentAtURL");
        this.owner.get().addFileUrl(url);
    }
    documentPickerWasCancelled?(controller: UIDocumentPickerViewController) {
        console.log("documentPickerWasCancelled");
    }
}

I declared this service as a service provider in my Module:

...
import { DocumentPickerService } from './document-picker.service';

@NgModule({
    imports: [ ... ],
    providers: [
        DocumentPickerService
    ],
})

And then in my component, I used the service like this:

import { Component, OnInit, Input, ViewContainerRef, ViewChild, Injectable } from '@angular/core';
import { DocumentPickerService } from '../document-picker.service';
import { android as androidApp, ios as iosApp } from "application";
...

@Component({
    moduleId: module.id,
    selector: 'Docs',
    templateUrl: 'docs.component.html',
    styleUrls: ['docs.component.css']
})
export class DocsComponent implements OnInit {
    ...
    constructor(
        ...
        private dp: DocumentPickerService
    ) {}

    public pickFile() {
        
        if(androidApp)
            ...
        else if (iosApp) {            
            var window = iosApp.nativeApp.keyWindow || (iosApp.nativeApp.windows.count > 0 && iosApp.nativeApp.windows[0]);
            if (window) {
                console.log(' showing native IOS document picker!!!');
                var rootController = window.rootViewController;
                if (rootController) {
                    this.dp.show(rootController);
                }
            }
        }
    }
}

As I said I didn’t yet get to retrieve the URL of the selected document in my component, but I will keep trying. Meanwhile, if you know how tell me, please. And then I will edit this post with complete solution.

Thank you again very much for this. Only getting this to show in my app was an enormous success for me.

EDIT:
Okay, now I see I just have to ask the service for fileUrl property to get the url. However, I will need to emit an event from the service once the file is “picked”.


#6

Yes, I forgot all about the Info.plist and Apple configuration.

I’m not sure why, but my implementation only started working when I wrote the delegate in javascript. I initially wrote it in typescript similar to the one you posted above, but the callbacks never worked. I also ended up with the document picker as a service, identically to the one you have posted.

Try using the delegate in javascript and require it in your show function. That is what did the trick for me. If you are able to make it work using only typescript, please let me know.

EDIT:
Just saw your edit. Do the callbacks work for you now?


#7

I will sure let you know if it works completely.

Exactly what callbacks are you talking about?
The addFileUrl method of DocumentPickerService writes the fileUrl.path to the console, so I’m assuming the fileUrl property of DocumentPickerService is available for reading from my component.

In my usecase scenario, the next step is to know when the file was picked and then pick up the URL, and upload the file to my server. So, I guess I will have to emit an event from the addFileUrl method of the Service to know when to start the upload, and see if the file is actually there in the temp URL location.

Will let you know if the scenario plays out.


#8

I was referring to the documentPickerDidPickDocumentAtUrl and documentPickerWasCancelled callbacks that never printed anything when i started this thread.

I would look into Observables for your service. You redesign your DocumentPickerService to something like this (did not test this):

import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { Observable } from "rxjs/Observable";
import * as utils from "tns-core-modules/utils/utils";

@Injectable()
export class DocumentPickerService {
    private documentPicker: UIDocumentPickerViewController;

    cancelledPicking = new Observable<boolean>();
    private _cancelledPicking = new BehaviorSubject<boolean>(null);
    pickedFile = new Observable<string>();
    private _pickedFile  = new BehaviorSubject<string>(null);

    private dataStore: {
        cancelledPicking: boolean,
        pickedFile: string
    };

    constructor() {
        this.dataStore = {
            cancelledPicking: null,
            pickedFile: null
        };

        this.cancelledPicking = this._cancelledPicking.asObservable();
        this.pickedFile = this._pickedFile.asObservable();
    }

    show(vc: UIViewController) {
        // show the picker
    }

    setCancelled(cancelled: boolean) {
        this.dataStore.cancelledPicking = cancelled;
        this._cancelledPicking.next((<any>Object).assign({}, this.dataStore).cancelledPicking);
    }

    setFilePicked(fileUrl: NSURL) {
        this.dataStore.pickedFile = fileUrl.path;
        this._pickedFile.next((<any>Object).assign({}, this.dataStore).pickedFile);
    }
}

You could then use these two set-functions in your delegate’s callbacks (documentPickerDidPickDocumentAtUrl and documentPickerWasCancelled).

In the component that uses the service you can then subscribe to the observables like this:

this.documentPicker.cancelledPicking.subscribe((cancelled) => {
    if (cancelled) {
        // do something with the cancelled boolean
    }
});
this.documentPicker.pickedFile.subscribe((fileUrl) => {
    if (fileUrl) {
        // do something with the fileUrl string
    }
});

#9

Yes, the documentPickerDidPickDocumentAtUrl does fire, and prints string in console.
Now I will try to implement the solution as you suggested and let you know.


#10

Allright, I implemented the code as you suggested.
However, the methods setFilePicked and setFileCancelled are never called. Instead, I moved the two lines from setFilePicked to addFileUrl method. It works from there, and it’s being called every time I pick a file from Document picker.

In my component I subscribe to pickedFile observable, but I subscribed in the ngOnInit instead of constructor.
Here’s how the subscription and action looks like:

this.dp.pickedFile.subscribe((url) => {
    if (url) {
        let filename = '';                                            
        let n = url.lastIndexOf('/');
        filename = url.substring(n+1);                                        
        const rawdata = NSData.alloc().initWithContentsOfFile(url);                                       
        const base64encoded = rawdata.base64EncodedStringWithOptions(0);
        let filedata = {
            jobID: this.jobId,
            name: filename,
            base64Data: base64encoded
       };
       this.jobsService.uploadDocument(filedata).subscribe();
   }
});

I had to base64 encode the content, because that’s the requirement from my server, and pack it into this object before sending.

So this is great. If you have a blog, this would make for a good blog post about Nativescript.

Do you have any knowledge about the reverse process – saving files from app to iCloud?

Thank you for all your help. It’s been fantastic.


#11

Thanks for the lead, am also behind icould intergration on my app, i copy pasted your
document-picker.service.ts but got some error in the files, am attaching the screenshot along with pls. Any help will be appreciated.


#12

@milosstanic: Have you also implemented reverse process in order to save file on iCloud drive ?
Could you please share some code in order to achieve this… ?
Thanks in advance