Issues with writing to Firebase database with updateChildValues with completion block - swift

I am trying to write some entries to a Firebase Realtime database updateChildValues (...). Things are working just fine unless I add completion block to the command.
Anyone having faced similar issues?
This code works just fine:
private func createCalendarEntry(userId: String){
var dbCalendarEntry = [String: Any]()
let ref = Database.database().reference().child("calendar").child(userId)
dbCalendarEntry["name"] = event.name
ref.updateChildValues(dbCalendarEntry)
}
Adding the completion block, nothing happens:
private func createCalendarEntry(userId: String), completion: ((Error?,String?) -> ())?) {
ref.updateChildValues(dbCalendarEntry, withCompletionBlock: { (error, reference) in
if error != nil {
print("Error updating data:")
completion?(error, ref.key)
}
else
{
print("No Error")
completion?(nil,nil)
}
})
}

Related

Why am I still able to fetch data, even with deleting FireStore object in Swift?

I deleted an entry in the Firestore and also checked it manually to confirm that. However, as long as I do not close the application, I can send a request to fetch the data and I still get the result. This should not be the case.
If you imagine having a shared photo with some textual information and you delete those information, this would mean, other users can still see the textual information (fetched from the Firestore) but not the image anymore (store in Firestorage).
I want to display a message on the UI, something like "The content does not exist anymore".
How I can achieve that? I used the following approach so far but it does not work at the moment:
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
}
if (querySnapshot?.documentID == "" || querySnapshot!.metadata.isFromCache) {
completionHandler(false)
}
else {
completionHandler(true)
}
})
}
Any solutions?
Non-existent documents will still return document snapshots, but they will be empty. Therefore, you must check the contents of the snapshot for the document, not the snapshot itself. Also, you should handle errors and the overall flow of the return better.
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if let doc = querySnapshot,
doc.exists {
completionHandler(true) // only one possible true condition
} else {
if let error = error {
print(error.localizedDescription)
}
completionHandler(false) // all else false
}
})
}
As a side note, I recommend reordering the parameters of the function to make it easier to read when called (conventionally, the completion handler comes last) and giving the boolean argument a name so it's easier to read when referencing (sometime later or by other developers).
public func verifyChallengeObject(ID: String, _ completion: #escaping (_ exists: Bool) -> Void) {
...
}
verifyChallengeObject(ID: "abc123", { (exists) in
if exists {
...
} else {
...
}
})

SwiftUI and Firebase - Stream error: 'Not found: No document to update:

So, I have a program that, when it opens, looks for a specific document name in a specific collection (both specified) and, when it is found, copies the document name and starts a listener. If it doesn't find the document name after 5 x 5 second intervals, the app stops. For some reason, when I run the code, after it does the first check I get about a thousand writes of this error:
[Firebase/Firestore][I-FST000001] WriteStream (7ffcbec0eac8) Stream error: 'Not found: No document to update:
Here's the code I'm using to call firestore:
let capturedCode: String? = "party"
.onAppear(perform: {
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
print("running code check sequence")
if let code = capturedCode {
calcCloud.checkSessionCode(code)
if env.doesCodeExist {
print("code found! applying to environment!")
env.currentSessionCode = code
calcCloud.watchCloudDataAndUpdate()
allClear(env: env)
timer.invalidate()
}
else if timerCycles < 5 {
timerCycles += 1
print("code not found, this is cycle \(timerCycles) of 5")
} else {
print("could not find document on firebase, now committing suicide")
let x = ""
let _ = Int(x)!
}
}
}
})
here is the code I'm using to check firebase:
func checkSessionCode(_ code: String) {
print("checkSessionCode running")
let docRef = self.env.db.collection(K.sessions).document(code)
docRef.getDocument { (document, error) in
if document!.exists {
print("Document data: \(document!.data())")
self.env.doesCodeExist = true
} else {
print("Document does not exist")
self.env.doesCodeExist = false
}
}
}
and here is the code that should be executed if the code is found and applied:
func watchCloudDataAndUpdate() {
env.db.collection(K.sessions).document(env.currentSessionCode!).addSnapshotListener { (documentSnapshot, error) in
guard let document = documentSnapshot else {
print("Error fetching snapshot: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
Where did I go wrong, and what is this error all about...thanks in advance :)
EDIT: For clarity, it seems that the errors begin once the onAppear finishes executing...
This is why I need to stop coding after 1am...on my simulator, I deleted my app and relaunched and everything started working again...sometimes the simplest answers are the right ones...

Assign value of a Firestore document to a variable

I am trying to read the value of a Firestore document. I have tried doing it two different ways, but each fails.
In the first one, an error is thrown on the return line: Unexpected non-void return value in void function. I found out why this happened, and so, I implemented the second way.
import UIKit
import Firestore
func readAvailableLists(forUser user: String) -> [String] {
let db = Firestore.firestore()
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
return UserInformationDocument(dictionary: document.data()!)?.lists!
} else {
print("Document does not exist")
}
}
}
In the second method, I assign the UserInformationDocument(dictionary: document.data()!)?.lists! to a variable and return that variable at the end of the function (see code below). However, when I do this, it the function returns an empty array. What surprises me is that the print return the correct value, but after long after the function has executed the return statement. Is it because it is an async demand? And if so, how should I fix this?
import UIKit
import Firestore
func readAvailableLists(forUser user: String) -> [String] {
let db = Firestore.firestore()
var firestoreUserDocument: [String] = []
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
firestoreUserDocument = (UserInformationDocument(dictionary: document.data()!)?.lists!)!
print((UserInformationDocument(dictionary: document.data()!)?.lists!)!)
} else {
print("Document does not exist")
}
}
return firestoreUserDocument
}
The Firebase call is an asynchronous function. It takes extra time to execute because it's talking to a server (as you've noted) - as a result, the completion block (the block that defines document and err in your example) happens at a different time, outside of the rest of the body of the function. This means you can't return a value from inside it, but you can pass another closure through to it, to execute later. This is called a completion block.
func readAvailableLists(forUser user: String, completion: #escaping ([String]?, Error?) -> Void) -> [String] {
let db = Firestore.firestore()
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
// We got a document from Firebase. It'd be better to
// handle the initialization gracefully and report an Error
// instead of force unwrapping with !
let strings = (UserInformationDocument(dictionary: document.data()!)?.lists!)!
completion(strings, nil)
} else if let error = error {
// Firebase error ie no internet
completion(nil, error)
}
else {
// No error but no document found either
completion(nil, nil)
}
}
}
You could then call this function elsewhere in your code as so:
readAvailableLists(forUser: "MyUser", completion: { strings, error in
if let strings = strings {
// do stuff with your strings
}
else if let error = error {
// you got an error
}
})

