How to save PDF files from Firebase Storage into App Documents for future use? - ios

I have connected my App with the Firebase Storage where my 19ea PDF files exists.
I would like to download those files and save them locally for future use.
Those PDF files will be used inside UIWebviews but they may need to be updated in time. Therefore, I have configured version control system with Firebase Database, so I will be able to push the newer versions when I update the files in the storage.
So, how I can save those files locally? (to a folder like: user/myapp/Documents/PDF etc?)
Also, how I can check if that folder contains any documents and how to delete them before downloading new files?
Here is what I have got so far.
I appreciate all the help.
// Firebase Storage Connection
static var refStorage:FIRStorageReference?
static var dataPDF = [NSData]()
func newDataDownload(){
// Compare Current Data Version with Online Data Version
if myFirebaseData.localDataVersion < myFirebaseData.onlineDataVersion {
// Set Firebase Storage Reference
myFirebaseData.refStorage = FIRStorage.storage().reference()
for i in 1...myFirebaseData.onlineTotalPDFCount {
// Create a reference to the file you want to download
let pulledPDF = FIRStorage.storage().reference().child("/PDF/\(i).pdf")
// Create local filesystem URL
let localURL = URL(string: "myApp/Documents/PDF/\(i)")!
pulledPDF.data(withMaxSize: myFirebaseData.maxPDFdownloadSize, completion: { (downPDF, err) in
if err == nil {
// Accessed the data
myFirebaseData.dataPDF.append(downPDF! as NSData)
print(myFirebaseData.dataPDF)
} else {
// If there is an error print it
print(err.debugDescription)
}
})
}
}
// If Data is successfully downloaded update Local Data Version
myFirebaseData.localDataVersion = myFirebaseData.onlineDataVersion

Use storageRef.write(toFile: completion:) (docs), like:
// Create a reference to the file you want to download
let pdfRef = storageRef.child("files/file.pdf")
// Create local filesystem URL
let localURL = URL(string: "path/to/local/file.pdf")!
// Download to the local filesystem
let downloadTask = pdfRef.write(toFile: localURL) { url, error in
if let error = error {
// Uh-oh, an error occurred!
} else {
// Local file URL for "path/to/local/file.pdf" is returned
}
}
Note that you can only write to /tmp and /Documents due to app sandboxing requirements (see Downloading Firebase Storage Files Device Issue for an example of how this fails otherwise).

Related

iOS Swift: Firebase Storage upload error - only the initial file upload works

Environment:
Xcode 12.0.1 (Swift 5.x)
iOS 13
Firebase 6.34.0
FirebaseFirestore 1.19.0
FirebaseStorage 3.9.1
GoogleDataTransport 7.5.1
PromisesObjC 1.2.11
Problem:
I initialize Firebase in iOS app and successfully write database data to Cloud FireStore.
I then upload related video file to Firebase Storage with an asynch call.
Cloud Firestore database writes ALWAYS work.
Using the index generated from Cloud Firestore write, Firebase storage is then used to upload a video and a data file with names as the index from (1) above.
The first first file upload always works with a new app launch.
The second or any additional file upload fails with the following error:
cloud storage VIDEO file upload error: Error Domain=FIRStorageErrorDomain Code=-13000 "An unknown error occurred, please check the server response." UserInfo={object=PBY7Ost7nPWD8jWWF4qG.mov, ResponseBody=Can not finalize upload. Current size is 1692167. Expected final size is 1665242., bucket=launch-me-47860.appspot.com, data={length = 83, bytes = 0x43616e20 6e6f7420 66696e61 6c697a65 ... 31363635 3234322e }, data_content_type=text/plain; charset=utf-8, NSLocalizedDescription=An unknown error occurred, please check the server response., ResponseErrorDomain=com.google.HTTPStatus, ResponseErrorCode=400}
I have found similar questions posted from 2016 and 2017 but this Firebase write problem seems different as the initial upload always works and then the next time I try to perform an upload it fails with the error.
I added a routine to try additional upload attempts if the first upload fails. They all fail.
Here is the upload code:
// MARK: Write file to Firebase Cloud Storage
private func fbCloudFileWrite(indexName: String) {
let fbStorage = Storage.storage()
print("(DEBUG FB) fbCloudFileWrite: upload indexName.csv and indexName.mov")
// now upload file to cloud FireStore
let fbStorageRef = fbStorage.reference()
// Create a reference to the file you want to upload
//let LaunchMeDataRef = fbStorageRef.child("LaunchMe/" + indexName + ".csv")
//let LaunchMeVideoRef = fbStorageRef.child("LaunchMe/" + indexName + ".mov")
var LaunchMeDataRef = fbStorageRef.child(indexName + ".csv")
var LaunchMeVideoRef = fbStorageRef.child(indexName + ".mov")
// Upload the file to the path "images/rivers.jpg"
fbWriteAttempts += 1
DispatchQueue.main.async {
print("(DEBUG FB) ***** write attemp #: \(self.fbWriteAttempts)")
if let vURL = self.videoURL, let dURL = self.dataFileURL {
let uploadVideoTask = LaunchMeVideoRef.putFile(from: vURL, metadata: nil) { metadata, err in
if let err = err {
print("(DEBUG FB) cloud storage VIDEO file upload error: \(err)")
if self.fbWriteAttempts < 4 {
self.fbCloudFileWrite(indexName: indexName)
}
} else {
print("(DEBUG FB) video uploaded: \(indexName)")
let uploadDataTask = LaunchMeDataRef.putFile(from: dURL, metadata: nil) { metadata, err in
if let err = err {
print("(DEBUG FB) cloud storage SENSOR file upload error: \(err)")
} else {
print("(DEBUG FB) sensor data uploaded: \(indexName)")
print("(DEBUG FB) set newRecording = false to prevent duplicates")
// all files successfully uploaded. Set newRecording to false
self.newRecording = false
}
} // close uploadDataTask
} // close else
} // close let uploadVideoTask
} // close vURL unwrap
else {
print("(DEBUG FB) videoURL could not be unwrapped")
}
} // close Dispatch.main.async
Strangely enough, I was able to solve this problem by setting the URL variable of the movie local directory path within the VideoPlayerViewController instead of passing the URL in from the previous controller during the segue. This makes no sense to me that this would be necessary except perhaps some caching that happens behind the scenes.
I know the correct URL is passed in as the correct video plays on the screen when it is passed in, yet the Firebase Storage upload tries to upload the previous video (hence the expected size error) when provided the same URL that played the correct video.
New function added within the controller that sets the URL instead of passing it in (prior to performing the Firebase Storage upload):
private func setVideoStorageURL() {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let path = "output.mov"
videoURL = paths[0].appendingPathComponent(path)
}
whereas the previous implementation that was not working passed the URL into the controller:
if let destinationVC = segue.destination as? VideoPlayerViewController {
print("(DEBUG) Setting data to be passed to VideoPlayerViewController")
destinationVC.newRecording = newRecording
//now set newRecording to false if it is true
if newRecording {
newRecording = false
}
destinationVC.audioMode = audioMode
destinationVC.selectedDevice = selectedDevice
destinationVC.videoURL = passFileURL

Realm iOS: How to handle Client Reset

Basically, I want to handle a case where any device got SyncError with type ClientResetError then, want my device to re-login to realm again. but as per documentation, we have to closeRealmSafely before I login to realm again, but I am not sure how to close realm safely.
I am going through the doc (https://docs.realm.io/sync/using-synced-realms/errors#client-reset) to handle client reset error and found it's very confusing . I want help to understand about the following code.
First there is no method available to closeRealmsafely. Please help me understand how can I close the realm safely?
How can I backup and when I will use it? Should I skip the reset error because in documentation it's mentions if the client reset process is not manually initiated, it will instead automatically take place after the next time the app is launched, upon first accessing the SyncManager singleton. It is the app’s responsibility to persist the location of the backup copy if needed, so that the backup copy can be found later."
Below is the error handler sample code from the doc.
let syncError = error as! SyncError
switch syncError.code {
case .clientResetError:
if let (path, clientResetToken) = syncError.clientResetInfo() {
closeRealmSafely()
saveBackupRealmPath(path)
SyncSession.immediatelyHandleError(clientResetToken)
}
default:
// Handle other errors...
()
}
}```
Finally we figured out how to handle the client reset error. We have taken following steps To avoid the data loss incase user is offline and came online and got reset error.
Save the local realm to another directory
Invalidate and nil the realm
Initiate realm manual reset - Call SyncSession.immediatelyHandleError with clientResetToken passed and it will delete the existing realm from directory
Show client reset alert - This will intimate user to relaunch the app.
On next launch realm creates a fresh realm from ROS.
After new realm connects, restore the realm records (if any) from the old realm saved in backup directory above.
Delete the backup realm(old realm) from directory.
switch syncError.code {
case .clientResetError:
if let (path, clientResetToken) = syncError.clientResetInfo() {
// taking backup
backUpRealm(realm: yourLocalRealm)
// making realm nil and invalidating
yourLocalRealm?.invalidate()
yourLocalRealm = nil
//Initiate realm manual reset - Call `SyncSession.immediatelyHandleError` with `clientResetToken` passed and it will delete the existing realm from directory
SyncSession.immediatelyHandleError(clientResetToken)
// can show alert to user to relaunch the app
showAlertforAppRelaunch()
}
default:
// Handle other errors...
()
}
}```
The back up realm code look like this:
func backUpRealm(realm: Realm?) {
do {
try realm?.writeCopy(toFile: backupUrl)
} catch {
print("Error backing up data")
}
}
After doing this backup will be available at backup path. On next launch device will connect and download a fresh realm from ROS so after device connects restore the realm records from the backup realm saved in the backup path.
The restore merge backup code will look like this. place the below method when realm connects after relauch.The ```restoredRealm`` is fresh downloaded realm on launch
func restoreAndMergeFromBackup(restoredRealm: Realm?) {
let realmBackUpFilePath = isRealmBackupExits()
// check if backup exists or not
if realmBackUpFilePath.exists {
let config = Realm.Configuration(
fileURL: URL(fileURLWithPath: realmBackUpFilePath.path),
readOnly: true)
let realm = try? Realm(configuration: config)
guard let backupRealm = realm else { return }
//Get your realm Objects
let objects = backupRealm.objects(YourRealmObject.self)
try? restoredRealm?.safeWrite {
for object in objects {
// taking local changes to the downloaded realm if it has
restoredRealm?.create(YourRealmObject.self, value: object, update: .modified)
}
self.removeRealmFiles(path: realmBackUpFilePath.path)
}
} else {
debug("backup realm does not exists")
}
}
private func isRealmBackupExits() -> (exists: Bool, path: String) {
let documentsPath = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let realmPathComponent = documentsPath.appendingPathComponent("your_backup.realm")
let filePath = realmPathComponent.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
return (true, filePath)
}
return (false, "")
}
private func removeRealmFiles(path: String) {
let realmURL = URL(fileURLWithPath: path)
let realmURLs = [
realmURL,
realmURL.appendingPathExtension("lock"),
realmURL.appendingPathExtension("realm"),
realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
do {
try FileManager.default.removeItem(at: URL)
} catch {
debug("error while deleting realm urls")
}
}
}```
In our testing we have found that there is a backup made by realm automatically so we deleted it for safety purpose. the path argument you will get in the if let (path, clientResetToken) = syncError.clientResetInfo()
func removeAutoGeneratedRealmBackUp(path: String) {
do {
try FileManager.default.removeItem(at: URL(fileURLWithPath: path))
} catch {
debug("error while deleting realm backUp path \(path)")
}
}

How to create a pre bundled realm file and upload data to it?

I am new to Realm and I want to ship a pre bundled Realm file my app, however the realm documentation is unclear to me on how to actually create a .realm file and also upload the desired pre bundled data to it. I have not been able to find any solution via SO, Google, or Youtube. Step-by-step instructions would be very helpful.
We're still looking at ways to officially make generating pre-populated Realms more easy. It's definitely a desired feature for the Realm Browser, but due to the way that Realm files require a migration when changing the schema, it's somewhat tricky to incorporate into a UI. It's definitely something we want to improve down the line.
At the moment, the Browser has a converter in it that can perform minimal data import like CSV files.
The Realm documentation is saying that the easiest way for you to produce a pre-populated Realm specific for your apps needs is to build a separate macOS app whose sole role is to generate the Realm file, pre-populate the data and then expose the resulting Realm file so you can copy it to your app.
The operation to do this is just like any normal Realm interaction (try! Realm() is what causes the file to actually be initially created), except you manually interact with the Realm file on disk upon completion.
I'm working on an app for an iOS conference, and the schedule data for the event is going to be stored in a Realm that is pre-bundled with the app when it ships.
Since I thought creating an extra macOS app would be overkill for an iOS app, I instead used iOS unit tests that would generate the pre-bundled Realm from scratch every time it was executed.
class Tests: XCTestCase {
func testGenerateNewDefaultRealm() {
let sources = [conferences, sponsors, conferenceDays] as [Any]
XCTAssert(generateDefaultRealm(named: "MyConferenceRealm.realm", sources: sources))
}
}
extension Tests {
public func generateDefaultRealm(named name: String, sources: [Any]) -> Bool {
// Create and configure the Realm file we'll be writing to
let realm = generateRealm(named: name)
// Open a Realm write transaction
realm.beginWrite()
// Loop through each source and add it to Realm
for source in sources {
if let objectArray = source as? [Object] {
for object in objectArray {
realm.add(object)
}
}
else if let objectDictionary = source as? [String : Object] {
for (_, object) in objectDictionary {
realm.add(object)
}
}
}
// Commit the write transaction
do {
try realm.commitWrite()
}
catch let error {
print(error.localizedDescription)
return false
}
// Print the file location of the generated Realm
print("=====================================================================")
print(" ")
print("Successfully generated at")
print(realm.configuration.fileURL!.path)
print(" ")
print("=====================================================================")
return true
}
public func generateRealm(named name: String) -> Realm {
let exportPath = NSTemporaryDirectory()
let realmPath = exportPath.appending(name)
// Delete previous Realm file
if FileManager.default.fileExists(atPath: realmPath) {
try! FileManager.default.removeItem(atPath: realmPath)
}
// Create new Realm file at path
let objectTypes: [Object.Type] = [Conference.self, ConferenceDay.self, SessionBlock.self, Event.self, Presentation.self,
Session.self, Location.self, Speaker.self, Sponsor.self, Venue.self]
let configuration = Realm.Configuration(fileURL: URL(string: realmPath), objectTypes: objectTypes)
let realm = try! Realm(configuration: configuration)
return realm
}
}
Running this unit test will generate a new Realm file in the NSTemporaryDirectory() directory of the Mac, and then feed in a set of Array and Dictionary objects of un-persisted Realm Object instances that are created each time the test is run. The location of the final Realm is then printed in the console so I can grab it and move it to the app bundle.

Backup Realm to iCloud Drive

I would like to backup a realm database file to an iCloud drive, like WhatsApp, I have some questions:
What is the best practice to do this?
I have a database located in a shared group folder to access it from extensions, how can I back it up? How can I show the progress bar of upload? Like WhatsApp for example?
If I put a realm file in a document folder it will be synced for each modify.
Are there some samples code that we can see?
Thanks for the help, have any ideas? links?
Just to clarify, this is a question about backing up a discrete Realm file itself to iCloud Drive, so that it would be visible in the iCloud Drive app. Not synchronizing the contents of the file to a CloudKit store.
If you leave the Realm file in the Documents directory, then if the user performs an iCloud or iTunes backup, the file will be backed up. All this means though is that if the user decides to upgrade to a new device and perform a restore using the old device's backup image, the Realm file will be restored then. If the user deletes the app from your old device before then, the iCloud backup will also be deleted.
If you want to export your Realm file so it can be permanently saved and accessed in iCloud Drive, you can export a copy of the Realm file to your app's iCloud ubiquity container. This is basically just another folder like the shared group's folder, but it's managed by iCloud. This folder sort of behaves like Dropbox in that anything you put in there is automatically synchronized.
The code would look something like this:
let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
let realmArchiveURL = containerURL.appendPathComponent("MyArchivedRealm.realm")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL)
This is a really basic example. The Apple documentation recommends you do this on a background thread since setting up the iCloud folder for the first time can create some time.
Updating this wouldn't happen automatically. You'll need to export a new copy of the Realm each time the user wants to perform a backup.
I have recently had the same requirements and I am able to achieve from below steps
Swift: 4+
Step:1
1.Setup Your cloudKit for your app with a Developer account
2. You can take reference: https://www.raywenderlich.com/1000-cloudkit-tutorial-getting-started
Step 2
- Add CloudKit Capabilities in your App
- Please check out the screenshot: https://prnt.sc/pdpda5
Step 3
- Check for cloud Enabled options for your iphone
// Return true if iCloud is enabled
func isCloudEnabled() -> Bool {
if DocumentsDirectory.iCloudDocumentsURL != nil { return true }
else { return false }
}
Step 4
- Setup the below variables for Local or iCloud Document directories
struct DocumentsDirectory {
static let localDocumentsURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: .userDomainMask).last!
static let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
}
Step:5
Below function is used for copyRealmFileToIcloudContainer
func uploadDatabaseToCloudDrive()
{
if(isCloudEnabled() == false)
{
self.iCloudSetupNotAvailable()
return
}
let fileManager = FileManager.default
self.checkForExistingDir()
let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents", isDirectory: true)
let iCloudDocumentToCheckURL = iCloudDocumentsURL?.appendingPathComponent("\(memberId)_default.realm", isDirectory: false)
let realmArchiveURL = iCloudDocumentToCheckURL//containerURL?.appendingPathComponent("MyArchivedRealm.realm")
if(fileManager.fileExists(atPath: realmArchiveURL?.path ?? ""))
{
do
{
try fileManager.removeItem(at: realmArchiveURL!)
print("REPLACE")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL!)
}catch
{
print("ERR")
}
}
else
{
print("Need to store ")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL!)
}
}
Step:6
- Once your realm file uploaded on the server , you can check this in your iPhone
- Steps
- 1.Go To Setting
- 2.Go To iCloud
- 3.Go To ManageStorage
- 4.You will see your application there
- 5.Tap on Application, you will able to see your realm file over there
Step:7
- Make Sure you have added the below lines in info.plist
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.com.example.app</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerName</key>
<string>iCloudDemoApp</string>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
</dict>
</dict>
#yonlau as per your request sharing answer for backup realm file , This is tested once and the realm data only have when they backup on iCloud.
func DownloadDatabaseFromICloud()
{
let fileManager = FileManager.default
// Browse your icloud container to find the file you want
if let icloudFolderURL = DocumentsDirectory.iCloudDocumentsURL,
let urls = try? fileManager.contentsOfDirectory(at: icloudFolderURL, includingPropertiesForKeys: nil, options: []) {
// Here select the file url you are interested in (for the exemple we take the first)
if let myURL = urls.first {
// We have our url
var lastPathComponent = myURL.lastPathComponent
if lastPathComponent.contains(".icloud") {
// Delete the "." which is at the beginning of the file name
lastPathComponent.removeFirst()
let folderPath = myURL.deletingLastPathComponent().path
let downloadedFilePath = folderPath + "/" + lastPathComponent.replacingOccurrences(of: ".icloud", with: "")
var isDownloaded = false
while !isDownloaded {
if fileManager.fileExists(atPath: downloadedFilePath) {
isDownloaded = true
print("REALM FILE SUCCESSFULLY DOWNLOADED")
self.copyFileToLocal()
}
else
{
// This simple code launch the download
do {
try fileManager.startDownloadingUbiquitousItem(at: myURL )
} catch {
print("Unexpected error: \(error).")
}
}
}
// Do what you want with your downloaded file at path contains in variable "downloadedFilePath"
}
}
}
}
2.Copy realm file from iCloud to Document directory
func copyFileToLocal() {
if isCloudEnabled() {
deleteFilesInDirectory(url: DocumentsDirectory.localDocumentsURL)
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(atPath: DocumentsDirectory.iCloudDocumentsURL!.path)
while let file = enumerator?.nextObject() as? String {
do {
try fileManager.copyItem(at: DocumentsDirectory.iCloudDocumentsURL!.appendingPathComponent(file), to: DocumentsDirectory.localDocumentsURL.appendingPathComponent(file))
print("Moved to local dir")
//HERE ACCESSING DATA AVAILABLE IN REALM GET FROM ICLOUD
let realm = RealmManager()
let array = realm.FetchObjects(type: Mood.self)
print(array?.count)
} catch let error as NSError {
print("Failed to move file to local dir : \(error)")
}
}
}
}
You could take a look at this Github project by mikemac8888.
Basically you make your model objects conform to RealmCloudObject:
class Note: Object, RealmCloudObject {
...
}
You have to implement a mapping function :
func toRecord() -> CKRecord {
...
record["text"] = self.text
record["dateModified"] = self.dateModified
}
... and the reverse function used to create Realm records out of CloudKit records:
public func changeLocalRecord(...) throws {
...
realm.create(objectClass as! Object.Type,
value: ["id": id,
"text": text,
"dateModified": NSDate(),
"ckSystemFields": recordToLocalData(record)],
update: true)
...
}
The full documentation could be read at the link I provided, obviously.

