Swift.assert - life after release

How often do you use Swift.assert()



in your code? I honestly use it quite often (If this is bad practice, then please write in the comments - why is it bad?). In my code you can often see, for example, such a call:



 Swift.assert(Thread.isMainThread)
      
      





Not so long ago, I decided that it would be nice to continue to observe the results from these calls, not only as part of the launch of the application in the simulator / device, but also from the actions of real users. By the way, here we can talk about Swift.precondition()



, Swift.fatalError()



, etc., although I try to avoid them. I read more about Unrecoverable Errors in Swift in this publication and it turned out to be very informative.



Closer to the point: in the code I found about 300 such calls. None of them worked during the usual local testing, and it pleased me. But I suggested that user actions might still trigger some of the calls.



From the user's point of view, crashes should not happen (again, my opinion). In extreme cases, the user must understand that some scenario went wrong and the development team is already working on fixing it. Similar exceptions have always been handled by me, and for the user it could affect in the most harmless form. For example, one of the hundreds of table cells was simply invisible.



With the user more or less everything is clear. It remains to deal with the delivery of logs to the developer. Firstly, it was required with minimal efforts to replace the current calls in the code with calls sending logs somewhere outside the application. Secondly, it was required to accurately localize the scene of the incident, otherwise it would be practically impossible to correlate the exception with the real code. Thirdly, it should be noted that modified calls can work during Unit testing, where Thread.isMainThread



should already be ignored, because I use the RxTest framework for certain types of testing (here I am also ready to listen to advice and criticism). The main point was that locally all exceptions should be triggered as before, i.e. Loggin.assert()



should fire at the same time that Swift.assert()



would fire



Fabric ( Crashlytics ) provides a great way to send events. It looks like this:



 Crashlytics.sharedInstance().recordCustomExceptionName("", reason: ""...
      
      





It remains to pack Crashlytics into some framework that can be loaded in its entirety into the application and in a truncated form (without Crashlytics dependency) in test targets.



I decided to do the “ packaging ” through CocoaPods :



 Pod::Spec.new do |s| s.name = 'Logging' ... s.subspec 'Base' do |ss| ss.source_files = 'Source/Logging+Base.swift' ss.dependency 'Crashlytics' end s.subspec 'Test' do |ss| ss.source_files = 'Source/Logging+Test.swift' end end
      
      





The code for the "combat target" is as follows:



 import Crashlytics public enum Logging { public static func send(_ reason: String? = nil, __file: String = #file, __line: Int = #line) { let file = __file.components(separatedBy: "/").last ?? __file let line = "\(__line)" let name = [line, file].joined(separator: "_") Crashlytics.sharedInstance().recordCustomExceptionName(name, reason: reason ?? "no reason", frameArray: []) } public static func assert(_ assertion: @escaping @autoclosure () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) { if assertion() == false { self.assertionFailure(reason, __file: __file, __line: __line) } } public static func assert(_ assertion: @escaping () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) { if assertion() == false { self.assertionFailure(reason, __file: __file, __line: __line) } } public static func assertionFailure(_ reason: String? = nil, __file: String = #file, __line: Int = #line) { Swift.assertionFailure(reason ?? "") self.send(reason, __file: __file, __line: __line) } }
      
      





For the "test target" i.e. without Crashlytics dependency:



 import Foundation public enum Logging { public static func send(_ reason: String? = nil, __file: String = #file, __line: Int = #line) { // } public static func assert(_ assertion: @escaping @autoclosure () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) { // } public static func assert(_ assertion: @escaping () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) { // } public static func assertionFailure(_ reason: String? = nil, __file: String = #file, __line: Int = #line) { // } }
      
      





Results:



Exceptions really started to work. Most of them reported an incorrect format for the received data: Decodable



sometimes received data with the Decodable



type. Sometimes the logs for Thread.isMainThread



worked, which were very quickly fixed in the next releases. The most interesting errors were miraculously caught by NSException .



Thanks for attention.



PS If you suddenly send such logs to Crashlytics too often, then the service may recognize your actions as spam. And you will see the following message:

Due to improper usage, non-fatal reporting has been disabled for multiple builds. Learn how to re-enable reporting in our documentation.
Therefore, it is worth considering in advance the frequency of sending logs. Otherwise, all build logs may be in danger of being ignored by Crashlytics.



All Articles