My iOS app in Xcode calls the following method both on app launch and then every 10 minutes. Although I can't reproduce the crash, Crashlytics has it crashing regularly around 'group.leave()', or when each submission task is leaving the dispatch group. Any ideas what could cause it?
My thoughts:
- dispatch group enter/leaves are unbalanced. could be the result of completion closure returning multiple times, but I don' think that's happening
- race conditions - maybe other parts of the app are trying to access the same data causing invalidations etc. would using a dispatch semaphore help with this?
Below is the code and stack trace. TIA!
Crashing method:
class func checkForSubmissionsToUpload(onSuccess: @escaping () -> Void){
let realm = Utils.getRealm()
if let uploadSubmissions = realm?.objects(STLocalSubmission.self).filter("needsUpload = %@", true) {
if uploadSubmissions.count == 0 {
onSuccess()
} else {
let group = DispatchGroup()
for submission in uploadSubmissions{
let isValidSubmission = !submission.isInvalidated
if isValidSubmission, let form = submission.formObject, let projectID = submission.projectID.value {
group.enter()
submitSubmission(
submission,
form: form,
projectID: projectID,
draft: submission.draft,
completion: { (response) in
DispatchQueue.main.async {
SVProgressHUD.dismiss()
group.leave()
if let error = response?["error"] as? Bool, let message = APIManager.messageFrom(dictionary: response) {
if error {
print("error: \(message)")
SVProgressHUD.showError(withStatus: message)//.showAlert(withTitle: "Error.", andMessage: message)
return
}
}
if (response == nil || response!.count <= 1) {
if let extraErrors = response?["extraErrors"] as? [Any], let message = extraErrors.first as? String {
print("error: \(message)")
// self.showAlert(withTitle: "Error.", andMessage: message)
SVProgressHUD.showError(withStatus: message)
return
}
}
if let submissionID = response?["submission_id"] as? Int ?? Int(submission.id) {
let serverSubmission = STServerSubmission(id: submissionID,
submission: submission)
// add the submission data to realm
let realm = Utils.getRealm()
try? realm?.write {
realm?.delete(submission)
realm?.add(serverSubmission, update: .all)
}
} else {
print("error - no submission id returned after submitting form data")
}
}
})
}
}
group.notify(queue: DispatchQueue.main) {
onSuccess()
}
}
}
}
UPDATE:
Including 'submitSubmission', which then calls 'request':
class func submitSubmission(_ submission: STLocalSubmission, form: STForm, projectID: Int, draft: Bool, completion: @escaping ([AnyHashable : Any]?) -> Void) {
var fields = [[String : Any?]]()
var approvalUserID = [Int?]()
if (form.isInvalidated || submission.isInvalidated) { return }
submission.uploadAllImages() {
response in
if response == false {
print("error submitting submission")
var errors = [String : Any]()
errors["error"] = true
errors["message"] = "Error uploading images"
completion(errors)
return
}
for row in submission.dataRows {
if (row.isInvalidated) { continue }
let rowObject = form.rowByTag(row.intTag)
if rowObject != nil {
if !draft {
// only perform data validation if it is not a draft
if rowObject?.required == true && row.serverValue == nil {
print("error submitting submission - no row server value for required row \(rowObject?.tag ?? 0)")
var errors = [String : Any]()
errors["error"] = true
if let title = rowObject?.title,
let formID = submission.formID.value,
let formVersionId = submission.formVersionID.value {
errors["message"] = "\nIncomplete Form.\nThe following field is required: \n\n \(title) \n\nForm ID: \(formID) \nForm Version ID: \(formVersionId)"
}
completion(errors)
return
}
}
var field = [String : Any?]()
field["id"] = row.intTag
field["label"] = rowObject?.title
print("row title = \(rowObject?.title) value = \(row.serverValue)")
if rowObject?.multiple ?? true{
if row.serverValue is [Any]{
field["value"] = row.serverValue
}else{
var array = [Any?]()
array.append(row.serverValue)
field["value"] = array
}
}else{
field["value"] = row.serverValue
}
field["required"] = rowObject?.required
if rowObject?.type == .register {
var array = ""
if row.registerSignIns.count != 0 {
let selectedUsers = row.registerSignIns
for user in selectedUsers {
if user.id != "" {
if array == "" {
array = "\(user.id)"
} else {
array = array + ",\(user.id)"
}
}
}
field["value"] = array
}
}
//check if array contains field for that id already and append the value part
if rowObject?.multiple ?? true {
var existCount = 0
for index in 0..<fields.count {
if fields[index]["id"] as? Int == row.intTag{
if (fields[index]["value"] as? [Any?]) != nil{
fields[index]["value"] = row.serverValue
}
}else{
existCount += 1
}
}
if existCount == fields.count {
let jsonData = try? JSONSerialization.data(withJSONObject: field, options: [])
let jsonString = String(data: jsonData!, encoding: .utf8)
print(jsonString ?? 0)
fields.append(field)
}
} else{
fields.append(field)
}
}
}
for approversID in submission.selectedUsers {
approvalUserID.append(approversID.id)
}
let parameters: [String : Any] = [ "fields" : fields,
"project_id" : projectID,
"draft" : draft,
"approval_users" : approvalUserID]
var stringId: String? = nil
if submission.isNewForm, let id = submission.formVersionID.value {
stringId = String(id)
}
else {
stringId = submission.id
}
guard let unwrappedID = stringId else {
print("error - submission has no id!")
return
}
let target = submission.isNewForm ? "forms" : "submissions"
APIManager.request(to: "api/\(target)/\(unwrappedID)/submit",
parameters: parameters,
needsAuthToken: true,
post: true,
completion: completion)
}
}
private class func request(to path: String,
parameters: [String: Any],
needsAuthToken: Bool, post: Bool,
completion: @escaping ([AnyHashable : Any]?) -> Void) {
// always set the apitoken in the URL, whether it's a GET or POST request
var getParameters: [String : Any]?
var postParameters: [String : Any]?
if post {
getParameters = [String : Any]()
postParameters = parameters
} else {
getParameters = parameters
}
if needsAuthToken {
guard let authToken = FXKeychain.default().object(forKey: "authToken") as? String else {
print("error with request to path: \(path) - no auth token")
FXKeychain.default().removeObject(forKey: "loggedIn")
let storyBoard = UIStoryboard(name: "Install+Login", bundle: nil)
var viewController: UIViewController? = nil
if UserDefaults.standard.object(forKey: "installCode") == nil {
viewController = storyBoard.instantiateViewController(withIdentifier: "InstallCodeVC")
} else {
viewController = storyBoard.instantiateViewController(withIdentifier: "LoginVC")
}
UIApplication.shared.delegate?.window??.rootViewController = viewController
UIApplication.shared.delegate?.window??.makeKeyAndVisible()
return
}
getParameters!["api_token"] = authToken
// print("Auth Token \(authToken)")
} else {
getParameters!["api_token"] = apiToken
}
// add code to completion closure. log out when returns unauthorised
let finalCompletion: ([AnyHashable : Any]?) -> (Void) = {
response in
DispatchQueue.main.async {
if let extraErrors = response?["extraErrors"] as? [Any] {
for object in extraErrors {
if let message = object as? String {
if message.contains("HTTP Status Code: 401") {
// log user out
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
FXKeychain.default().removeObject(forKey: "authToken")
FXKeychain.default().removeObject(forKey: "userID")
let storyBoard = UIStoryboard(name: "Login", bundle: nil)
var viewController: UIViewController? = nil
viewController = storyBoard.instantiateViewController(withIdentifier: "loginVC")
UIApplication.shared.delegate?.window??.rootViewController = viewController
UIApplication.shared.delegate?.window??.makeKeyAndVisible()
return
}
}
}
}
completion(response)
}
}
print("API Request = \(path) - Params = \(getParameters)")
if path == "check-install-code" {
STNetworkManager.request(toURL: baseURLString + path, withGetParameters: getParameters, postParameters: postParameters, completion: finalCompletion)
} else {
STNetworkManager.request(toURL: ((UserDefaults.standard.object(forKey: "baseUrl") as? String ?? nil) ?? baseURLString) + path, withGetParameters: getParameters, postParameters: postParameters, completion: finalCompletion)
}
}
Crashlytics stack trace:
Crashed: com.apple.main-thread
0 libdispatch.dylib 0x689fc dispatch_group_leave$VARIANT$mp.cold.1 + 36
1 libdispatch.dylib 0x4d5c _dispatch_group_wake + 138
2 SitePlex 0xfc3f8 closure #1 in closure #1 in static APIManager.checkForSubmissionsToUpload(onSuccess:) + 1683 (APIManager.swift:1683)
3 SitePlex 0xf1a8c thunk for @escaping @callee_guaranteed () -> () + 4299774604 (<compiler-generated>:4299774604)
4 libdispatch.dylib 0x637a8 _dispatch_call_block_and_release + 24
5 libdispatch.dylib 0x64780 _dispatch_client_callout + 16
6 libdispatch.dylib 0x10bdc _dispatch_main_queue_drain + 928
7 libdispatch.dylib 0x1082c _dispatch_main_queue_callback_4CF$VARIANT$mp + 36
8 CoreFoundation 0x91a2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
9 CoreFoundation 0x756c8 __CFRunLoopRun + 2080
10 CoreFoundation 0x79da0 CFRunLoopRunSpecific + 584
11 GraphicsServices 0x1998 GSEventRunModal + 160
12 UIKitCore 0x37180c -[UIApplication _run] + 868
13 UIKitCore 0x371484 UIApplicationMain + 312
14 SitePlex 0x880c main + 15 (AppDelegate.swift:15)
15 ??? 0x1d2f90344 (Missing)
com.apple.uikit.eventfetch-thread
0 libsystem_kernel.dylib 0x1030 mach_msg2_trap + 8
1 libsystem_kernel.dylib 0x12b18 mach_msg2_internal + 76
2 libsystem_kernel.dylib 0x12db8 mach_msg_overwrite + 484
3 libsystem_kernel.dylib 0x1524 mach_msg + 20
4 CoreFoundation 0x741c8 __CFRunLoopServiceMachPort + 156
5 CoreFoundation 0x75360 __CFRunLoopRun + 1208
6 CoreFoundation 0x79da0 CFRunLoopRunSpecific + 584
7 Foundation 0x3e138 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 208
8 Foundation 0x3e034 -[NSRunLoop(NSRunLoop) runUntilDate:] + 60
9 UIKitCore 0x493cd8 -[UIEventFetcher threadMain] + 404
10 Foundation 0x55c9c __NSThread__start__ + 704
11 libsystem_pthread.dylib 0x30ec _pthread_start + 116
12 libsystem_pthread.dylib 0x172c thread_start + 8
Realm notification listener
0 libsystem_kernel.dylib 0x2800 kevent + 8
1 Realm 0x39c6d8 realm::_impl::ExternalCommitHelper::listen() + 156
2 Realm 0x39c828 void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct>>, realm::_impl::ExternalCommitHelper::ExternalCommitHelper(realm::_impl::RealmCoordinator&)::$_0>>(void*) + 52
3 libsystem_pthread.dylib 0x30ec _pthread_start + 116
4 libsystem_pthread.dylib 0x172c thread_start + 8
com.google.firebase.crashlytics.MachExceptionServer
0 FirebaseCrashlytics 0x1a4e4 FIRCLSProcessRecordAllThreads + 184
1 FirebaseCrashlytics 0x1a8c4 FIRCLSProcessRecordAllThreads + 1176
2 FirebaseCrashlytics 0x14250 FIRCLSHandler + 48
3 FirebaseCrashlytics 0x16bc8 FIRCLSMachExceptionServer + 1248
4 libsystem_pthread.dylib 0x30ec _pthread_start + 116
5 libsystem_pthread.dylib 0x172c thread_start + 8
com.apple.CoreMotion.MotionThread
0 libsystem_kernel.dylib 0x1030 mach_msg2_trap + 8
1 libsystem_kernel.dylib 0x12b18 mach_msg2_internal + 76
2 libsystem_kernel.dylib 0x12db8 mach_msg_overwrite + 484
3 libsystem_kernel.dylib 0x1524 mach_msg + 20
4 CoreFoundation 0x741c8 __CFRunLoopServiceMachPort + 156
5 CoreFoundation 0x75360 __CFRunLoopRun + 1208
6 CoreFoundation 0x79da0 CFRunLoopRunSpecific + 584
7 CoreFoundation 0xb8c58 CFRunLoopRun + 60
8 CoreMotion 0x12fac CLMotionActivity::isTypeInVehicle(CLMotionActivity::Type) + 23288
9 libsystem_pthread.dylib 0x30ec _pthread_start + 116
10 libsystem_pthread.dylib 0x172c thread_start + 8
com.apple.NSURLConnectionLoader
0 libsystem_kernel.dylib 0x1030 mach_msg2_trap + 8
1 libsystem_kernel.dylib 0x12b18 mach_msg2_internal + 76
2 libsystem_kernel.dylib 0x12db8 mach_msg_overwrite + 484
3 libsystem_kernel.dylib 0x1524 mach_msg + 20
4 CoreFoundation 0x741c8 __CFRunLoopServiceMachPort + 156
5 CoreFoundation 0x75360 __CFRunLoopRun + 1208
6 CoreFoundation 0x79da0 CFRunLoopRunSpecific + 584
7 CFNetwork 0x228520 _CFURLStorageSessionDisableCache + 50860
8 Foundation 0x55c9c __NSThread__start__ + 704
9 libsystem_pthread.dylib 0x30ec _pthread_start + 116
10 libsystem_pthread.dylib 0x172c thread_start + 8
Thread
0 libsystem_kernel.dylib 0x14f0 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x1c1c _pthread_wqthread + 360
2 libsystem_pthread.dylib 0x1720 start_wqthread + 8
Thread
0 libsystem_kernel.dylib 0x14f0 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x1c1c _pthread_wqthread + 360
2 libsystem_pthread.dylib 0x1720 start_wqthread + 8
Thread
0 libsystem_kernel.dylib 0x14f0 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x1c1c _pthread_wqthread + 360
2 libsystem_pthread.dylib 0x1720 start_wqthread + 8
Thread
0 libsystem_pthread.dylib 0x1718 start_wqthread + 270
Thread
0 libsystem_kernel.dylib 0x14f0 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x1c1c _pthread_wqthread + 360
2 libsystem_pthread.dylib 0x1720 start_wqthread + 8