How do I store private app data on Google Drive with my iOS app?

I have an iOS app that has a local database. I'd like to back that up for users who choose to sign in with Google. The web (https://developers.google.com/drive/web/appdata) and android (https://developers.google.com/drive/android/appfolder) have guides on how to do this, but I can't find a similar one for iOS. Does it exist?
If you already have code to upload a file to the user's Drive account, it is very easy to switch to uploading into the private app folder instead. When making the Files.insert call, the file will be added to all of the folders listed in the parents[] array. (If this array is empty, by default the file is added to the root folder.) To upload the file into the private app data folder, simply set the parents[] array to appfolder. You have to do this at the same time as uploading the file, because once it has been uploaded the file can't be moved between the user's drive and your app's private data folder.
(Note: you may need to use the regular REST API to do this, because Google's Drive API for iOS docs do not show any methods for actually uploading a new file to Drive.)
Check this how this is working for me in swift 4.2 and above:
let googleDrive: GTLRDrive_File = GTLRDrive_File()
googleDrive.name = "name.json"
googleDrive.parents = ["appDataFolder"]
let uploadParam: GTLRUploadParameters = GTLRUploadParameters(data: data, mimeType: "application/json")
uploadParameters.shouldUploadWithSingleRequest = true;
let queryDrive: GTLRDriveQuery_FilesCreate = GTLRDriveQuery_FilesCreate.query(withObject: metadata, uploadParameters: uploadParam)
queryDrive.fields = "id"
self.service.executeQuery(queryDrive) { (result, response, error) in
if let file = response as? GTLRDrive_File {
if (error == nil) {
print(file.identifier)
/// your code here
} else {
// handle error part
}
}
else {
//handle exception part
}
}
"data" the json data you get this like below
let param = [["key": "value"], ["key": "value"], ["key": "value"]]
let data = try JSONSerialization.data(withJSONObject: param, options: .prettyPrinted)

Resources