Updating UI after retrieving device settings

I want to do something simple in Swift. I have to retrieve some setting from a device and then initialize some UI controls with those settings. It may take a few seconds to complete the retrieval so I don't want the code to continue until after the retrieval (async).
I have read countless posts on many websites including this one and read many tutorials. None seem to work for me.
Also, in the interest of encapsulation, I want to keep the details within the device object.
When I run the app I see the print from the initializing method before I see the print from the method.
// Initializing method
brightnessLevel = 100
device.WhatIsTheBrightnessLevel(level: &brightnessLevel)
print("The brightness level is \(brightnessLevel)")
// method with the data retrieval code
func WhatIsTheBrightnessLevel(level brightness: inout Int) -> CResults
{
var brightness: Int
var characteristic: HMCharacteristic
var name: String
var results: CResults
var timeout: DispatchTime
var timeoutResult: DispatchTimeoutResult
// Refresh the value by querying the lightbulb
name = m_lightBulbName
characteristic = m_brightnessCharacteristic!
brightness = 100
timeout = DispatchTime.now() + .seconds(CLightBulb.READ_VALUE_TIMEOUT)
timeoutResult = .success
results = CResults()
results.SetResult(code: CResults.code.success)
let dispatchGroup = DispatchGroup()
DispatchQueue.global(qos: .userInteractive).async
{
//let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
characteristic.readValue(completionHandler:
{ (error) in
if error != nil
{
results.SetResult(code: CResults.code.homeKitError)
results.SetHomeKitDescription(text: error!.localizedDescription)
print("Error in reading the brightness level for \(name): \(error!.localizedDescription)")
}
else
{
brightness = characteristic.value as! Int
print("CLightBulb: -->Read the brightness level. It is \(brightness) at " + Date().description(with: Locale.current))
}
dispatchGroup.leave()
})
timeoutResult = dispatchGroup.wait(timeout: timeout)
if (timeoutResult == .timedOut)
{
results.SetResult(code: CResults.code.timedOut)
}
else
{
print("CLightBulb: (After wait) The brightness level is \(brightness) at " + Date().description(with: Locale.current))
self.m_brightnessLevel = brightness
}
}
return(results)
}
Thank you!
If you're going to wrap an async function with your own function, it's generally best to give your wrapper function a completion handler as well. Notice the call to your completion handler. This is where you'd pass the resulting values (i.e. within the closure):
func getBrightness(characteristic: HMCharacteristic, completion: #escaping (Int?, Error?) -> Void) {
characteristic.readValue { (error) in
//Program flows here second
if error == nil {
completion(characteristic.value as? Int, nil)
} else {
completion(nil, error)
}
}
//Program flows here first
}
Then when you call your function, you just need to make sure that you're handling the results within the completion handler (i.e. closure):
getBrightness(characteristic: characteristic) { (value, error) in
//Program flows here second
if error == nil {
if let value = value {
print(value)
}
} else {
print("an error occurred: \(error.debugDescription)")
}
}
//Program flows here first
Always keep in mind that code will flow through before the async function completes. So you have to structure your code so that anything that's depending on the value or error returned, doesn't get executed before completion.

