Firebase Offline Support: upload posts while user is offline and sync when user comes online in iOS Swift app - swift

I am using firebase in an iOS-Swift project in which I have to enable offline support for uploading posts, in the post there is a picture and caption just like Instagram, so what I want is when user is offline and he/she wants to upload a post, his/her picture get saved in cache and when user comes online that photo get uploaded and give back a download url that we can use for saving posts-details it in database.
sample code is:
let photoIDString = UUID().uuidString
let storageRef = Storage.storage().reference(forURL: "storage ref URL").child("posts").child(photoIDString)
storageRef.putData(imageData, metadata: nil, completion: { (metadata, error) in
guard let metadata = metadata else {
return
}
if error != nil {
return
}
storageRef.downloadURL(completion: { ( url, error ) in
guard let downloadURL = url else {
return
}
let photoUrl = downloadURL.absoluteString
self.sendDataToDatabase(photoUrl: photoUrl)
})
}
)
I want to know what changes should I make in my code to provide the offline capability. Any code snippet will help more.

The problem is better view as re-send to server when there is an error.
For your offline case, you can check if the error return is a network error, or manually check network connection availability.
You can create a re-send array of object
e.g
var resendList : [YourObjectType]
// when failed to send to server
resendList.append(yourFailedObject)
And then, 2 solutions:
Check the network connectivity manually and reupload in when the app become active in func applicationDidBecomeActive(_ application: UIApplication) in appDelegate. For checking connectivity you can try the method here: Check for internet connection with Swift But this has a problem that, the user has to go out the app and back again with network connected
Keep track(listen to notification) on the connectivity change, using a suggestion method by https://stackoverflow.com/a/27310748/4919289 and reupload it to server
and loop through all objects in resendList and re-upload again.

I am not an iOS developer, but I can share logical flow and some references.
When user clicks on upload: Check if network is available?
if yes: upload the post.
if no:
save the post to app storage or offline database
set broadcast receiver to receive broadcast when device comes online. This link may be helpful.
upload post when device comes online.
If you are looking for solution that is offered by Firebase, you may find more details here.

Firebase offers you plenty of ways to do this in their documentation. https://firebase.google.com/docs/database/ios/offline-capabilities
When uploading to the firebase server, it will queue itself and wait until it has a internet connection again to upload. If this happens to timeout or you want to do it your own way just attempt to upload with a completionHandler on the setValue or updateChild functions - if not successfully and the error message is because of internet, add it to a local cache to the phone with the data and the path to the firebase server.
onLoad, attempt the same upload again until it succeeds, once it succeeds - clear the local cache.

Related

Can you use App Store Sandbox testing (verifyReceipt specifically) before submitting the app or the related IAPs?

I've been trying to use the verifyReceipt endpoint to verify in-app purchase transactions with no success.
Here's how I'm doing it.
1. Read the receipt data in iOS (this is copy paste from Apple's documentation):
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
// sendReceiptToBackEnd(receiptString)
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
2. In the backend, I followed this advice from Apple:
As a best practice, always call the production URL for verifyReceipt first, and proceed to verify with the sandbox URL if you receive a 21007 status code.
So first I try this (python code):
data = {
'receipt-data': receipt_string, # this is what we get from the frontend
'password': settings.APP_STORE_SHARED_SECRET
}
url = 'https://buy.itunes.apple.com/verifyReceipt'
response = requests.post(url, json=data)
response_data = response.json()
status = response_data['status']
Status here is 21007, which is expected according to the advice above. So then I try the sandbox:
url = 'https://sandbox.itunes.apple.com/verifyReceipt'
response = requests.post(url, json=data)
response_data = response.json()
status = response_data['status']
And I get 21002 - which means there's probably something wrong with what I sent.
Initially I was trying to test this using Xcode locally, but I found out later that verifyReceipt doesn't work with this feature because app's are not signed by the App Store. So I deleted the local configs and set the StoreKit Configuration in scheme back to none, and verified the app is using the App Store Connect as it was picking up any changes I made there in the price.
I'm out of ideas on how to debug this further, so I'm wondering could it be because the App & IAPs are not submitted yet? Currently the app is in the "Prepare for Submission" state, and the IAPs are in the "Ready to Submit" state. Or maybe there is something else I'm missing?
Edit: The answer to my question is "yes, you can." I copied and pasted the json and sent it manually using Postman and it worked fine so something is happening in the backend. I'm leaving the question for reference but if someone recommends deleting please let me know.
I saw your edit, but just as a follow-up code 21002 is a "malformed body" response (or theoretically an Apple server issue, but I've only seen it when mangling the receipt data).
You can see the full list of codes and meanings here:
https://developer.apple.com/documentation/appstorereceipts/status
Incidentally, as you build our your server to process iTunes responses you may find this guide useful for processing the receipt on the server:
https://www.namiml.com/blog/app-store-verify-receipt-definitive-guide

Update Firebase database object with multiple uploads

I have a post object that has two parts (1)photo and (2)video. In the posting flow the user selects and image, then hits next to the next screen where they select a video. Both the photo and the video are uploaded to Firebase as part of a post object. See the simple design.
wireframe design
I'm getting stuck creating the post object with both the photo url and the video url. I've tried:
1- After selecting the photo and moving to the next step, I upload the photo to Firebase storage and create the post object in the database with the photo url and an empty string for the video url.
2-Then after selecting the video, I upload the video to Firebase storage, and try to update the existing post object with the video url...
However I have not been able to update the correct post object, and don't know how I can retrieve the postByAutoID in order to update the correct post.
Here is what the database object looks like:
I tried to pass the photoURL forward to the second part of the post, but the upload and generation of the url is not complete by the time the variable gets passed.
Any help is appreciated!
With web, or JavaScript, uploads to firebase cloud storage return promises, therefore Promise.all an array of individual uploads.
As for swift, or IOS, you will have to push each upload (.putData) to memory then iterate/loop over them.
See Upload Files on iOS which provides a starting point and add your loop/iteration.
// Data in memory
let data = Data()
// Create a reference to the file you want to upload
let riversRef = storageRef.child("images/rivers.jpg")
// Upload the file to the path "images/rivers.jpg"
let uploadTask = riversRef.putData(data, metadata: nil) { (metadata, error) in
guard let metadata = metadata else {
// Uh-oh, an error occurred!
return
}
// Metadata contains file metadata such as size, content-type.
let size = metadata.size
// You can also access to download URL after upload.
riversRef.downloadURL { (url, error) in
guard let downloadURL = url else {
// Uh-oh, an error occurred!
return
}
}
}

Firebase Connectivity Test Shows Disconnected At Start Even When Connected. How Do I Change This?

I am trying to make sure Firebase has a connection before continuing to load the app. I'm using the code from Firebase's own code sample. I have placed it in the ViewDidLoad function on my home view controller:
let connectedRef = Database.database().reference(withPath: ".info/connected")
connectedRef.observe(.value, with: { snapshot in
if let connected = snapshot.value as? Bool, connected {
print("Connected")
} else {
print("Not connected")
// show alert here
}
})
The problem is that the above code always shows "Not Connected" before then showing "Connected". This is a problem because when the app is not connected, it's supposed to show an alert, and the alert will then fire every time the user opens the app.
Is this expected behavior? If so, is there a way around it?
How do I check for Firebase connectivity without Firebase returning that it's not connected first every time?
The behavior you're observing is expected. The Firebase SDK can't establish its connection immediately upon startup. There is always going to be some latency between launch and whenever a connection is first available.
Also, I don't think this strategy is a good idea, because mobile connections can be intermittent and flakey. Firebase can not possibly ensure that your app will retain a good connection even after it's first established. Your app will be easier to use if you assume that it doesn't have a connection all the time. Users have come to expect some level of offline usage, and Realtime Database supports that to some degree with offline data persistence.

CloudKit CKShare URL Goes Nowhere

I have successfully saved a CKShare URL to CloudKit, and I can see that the user is INVITED in the CloudKit Dashboard. My Mac app emailed the URL to that person, but when they click it, all they see it this screen on icloud.com:
Clicking OK makes everything disappear so all you see is the background on the web page.
My understanding is that the URL is supposed to open my Mac app where it will fire userDidAcceptCloudKitShareWith in my app delegate. But it does nothing.
Could this be because my app is in development and not in the Mac App Store yet? Do I need a custom URL scheme to get it to open my app?
Documentation on this stuff is pretty sparse. I'd love any help someone can provide.
I have since learned that you must specify a fallback URL for your CloudKit container. In cases where the app isn't installed (or isn't recognized, which seems to be the case when doing dev builds in Xcode like I am), CloudKit will forward share URL to somewhere you specify. They append the unique share ID to the URL so that you can process it on your own web page.
In the CloudKit dashboard, go to Environment Settings... and you'll see this popup:
I have it redirect to https://myapp.com/share/?id= and on my web page where it redirects to, I do a $_GET['id'] to grab the id. I then do another redirect to my application using a custom URL scheme and pass the share ID (e.g. myapp://abc123 where abc123 is the share ID).
In my app delegate, I receive the URL like this:
func application(_ application: NSApplication, open urls: [URL]) {
if let url = urls.first, let shareId = url.host{
fetchShare(shareId) //<-- sharedId = abc123
}
}
I then use CKFetchShareMetadataOperation to look up the URL of the share and CKAcceptSharesOperation to accept it like this:
func fetchShare(shareId: String){
if let url = URL(string: "https://www.icloud.com/share/\(shareId)"){
let operation = CKFetchShareMetadataOperation(shareURLs: [url])
operation.perShareMetadataBlock = { url, metadata, error in
if let metadata = metadata{
//:::
acceptShare(metadata: metadata)
}
}
operation.fetchShareMetadataCompletionBlock = { error in
if let error = error{
print("fetch Share error: \(error)")
}
}
CKContainer.default().add(operation)
}
}
func acceptShare(metadata: CKShareMetadata){
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
operation.acceptSharesCompletionBlock = { error in
if let error = error{
print("accept share error: \(error)")
}else{
//Share accepted!
}
}
CKContainer.default().add(operation)
}
I think there are easier ways to work through this using NSItemProvider and NSSharingService, but I'm doing a lot of custom UI and wanted to have full control of the share workflow.
I hope this helps someone. :)

Swift: Uploading image to Firebase cloud storage when user closes app

How do I upload an image to Firebase Storage when the user terminates the app?
Here's my code:
firebaseRef.child("\(uid)/\(filePath)").put(data, metadata: metadata) { (metadata, error) in
// NOTE: This spot never reaches when user closes the app
if error != nil || metadata == nil {
log.error("Fail to store image in cloud storage")
} else {
// Do something with the metadata url
}
As you can see, the async completion block never completes if the user terminates the app and I'm not able to upload the user's image to Firebase Storage
How do I safeguard against user terminating the app after uploading the picture? I have a preliminary idea. Could I store the image in CoreData/NSUserDefaults temporarily, and retry the upload when the user opens the app again?
Good question. At present, on iOS, there's no way of persisting and restarting the upload or download if the app is backgrounded or killed. This feature exists on Android (since the Activity is reset on screen rotation, making this a far more common issue), and we're planning on making it available on iOS in the future.
Eventually, we want to simply make this a flag [FIRStorage enablePersistentUploads:YES] or similar, to automatically do this for you, so you don't have to think about it at all.