swift OSX: serially generating files using GCD

I am trying to generate .aiff files using NSSpeechSynthesizer.startSpeakingString() and am using GCd using a serial queue as NSSpeechSynthesizer takes in a string and creates an aiff file at a specified NSURL address. I used the standard for loop method for a list of strings in a [String:[String]] but this creates some files which have 0 bytes.
Here is the function to generate the speech:
func createSpeech(type: String, name: String) {
if !NSFileManager.defaultManager().fileExistsAtPath("\(dataPath)\(type)/\(name)/\(name).aiff"){
do{
try NSFileManager().createDirectoryAtPath("\(dataPath)\(type)/\(name)/", withIntermediateDirectories: true, attributes: nil)
let URL = NSURL(fileURLWithPath: "\(dataPath)\(type)/\(name)/\(name).aiff")
print("Attempting to save speech \(name).aiff")
self.synth.startSpeakingString(name, toURL: URL)
}catch{
print("error occured")
}
}
}
And here is the function that traverses the dictionary to create the files:
for key in self.nodeLibrary.keys{
dispatch_sync(GlobalBackgroundQueue){
let type = self.nodeLibrary[key]?.0
let name = key.componentsSeparatedByString("_")[0]
if !speechCheck.contains(name){
mixer.createSpeech(type!, name: name)
}
}
}
The globalBackgroundQueue is an alias to the GCD queue call _T for readability.
The routine runs fine, creates folders and subfolders as required by another external function then synthesizes the speech but in my case I always get one or some which don't load properly, giving 0 bytes or a too small number of bytes which makes the file unuseable.
I read the following post and have been using these GCD methods for a while but I'm not sure where I'm wrong here:
http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1
Any help greatly appreciated as usual
edit: Updated with completion closure and found possibly a bug
I have created a closure function as below and use it in another helper method which checks for any errors such as sourceFile.length being 0 once loaded. However, all files exhibit a 0 length which is not possible as I checked each file's audio properties using finder's property command+i.
func synthesise(type: String, name: String, completion: (success: Bool)->()) {
if !NSFileManager.defaultManager().fileExistsAtPath("\(dataPath)\(type)/\(name)/\(name).aiff"){
do{
try NSFileManager().createDirectoryAtPath("\(dataPath)\(type)/\(name)/", withIntermediateDirectories: true, attributes: nil)
let URL = NSURL(fileURLWithPath: "\(dataPath)\(type)/\(name)/\(name).aiff")
let success = self.synth.startSpeakingString(name, toURL: URL)
completion(success: success)
}catch{
print("error occured")
}
}
}
func loadSpeech(type: String, name: String){
synthesise(type, name: name, completion: {(success: Bool)->Void in
if success{
print("File \(name) created successfully with return \(self.synthSuccess), checking file integrity")
let URL = NSURL(fileURLWithPath: "\(self.dataPath)\(type)/\(name)/\(name).aiff")
do{
let source = try AVAudioFile(forReading: URL)
print("File has length: \(source.)")
}catch{
print("error loading file")
}
}else{
print("creation unsuccessful, trying again")
self.loadSpeech(type, name: name)
}
})
}
The files are generated with their folders and both the method startSpeakingString->Bool and the delegate function I have in my class which updates the synthSuccess property show true. So I load an AVAudioFile to check its length. All file lengths are 0. Which they are not except for one.
When I say bug, this is from another part of the app where I load an AVAudioEngine and start loading buffers with the frameCount argument set to sourceAudioFile.length which gives a diagnostic error but this is out of context right now.
startSpeakingString(_:toURL:) will start an asynchronous task in the background. Effectively, your code starts a number of asynchronous tasks that run concurrently. This may be the cause of the problem that you experience.
A solution would need to ensure that only one task is active at a time.
The problem with startSpeakingString(_:toURL:) is, that it starts an asynchronous task - but the function itself provides no means to get notified when this task is finished.
However, there's a delegate which you need to setup in order to be notified.
So, your solution will require to define a NSSpeechSynthesizerDelegate.
You may want to create your own helper class that exposes an asynchronous function which has a completion handler:
func exportSpeakingString(string: String, url: NSURL,
completion: (NSURL?, ErrorType?) -> ())
Internally, the class creates an instance of NSSpeechSynthesizer and NSSpeechSynthesizerDelegate and implements the delegate methods accordingly.
To complete the challenge, you need to search for an approach to run several asynchronous functions sequentially. There are already solutions on SO.
Edit:
I setup my own project to either confirm or neglect a possible issue in the NSSpeechSynthesizer system framework. So far, may own tests confirm that NSSpeechSynthesizer works as expected.
However, there are few subtleties worth mentioning:
Ensure you create a valid file URL which you pass as an argument to parameter URL in method startSpeakingString(:toURL:).
Ensure you choose an extension for the output file which is known by NSSpeechSynthesizer and the system frameworks playing this file, for example .aiff. Unfortunately, the documentation is quite lacking here - so I had to trial and error. The list of supported audio file formats by QuickTime may help here. Still, I have no idea how NSSpeechSynthesizer selects the output format.
The following two classes compose a simple easy to use library:
import Foundation
import AppKit
enum SpeechSynthesizerError: ErrorType {
case ErrorActive
case ErrorURL(message: String)
case ErrorUnknown
}
internal class InternalSpeechSynthesizer: NSObject, NSSpeechSynthesizerDelegate {
typealias CompletionFunc = (NSURL?, ErrorType?) -> ()
private let synthesizer = NSSpeechSynthesizer(voice: nil)!
private var _completion: CompletionFunc?
private var _url: NSURL?
override init() {
super.init()
synthesizer.delegate = self
}
// CAUTION: This call is not thread-safe! Ensure that multiple method invocations
// will be called from the same thread!
// Only _one_ task can be active at a time.
internal func synthesize(input: String, output: NSURL, completion: CompletionFunc) {
guard _completion == nil else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
completion(nil, SpeechSynthesizerError.ErrorActive)
}
return
}
guard output.path != nil else {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
completion(nil, SpeechSynthesizerError.ErrorURL(message: "The URL must be a valid file URL."))
}
return
}
_completion = completion
_url = output
if !synthesizer.startSpeakingString(input, toURL: output) {
fatalError("Could not start speeaking")
}
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
willSpeakWord characterRange: NSRange,
ofString string: String)
{
NSLog("willSpeakWord")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
willSpeakPhoneme phonemeOpcode: Int16)
{
NSLog("willSpeakPhoneme")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
didEncounterErrorAtIndex characterIndex: Int,
ofString string: String,
message: String)
{
NSLog("didEncounterErrorAtIndex")
}
internal func speechSynthesizer(sender: NSSpeechSynthesizer,
didFinishSpeaking finishedSpeaking: Bool)
{
assert(self._url != nil)
assert(self._url!.path != nil)
assert(self._completion != nil)
var error: ErrorType?
if !finishedSpeaking {
do {
error = try self.synthesizer.objectForProperty(NSSpeechErrorsProperty) as? NSError
} catch let err {
error = err
}
}
let url: NSURL? = NSFileManager.defaultManager().fileExistsAtPath(self._url!.path!) ? self._url : nil
let completion = self._completion!
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
if url == nil && error == nil {
error = SpeechSynthesizerError.ErrorUnknown
}
completion(url, error)
}
_completion = nil
_url = nil
}
}
public struct SpeechSynthesizer {
public init() {}
private let _synthesizer = InternalSpeechSynthesizer()
public func synthesize(input: String, output: NSURL, completion: (NSURL?, ErrorType?) -> ()) {
_synthesizer.synthesize(input, output: output) { (url, error) in
completion(url, error)
}
}
}
You can use it as shown below:
func testExample() {
let expect = self.expectationWithDescription("future should be fulfilled")
let synth = SpeechSynthesizer()
let url = NSURL(fileURLWithPath: "/Users/me/Documents/speech.aiff")
synth.synthesize("Hello World!", output: url) { (url, error) in
if let url = url {
print("URL: \(url)")
}
if let error = error {
print("Error: \(error)")
}
expect.fulfill()
}
self.waitForExpectationsWithTimeout(1000, handler: nil)
// Test: output file should exist.
}
In the code above, check the result of the call to synth.startSpeakingString(name, toURL: URL), which can return false if the synthesiser could not start speaking. If it fails, find out why, or just retry it.
Plus, add [NSSpeechSynthesiserDelegate][1], and look for the speechSynthesizer:didFinishSpeaking: callbacks there. When the synthesiser thinks it has finished speaking, check the file size. If it is zero, retry the operation.