This article describes how to use Crashlytics logging in objective-c. However, after going throught the installation steps for propertly referencing Crashlytics and Fabric into my project, I don't seem to have access to that method.
Looking at the Crashlytics.h file, I can see it defined using compiler flags:
#ifdef DEBUG
#define CLS_LOG(__FORMAT__, ...) CLSNSLog((#"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define CLS_LOG(__FORMAT__, ...) CLSLog((#"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif
This block just appears to wrap the CLSNLog and the CLSLog functions depending on the compiler flag.
So, thinking I'd just go straight to the source, I tried to reference CLSLog directly from a swift file. Still no luck:
My-Bridging-Header.h:
#import <Crashlytics/Crashlytics.h>
Log.swift:
import Foundation
import Fabric
import Crashlytics
func Log(message: String) {
NSLog("%#", message)
CLS_LOG("%#", message)
CLSLog("%#", message)
}
The last two lines in the Log function throw the error, Use of unresolved identifier. Crashlytics crash reporting works just fine, except for the logging feature. According to this article, logging support for Swift has been implemented.
As far as versions go, I'm running the latest version of Fabric/Crashlytics (December release, at the time of this post).
(Interesting note, I can see/use CLSLogv()...)
Does anyone know the correct way to incorporate CLS_LOG for use in a Swift project?
Mike from Crashlytics here.
To use custom logging in Swift, just use CLSLogv or CLSNSLogv. You need to make an array and then call getVaList function on that array.
Here's a snippet:
CLSLogv("Log something %d %d %#", getVaList([1, 2, "three"]))
For CLSNSLogv:
CLSNSLogv("hello %#", getVaList(["goodbye"]))
Here is my version adapted from Dima's answer. I have no need of the arguments, since you can do all the formatting within the Swift string that you pass.
func DebugLog(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
let output: String
if let filename = URL(fileURLWithPath: file.description).lastPathComponent.components(separatedBy: ".").first {
output = "\(filename).\(function) line \(line) $ \(message)"
} else {
output = "\(file).\(function) line \(line) $ \(message)"
}
#if targetEnvironment(simulator)
NSLogv("%#", getVaList([output]))
#elseif DEBUG
CLSNSLogv("%#", getVaList([output]))
#else
CLSLogv("%#", getVaList([output]))
#endif
}
And you would use it like this:
DebugLog("this is a log message")
DebugLog("this is a log message \(param1) \(param2)")
EDIT: Updated to Swift 3.1
I needed something similar to CLS_LOG() in Swift that printed out contextual information about the location of the call. Normally this would not be possible without preprocessor directives but I found out how to replicate this behavior pretty closely in Swift here:
https://developer.apple.com/swift/blog/?id=15
The identifiers we need (#file, #function, #line) show information about the caller if you set them as default values in an argument list.
Note: If you are logging errors that may have % symbols in them, such as network query strings, this may crash. You'll need to join the string first (e.g. let string = "\(filename).\(function) line \(line) $ \(message)")
Swift 3 version (note: this is a global function, so it should be placed outside of any struct or class definition):
/// Usage:
///
/// CLS.log("message!")
/// CLS.log("message with parameter 1: %# and 2: %#", ["First", "Second"])
///
func CLS_LOG_SWIFT(format: String = "", _ args: [CVarArg] = [], file: String = #file, function: String = #function, line: Int = #line)
{
let filename = URL(string: file)?.lastPathComponent.components(separatedBy: ".").first
#if DEBUG
CLSNSLogv("\(filename).\(function) line \(line) $ \(format)", getVaList(args))
#else
CLSLogv("\(filename).\(function) line \(line) $ \(format)", getVaList(args))
#endif
}
Swift 2 version:
// CLS_LOG_SWIFT()
// CLS_LOG_SWIFT("message!")
// CLS_LOG_SWIFT("message with parameter 1: %# and 2: %#", ["First", "Second"])
func CLS_LOG_SWIFT(format: String = "",
_ args:[CVarArgType] = [],
file: String = __FILE__,
function: String = __FUNCTION__,
line: Int = __LINE__)
{
let filename = NSURL(string:file)?.lastPathComponent?.componentsSeparatedByString(".").first
#if DEBUG
CLSNSLogv("\(filename).\(function) line \(line) $ \(format)", getVaList(args))
#else
CLSLogv("\(filename).\(function) line \(line) $ \(format)", getVaList(args))
#endif
}
// CLS_LOG() output: -[ClassName methodName:] line 10 $
// CLS_LOG_SWIFT() output: ClassName.methodName line 10 $
And here is a gist with some more information and the actual file I put this code in: https://gist.github.com/DimaVartanian/a8aa73ba814a61f749c0
As you can see it is pretty close to the original macro and only differs in that you can't see if you are calling a class method or an instance method, and that you need to include your format argument list enclosed in an array. Both are limitations I believe there is no way around right now but pretty minor. You also need to make sure DEBUG is defined in your Swift compiler flags. It does not carry over from your regular flags automatically.
You have to create an intermediary bridge like this:
CrashlyticsBridge.h:
#import <Foundation/Foundation.h>
#interface CrashlyticsBridge : NSObject
+ (void)log:(NSString *)message;
#end
CrashlyticsBridge.m
#import "CrashlyticsBridge.h"
#import <Crashlytics/Crashlytics.h>
#implementation CrashlyticsBridge
+ (void)log:(NSString *)message {
CLS_LOG(#"%#", message);
}
#end
My-Bridging-Header.h:
#import "CrashlyticsBridge.h"
Then, you can simply add that to your Log function:
func Log(message: String) {
CrashlyticsBridge.log(message)
}
This will give you the Crashlytics logging and NSLogging while you are debugging.
Swift 3 compatible
You'll need to set up a compiler flag to use the Swift preprocessor - go to the Swift Compiler - Custom Flags section of Build Settings to set up a -D DEBUG flag
func dLog(message: Any, filename: String = #file, function: String = #function, line: Int = #line) {
#if DEBUG
print("[\(filename.lastPathComponent):\(line)] \(function) - \(message)")
#else
CLSLogv("[\(filename.lastPathComponent):\(line)] \(function) - \(message)", getVaList([""]))
#endif
}
dLog(object)
Swift 3 compatible version for log message in Crashlytics
func CLS_LOG_SWIFT(_ format: String = "", _ args: [CVarArg] = [], file: String = #file, function: String = #function, line: Int = #line) {
let formatString: String!
if let filename = file.components(separatedBy: "/").last?.components(separatedBy: ".").first {
formatString = "\(filename).\(function) line \(line) $ \(format)"
}else{
formatString = "\(file).\(function) line \(line) $ \(format)"
}
#if DEBUG
CLSNSLogv(formatString, getVaList(args))
#else
CLSLogv(formatString, getVaList(args))
#endif
}
How about like this?
import Foundation
import Crashlytics
func CLSLog(_ format: String = "", _ args: CVarArg..., file: String = #file, function: String = #function, line: Int = #line) {
let formatString: String!
if let filename = file.components(separatedBy: "/").last?.components(separatedBy: ".").first {
formatString = "\(filename).\(function) line \(line) $ \(format)"
} else {
formatString = "\(file).\(function) line \(line) $ \(format)"
}
#if DEBUG
CLSNSLogv(formatString, getVaList(args))
#else
CLSLogv(formatString, getVaList(args))
#endif
}
No need for the array then, just list the variadic parameters
CLSLog("message")
CLSLog("message %# %#", "one", "two")
Any one who want to log error usin Crashlytics can use the below code and its working fine for me :)
Crashlytics.sharedInstance().recordError(error)
error is NSERROR object that holds the error that produced during some action
Related
I'm reading about some good practices for developing iOS apps and looking at the possibility of monitoring logs of an iOS app installed from App Store using Console.app. So, I was testing here, but I noticed that print statements didn't show up in Console.app, only NSLog does. My question is: is there any way that is possible to see logs that are made with print commands within iOS apps installed on a device? With Frida, Console.app or any other means?
If there is no other method, does it mean that print commands are more secure than NSLog? This seems very counterintuitive to me 🤔
print statement in iOS apps are not logged to one the [persistent] logging systems on iOS, therefore you can not access the output of an app via print statements if they had occur in the past.
By default you can only seem the output of print commands in XCode output panel. However the print commands themselves are always included in the debug and release builds and are therefore executed. Just the output of the print statements is discarded if no XCode is connected to retrieve it.
I tested this by building the following SwiftUI test app (see the end of this answer), made sure the Archive profile is set to RELEASE and the archived the project, to build an IPA file.
The IPA file was then analyzed in IdaPro to see the actual ARM assembler code.
And in all tests using different options (e.g. "Rebuild from Bitcode" (de)activated) the code was always there.
Therefore if you attach Frida to an app you can e.g. hook the print method print(_:separator:terminator:) to retrieve all output that would otherwise be discarded.
struct ContentView: View {
#State var number : Int = 1
var body: some View {
VStack {
Button(" Print ") {
print("print test abcdefgh number %d", number)
}.padding()
Button(" os_log ") {
os_log("os_log test abcdefgh number %d", number)
}.padding()
Button("randomize") {
self.number = Int.random(in: 1...11111)
}.padding()
}
}
}
If, and only if, you want to use print and printf in your app to go to a file or whatever file descriptor:
import SwiftUI
import Darwin
import os.log
extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!
static let `default` = OSLog(subsystem: subsystem, category: "default")
}
extension TestApp {
func subscribeFileToStderrAndStdoutIfNotAttachedToDebugger() {
if isatty(STDERR_FILENO) != 1 {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let logfileUrl = documentsUrl.appendingPathComponent("out.log")
logfileUrl.withUnsafeFileSystemRepresentation { path in
guard let path = path else {
return
}
print("redirect stdout and stderr to: \(String(cString: path))")
let file = fopen(path, "a")
assert(file != nil, String(cString: strerror(errno)))
let fd = fileno(file)
assert(fd >= 0, String(cString: strerror(errno)))
let result1 = dup2(fd, STDERR_FILENO)
assert(result1 >= 0, String(cString: strerror(errno)))
let result2 = dup2(fd, STDOUT_FILENO)
assert(result2 >= 0, String(cString: strerror(errno)))
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
subscribeFileToStderrAndStdoutIfNotAttachedToDebugger()
return true
}
}
I have a framework built in Objetive-C. That framework is to connect and interact with a Bluetooth device.
In the demo code, the Objetive-C delegate function looks like. The demo code was provided by the creator of the framework.
-(void)babyScaleManagerScanDevices:(NSArray<ELBabyScaleDeviceModel *> *)babyScaleDevices{
NSLog(#"babyScaleManagerScanDevices = %#",babyScaleDevices);
ELBabyScaleDeviceModel *model = babyScaleDevices.firstObject;
}
I've included the framework in my swift project and imported the headers. I'm trying to obtain the same result by doing:
func babyScaleManagerScanDevices(_ babyScaleDevices: [ELBabyScaleDeviceModel]?) {
guard let device = babyScaleDevices?.first else {
print("Error unwrapping first device")
return
}
print("Device: \(String(describing: device))")
}
I get the following exception:
Thread 1: Precondition failed: NSArray element failed to match the Swift Array Element type
Expected ELBabyScaleDeviceModel but found ELPeripheralModel
Precondition failed: NSArray element failed to match the Swift Array Element type
Expected ELBabyScaleDeviceModel but found ELPeripheralModel: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1100.2.274.2/swift/stdlib/public/core/ArrayBuffer.swift, line 354
Inspecting babyScaleDevices array show:
babyScaleDevices [ELBabyScaleDeviceModel]? 1 value some
[0] ELPeripheralModel * 0x281cae100 0x0000000281cae100
This result is the same in the demo code in Objetive-C and my Swift project.
The class ELBabyScaleDeviceModel.h looks like:
#import "ELPeripheralModel.h"
NS_ASSUME_NONNULL_BEGIN
#interface ELBabyScaleDeviceModel : ELPeripheralModel
#end
NS_ASSUME_NONNULL_END
Can you explain me what is happening?
You have to specify Array to NSArray
Add this line to your code
let devices = babyScaleDevices as NSArray
You can try this
func babyScaleManagerScanDevices(_ babyScaleDevices: [ELBabyScaleDeviceModel]?) {
let devices = babyScaleDevices as NSArray
guard let device = devices.firstObject else {
print("Error unwrapping first device")
return
}
print("Device: \(String(describing: device))")
}
And after then check this -> Array vs NSArray
Try to change
func babyScaleManagerScanDevices(_ babyScaleDevices: [ELBabyScaleDeviceModel]?)
to
func babyScaleManagerScanDevices(_ babyScaleDevices: [Any]?)
and cast specific elements to ELBabyScaleDeviceModel, for example, in for.
It seems like the creator of this framework put ELPeripheralModel in the array instead of ELBabyScaleDeviceModel
I think its just the way that the code is bridged to Swift.
Could you try to specify the type as [ELPeripheralModel] and then cast it?
func babyScaleManagerScanDevices(_ babyScaleDevices: [ELPeripheralModel]?) {
guard let devices = devices = babyScaleDevices as? [ELBabyScaleDeviceModel],
let device = devices?.first else {
print("Error unwrapping first device")
return
}
print("Device: \(String(describing: device))")
}
Consider the following example:
import Foundation
import os.log
class OSLogWrapper {
func logDefault(_ message: StaticString, _ args: CVarArg...) {
os_log(message, type: .default, args)
}
func testWrapper() {
logDefault("WTF: %f", 1.2345)
}
}
If I create a new instance of OSLogWrapper and call testWrapper()
let logger = OSLogWrapper()
logger.testWrapper()
I get the following output in the Xcode console:
2018-06-19 18:21:08.327979-0400 WrapperWTF[50240:548958] WTF: 0.000000
I've checked everything I can think of and I can't make heads or tails of what's going wrong here. Looking through the documentation isn't yielding anything helpful.
The compiler implements variadic arguments by casting each argument to the declared variadic type, packaging them into an Array of that type, and passing that array to the variadic function. In the case of testWrapper, the declared variadic type is CVarArg, so when testWrapper calls logDefault, this is what happens under the covers: testWrapper casts 1.2345 to a CVarArg, creates an Array<CVarArg>, and passes it to logDefault as args.
Then logDefault calls os_log, passing it that Array<CVarArg> as an argument. This is the bug in your code. The bug is quite subtle. The problem is that os_log doesn't take an Array<CVarArg> argument; os_log is itself variadic over CVarArg. So Swift casts args (an Array<CVarArg>) to CVarArg, and sticks that casted CVarArg into another Array<CVarArg>. The structure looks like this:
Array<CVarArg> created in `logDefault`
|
+--> CVarArg (element at index 0)
|
+--> Array<CVarArg> (created in `testWrapper`)
|
+--> CVarArg (element at index 0)
|
+--> 1.2345 (a Double)
Then logDefault passes this new Array<CVarArg> to os_log. So you're asking os_log to format its first element, which is (sort of) an Array<CVarArg>, using %f, which is nonsense, and you happen to get 0.000000 as output. (I say “sort of” because there are some subtleties here which I explain later.)
So, logDefault passes its incoming Array<CVarArg> as one of potentially many variadic parameters to os_log, but what you actually want logDefault to do is pass on that incoming Array<CVarArg> as the entire set of variadic parameters to os_log, without re-wrapping it. This is sometimes called “argument splatting” in other languages.
Sadly for you, Swift doesn't yet have any syntax for argument splatting. It's been discussed more than once in Swift-Evolution (in this thread, for example), but there's not yet a solution on the horizon.
The usual solution to this problem is to look for a companion function that takes the already-bundled-up variadic arguments as a single argument. Often the companion has a v added to the function name. Examples:
printf (variadic) and vprintf (takes a va_list, C's equivalent of Array<CVarArg>)
NSLog (variadic) and NSLogv (takes a va_list)
-[NSString initWithFormat:] (variadic) and -[NSString WithFormat:arguments:] (takes a va_list)
So you might go looking for an os_logv. Sadly, you won't find one. There is no documented companion to os_log that takes pre-bundled arguments.
You have two options at this point:
Give up on wrapping os_log in your own variadic wrapper, because there is simply no good way to do it, or
Take Kamran's advice (in his comment on your question) and use %# instead of %f. But note that you can only have a single %# (and no other format specifiers) in your message string, because you're only passing a single argument to os_log. The output looks like this:
2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
"1.2345"
)
You could also file an enhancement request radar at https://bugreport.apple.com asking for an os_logv function, but you shouldn't expect it to be implemented any time soon.
So that's it. Do one of those two things, maybe file a radar, and move on with your life. Seriously. Stop reading here. There's nothing good after this line.
Okay, you kept reading. Let's peek under the hood of os_log. It turns out the implementation of the Swift os_log function is part of the public Swift source code:
#_exported import os
#_exported import os.log
import _SwiftOSOverlayShims
#available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *)
public func os_log(
_ type: OSLogType,
dso: UnsafeRawPointer = #dsohandle,
log: OSLog = .default,
_ message: StaticString,
_ args: CVarArg...)
{
guard log.isEnabled(type: type) else { return }
let ra = _swift_os_log_return_address()
message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
// Since dladdr is in libc, it is safe to unsafeBitCast
// the cstring argument type.
buf.baseAddress!.withMemoryRebound(
to: CChar.self, capacity: buf.count
) { str in
withVaList(args) { valist in
_swift_os_log(dso, ra, log, type, str, valist)
}
}
}
}
So it turns out there is a version of os_log, called _swift_os_log, that takes pre-bundled arguments. The Swift wrapper uses withVaList (which is documented) to convert the Array<CVarArg> to a va_list and passes that on to _swift_os_log, which is itself also part of the public Swift source code. I won't bother quoting its code here because it's long and we don't actually need to look at it.
Anyway, even though it's not documented, we can actually call _swift_os_log. We can basically copy the source code of os_log and turn it into your logDefault function:
func logDefaultHack(_ message: StaticString, dso: UnsafeRawPointer = #dsohandle, _ args: CVarArg...) {
let ra = _swift_os_log_return_address()
message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
buf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buf.count) { str in
withVaList(args) { valist in
_swift_os_log(dso, ra, .default, .default, str, valist)
}
}
}
}
And it works. Test code:
func testWrapper() {
logDefault("WTF: %f", 1.2345)
logDefault("WTF: %#", 1.2345)
logDefaultHack("Hack: %f", 1.2345)
}
Output:
2018-06-20 02:22:56.131875-0500 test[39313:6086331] WTF: 0.000000
2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
"1.2345"
)
2018-06-20 02:22:56.132807-0500 test[39313:6086331] Hack: 1.234500
Would I recommend this solution? No. Hell no. The internals of os_log are an implementation detail and likely to change in future versions of Swift. So don't rely on them like this. But it's interesting to look under the covers anyway.
One last thing. Why doesn't the compiler complain about converting Array<CVarArg> to CVarArg? And why does Kamran's suggestion (of using %#) work?
It turns out these questions have the same answer: it's because Array is “bridgeable” to an Objective-C object. Specifically:
Foundation (on Apple platforms) makes Array conform to the _ObjectiveCBridgeable protocol. It implements bridging of Array to Objective-C by returning an NSArray.
Foundation also makes Array conform to the CVarArg protocol.
The withVaList function asks each CVarArg to convert itself to its _cVarArgEncoding.
The default implementation of _cVarArgEncoding, for a type that conforms to both _ObjectiveCBridgeable and CVarArg, returns the bridging Objective-C object.
The conformance of Array to CVarArg means the compiler won't complain about (silently) converting an Array<CVarArg> to a CVarArg and sticking it into another Array<CVarArg>.
This silent conversion is probably often an error (as it was in your case), so it would be reasonable for the compiler to warn about it, and allow you to silence the warning with an explicit cast (e.g. args as CVarArg). You could file a bug report at https://bugs.swift.org if you want.
As mentioned in my comment to Rob Mayoff's answer above, for anybody experiencing the same kind of issue with os_signpost(), here is a wrapper class I made around it:
import Foundation
import os
import _SwiftOSOverlayShims
public final class Signpost {
private final let log: OSLog
public init(log: OSLog) {
self.log = log
}
public final func begin(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
if #available(iOS 12.0, *) {
signpost(.begin, dso: dso, name: name, idObject: idObject)
}
}
public final func begin(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
if #available(iOS 12.0, *) {
signpost(.begin, dso: dso, name: name, idObject: idObject, format, arguments)
}
}
public final func event(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
if #available(iOS 12.0, *) {
signpost(.event, dso: dso, name: name, idObject: idObject)
}
}
public final func event(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
if #available(iOS 12.0, *) {
signpost(.event, dso: dso, name: name, idObject: idObject, format, arguments)
}
}
public final func end(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
if #available(iOS 12.0, *) {
signpost(.end, dso: dso, name: name, idObject: idObject)
}
}
public final func end(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
if #available(iOS 12.0, *) {
signpost(.end, dso: dso, name: name, idObject: idObject, format, arguments)
}
}
#available(iOS 12.0, *)
private final func signpost(_ type: OSSignpostType, dso: UnsafeRawPointer = #dsohandle, name: StaticString, idObject: AnyObject? = nil) {
guard log.signpostsEnabled else { return }
let signpostID = getSignpostId(forObject: idObject)
os_signpost(type, dso: dso, log: log, name: name, signpostID: signpostID)
}
#available(iOS 12.0, *)
private final func signpost(
_ type: OSSignpostType,
dso: UnsafeRawPointer,
name: StaticString,
idObject: AnyObject? = nil,
_ format: StaticString,
_ arguments: [CVarArg])
{
// This crazy mess is because [CVarArg] gets treated as a single CVarArg and repassing a CVarArg... actually passes a [CVarArg]
// This was copied from the publicly available Swift source code at https://github.com/apple/swift/blob/master/stdlib/public/Darwin/os/os_signpost.swift#L40
// THIS IS A HACK
guard log.signpostsEnabled else { return }
let signpostID = getSignpostId(forObject: idObject)
guard signpostID != .invalid && signpostID != .null else { return }
let ra = _swift_os_log_return_address()
name.withUTF8Buffer { (nameBuf: UnsafeBufferPointer<UInt8>) in
// Since dladdr is in libc, it is safe to unsafeBitCast
// the cstring argument type.
nameBuf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: nameBuf.count) { nameStr in
format.withUTF8Buffer { (formatBuf: UnsafeBufferPointer<UInt8>) in
// Since dladdr is in libc, it is safe to unsafeBitCast
// the cstring argument type.
formatBuf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: formatBuf.count) { formatStr in
withVaList(arguments) { valist in
_swift_os_signpost_with_format(dso, ra, log, type, nameStr, signpostID.rawValue, formatStr, valist)
}
}
}
}
}
}
#available(iOS 12.0, *)
private final func getSignpostId(forObject idObject: AnyObject?) -> OSSignpostID {
if let idObject = idObject {
return OSSignpostID(log: log, object: idObject)
}
return .exclusive
}
}
I needed a wrapper for os_log that could log errors to crashlytics in case the type was of .error. My project is iOS 13 so I needed to to use the older os_log syntax. As Rob Mayoff mentioned there is an issue with implicit conversion from CVarArg... to Array<CVArg> causing issues when passed to the os_log method.
For me creating a simple switch on the count of the the args and doing separate calls to os_log for each count, taking out every argument from the array and passing it to os_log separately fixed the issue. The code is verbose, but it does the job for me.
func log(_ message: StaticString, log: OSLog = .default, type: OSLogType = .default, _ args: CVarArg...) {
switch args.count {
case 0:
os_log(message, log: log, type: type)
case 1:
os_log(message, log: log, type: type, args[0])
case 2:
os_log(message, log: log, type: type, args[0], args[1])
case 3:
os_log(message, log: log, type: type, args[0], args[1], args[2])
case 4:
os_log(message, log: log, type: type, args[0], args[1], args[2], args[3])
case 5:
os_log(message, log: log, type: type, args[0], args[1], args[2], args[3], args[4])
default:
assertionFailure("Currently only up to five arguments are supported")
}
// log error to crashlytics
}
I am experimenting with a piece of code and I need some assistance. I am creating a file that is a swift file that contains a class and a variable. I am able to successfully create and read the file. Now, is it possible for my to use this swift file and access its variable (v, in this case)?
func writeF() {
let file = "Sample.swift"
let text = "import Foundation \n" +
"public class Sample { \n" +
" let v: Int = 0 \n" +
"}"
if let dir = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first {
let path = NSURL(fileURLWithPath: dir).URLByAppendingPathComponent(file)
//writing
do {
try text.writeToURL(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch {print("error writing file")}
//reading
do {
let text2 = try NSString(contentsOfURL: path, encoding: NSUTF8StringEncoding)
print(text2)
}
catch {
print("error reading file")
}
}
}
You can't add code on runtime. When your code is compiled, there are no *.swift files left, that can be read by humans. After compiling, your code is basically 0 and 1 only.
As FelixSFD said in their answer, you cannot dynamically build a Swift file and compile it at runtime on the device, at least not in the normal Sandboxed environment. If you have a Jailbroken device, you can build and install the Swift open source runtime and compile programs on-the-fly that way.
As an alternative, you could look into the JavaScriptCore framework to build and run dynamic JavaScript code, and bridge it into your app.
Here's a quick example of passing an object to JavaScript and returning the same object with a class mapping:
import JavaScriptCore
let js = "function test(input) { return input }"
class TestClass: NSObject {
var name: String
init(name: String) {
self.name = name
}
}
let context = JSContext()
context.evaluateScript(js)
let testFunc = context.objectForKeyedSubscript("test")
let result = testFunc.callWithArguments([TestClass(name: "test")])
result.toDictionary()
let testObj = result.toObjectOfClass(TestClass.self) as? TestClass
testObj?.name // "test"
I would like to globally ignore all println() calls in my Swift code if I am not in a Debug build. I can't find any robust step by step instructions for this and would appreciate guidance. is there a way to do this globally, or do I need to surround every println() with #IF DEBUG/#ENDIF statements?
The simplest way is to put your own global function in front of Swift's println:
func println(object: Any) {
Swift.println(object)
}
When it's time to stop logging, just comment out the body of that function:
func println(object: Any) {
// Swift.println(object)
}
Or you can make it automatic by using a conditional:
func println(object: Any) {
#if DEBUG
Swift.println(object)
#endif
}
EDIT In Swift 2.0 println is changed to print. Unfortunately it now has a variadic first parameter; this is cool, but it means you can't easily override it because Swift has no "splat" operator so you can't pass a variadic in code (it can only be created literally). But you can make a reduced version that works if, as will usually be the case, you are printing just one value:
func print(items: Any..., separator: String = " ", terminator: String = "\n") {
Swift.print(items[0], separator:separator, terminator: terminator)
}
In Swift 3, you need to suppress the external label of the first parameter:
func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
Swift.print(items[0], separator:separator, terminator: terminator)
}
Updated for Swift 4.x:
With Swift 2.0/3.0 and Xcode 7/8 now out of beta, there have been some changes to how you disable the print function in release builds.
There are some important points mentioned by #matt and #Nate Birkholz above that are still valid.
The println() function has been replaced by print()
To use the #if DEBUG macro then you have to define the "Swift Compiler - Custom Flags -Other Flags" to contain the value -D DEBUG
I would recommend overriding the Swift.print() function in the global scope so that you can use the print() function as normal in your code, but it will remove output for non-debug builds. Here is a function signature that you can add at the global scope to do this in Swift 2.0/3.0:
func print(items: Any..., separator: String = " ", terminator: String = "\n") {
#if DEBUG
var idx = items.startIndex
let endIdx = items.endIndex
repeat {
Swift.print(items[idx], separator: separator, terminator: idx == (endIdx - 1) ? terminator : separator)
idx += 1
}
while idx < endIdx
#endif
}
Note: We have set the default separator to be a space here, and the default terminator to be a newline. You can configure this differently in your project if you would like.
Hope this helps.
Update:
It is usually preferable to put this function at the global scope, so that it sits in front of Swift's print function. I find that the best way to organize this is to add a utility file to your project (like DebugOptions.Swift) where you can place this function at the global scope.
As of Swift 3 the ++ operator will be deprecated. I have updated the snippet above to reflect this change.
The problem with all these approaches, including mine, is that they do not remove the overhead of evaluating the print arguments. No matter which of them you use, this is going to be expensive:
print(myExpensiveFunction())
The only decent solution is to wrap the actual print call in conditional compilation (let's assume that DEBUG is defined only for debug builds):
#if DEBUG
print(myExpensiveFunction())
#endif
That, and only that, prevents myExpensiveFunction from being called in a release build.
However, you can push back evaluation one level by using autoclosure. Thus, you could rewrite my solution (this is Swift 3) like this:
func print(_ item: #autoclosure () -> Any, separator: String = " ", terminator: String = "\n") {
#if DEBUG
Swift.print(item(), separator: separator, terminator: terminator)
#endif
}
This solves the problem just in the case where you are printing just one thing, which is usually true. That's because item() is not called in release mode. print(myExpensiveFunction()) thus ceases to be expensive, because the call is wrapped in a closure without being evaluated, and in release mode, it won't be evaluated at all.
Easy Answer - Xcode 14, Swift 5
Create a new file in your project and paste in this code:
import Foundation
func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
#if DEBUG
Swift.print(items, separator: separator, terminator: terminator)
#endif
}
This function signature matches the default Swift print so it overwrites the function throughout your project. If needed you can still access the original by using Swift.print().
Once you've added the code above, keep using print() the as usual and it will only print in debug builds.
As noted, i am a student and need things defined a little more clearly to follow along. After lots of research, the sequence I needed to follow is:
Click on the project name at the top of the File Navigator at the left of the Xcode project window. This is line that has the name of the project, how many build targets there are, and the iOS SDK version.
Choose the Build Settings tab and scroll down to the "Swift Compiler - Custom Flags" section near the bottom. Click the Down Arrow next to Other Flags to expand the section.
Click on the Debug line to select it. Place your mouse cursor over the right side of the line and double-click. A list view will appear. Click the + button at the lower left of the list view to add a value. A text field will become active.
In the text field, enter the text -D DEBUG and press Return to commit the line.
Add a new Swift file to your project. You are going to want to make a custom class for the file, so enter text along the lines of the following:
class Log {
var intFor : Int
init() {
intFor = 42
}
func DLog(message: String, function: String = __FUNCTION__) {
#if DEBUG
println("\(function): \(message)")
#endif
}
}
I was having trouble getting the class to be accepted by Xcode today, so the init may be a bit more heavyweight than necessary.
Now you will need to reference your custom class in any class in which you intend to use the new custom function in place of println() Add this as a property in every applicable class:
let logFor = Log()
Now you can replace any instances of println() with logFor.DLog(). The output also includes the name of the function in which the line was called.
Note that inside class functions I couldn't call the function unless I made a copy of the function as a class function in that class, and println() is also a bit more flexible with the input, so I couldn't use this in every instance in my code.
Here is a function that I use, which works perfectly in Swift 3:
func gLog<T>( _ object: #autoclosure() -> T, _ file: String = #file, _ function: String = #function, _ line: Int = #line)
{
#if DEBUG
let value = object()
let stringRepresentation: String
if let value = value as? CustomDebugStringConvertible
{
stringRepresentation = value.debugDescription
}
else if let value = value as? CustomStringConvertible
{
stringRepresentation = value.description
}
else
{
fatalError("gLog only works for values that conform to CustomDebugStringConvertible or CustomStringConvertible")
}
let fileURL = NSURL(string: file)?.lastPathComponent ?? "Unknown file"
let queue = Thread.isMainThread ? "UI" : "BG"
let gFormatter = DateFormatter()
gFormatter.dateFormat = "HH:mm:ss:SSS"
let timestamp = gFormatter.string(from: Date())
print("✅ \(timestamp) {\(queue)} \(fileURL) > \(function)[\(line)]: " + stringRepresentation + "\n")
#endif
}
Here is an example of the output it generates:
Explanation:
the green checkmark is used to enable you to quickly see your print (gLog) messages in the console, where they can sometimes get lost in a sea of other messages
the time/date stamp
the thread it is being run on -- in my case it is either the MainThread (which I call UI), or not the MainThread (which I call BG, for background thread)
the name of the file that the gLog message resides in
the function within the file that the gLog message resides in
the line number of the gLog message
the actual gLog message you would like to print out
Hope this is useful to someone else!
Tested with Swift 2.1 & Xcode 7.1.1
There's an easy way to exclude all print statements from release versions, once you know that empty functions are removed by the Swift compiler.
Side note : In the era of Objective-C, there was a pre-parser which could be used to remove NSLog statements before the compiler kicked in, like described in my answer here. But since Swift no longer has a pre-parser this approach is no longer valid.
Here's what I use today as an advanced and easily configurable log function, without ever having to worry about removing it in release builds. Also by setting different compiler flags, you can tweak the information that is logged as needed.
You can tweak the function as needed, any suggestion to improve it is welcome!
// Gobal log() function
//
// note that empty functions are removed by the Swift compiler -> use #if $endif to enclose all the code inside the log()
// these log() statements therefore do not need to be removed in the release build !
//
// to enable logging
//
// Project -> Build Settings -> Swift Compiler - Custom flags -> Other Swift flags -> Debug
// add one of these 3 possible combinations :
//
// -D kLOG_ENABLE
// -D kLOG_ENABLE -D kLOG_DETAILS
// -D kLOG_ENABLE -D kLOG_DETAILS -D kLOG_THREADS
//
// you can just call log() anywhere in the code, or add a message like log("hello")
//
func log(message: String = "", filePath: String = #file, line: Int = #line, function: String = #function) {
#if kLOG_ENABLE
#if kLOG_DETAILS
var threadName = ""
#if kLOG_THREADS
threadName = NSThread.currentThread().isMainThread ? "MAIN THREAD" : (NSThread.currentThread().name ?? "UNKNOWN THREAD")
threadName = "[" + threadName + "] "
#endif
let fileName = NSURL(fileURLWithPath: filePath).URLByDeletingPathExtension?.lastPathComponent ?? "???"
var msg = ""
if message != "" {
msg = " - \(message)"
}
NSLog("-- " + threadName + fileName + "(\(line))" + " -> " + function + msg)
#else
NSLog(message)
#endif
#endif
}
Here's where you set the compiler flags :
An example output with all flags on looks like this :
2016-01-13 23:48:38.026 FoodTracker[48735:4147607] -- [MAIN THREAD] ViewController(19) -> viewDidLoad() - hello
The code with the log() looks like this :
override func viewDidLoad() { log("hello")
super.viewDidLoad()
// Handle the text field's user input through delegate callbacks
nameTextField.delegate = self
}
Even simpler, after making sure -D DEBUG is set for the OTHER_SWIFT_FLAGS Debug build settings:
#if !DEBUG
func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { }
#endif
XCode 8 introduced a few new build settings.
In particular one referred to Active Compilation Conditions does in a similar way what Other Flags settings did.
"Active Compilation Conditions" is a new build setting for passing conditional compilation flags to the Swift compiler.
As per XCode 8 (tested in 8.3.2) you will get this by default:
So without any config you can write the following:
#if DEBUG
print("⚠️ Something weird happened")
#endif
I strongly recommend you that if you use this approach extensively create a class/struct/function that wraps this logging logic. You may want to extend this further down the road.
Varun Naharia has the better solution so far. I would combine his answer with Rivera's ...
create a -D DEBUG flag on the compiler directives, build settings.
then add this code:
#if !DEBUG
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
}
#endif
This code will convert every print into nothing for release.
Swift 4
Xcode 10.0
maybe you could use this
func dPrint(_ message: #autoclosure () -> Any) {
#if DEBUG
print(message())
#endif
}
The reason of using #autoclosure is that if you pass a function as the message parameter, the function will be called only in debug mode, it will cause a performance hit.
unlike the Swift.print(_ items: Any..., separator: String = default, terminator: String = default) function, my solution has only one parameter, because in most cases, we don't pass multiple parameters as the print function only shows information in console, we can just convert the parameters to String: "\(param1)"+"\(param2)", right?
hope u like my solution
for my solution i make it simple
import UIKit
class DLog: NSObject {
init(title:String, log:Any) {
#if DEBUG
print(title, log)
#endif
}
}
then to show it just call
_ = DLog(title:"any title", log:Any)
You can also use a breakpoint, set it to continue after evaluation, and write the print message in the breakpoint!
You could define debug_println whose contents would be roughly:
#if DEBUG
println()
#endif
My Solution is use this code in AppDelegate before class
// Disable console log in live app
#if !arch(x86_64) && !arch(i386)
public func debugPrint(items: Any..., separator: String = " ", terminator: String = "\n") {
}
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
}
#endif
class AppDelegate: UIResponder, UIApplicationDelegate {
// App Delegate Code
}
Even simpler: take advantage of the fact that asserts are removed from release builds and only from there call the print. This removes all log calls (yes, even the calls to Log.da) as they are empty when building for release.
But I also heard that prints are removed for release builds, but not been able to find it in writing. So for now, I am using something like this Log below. I have a more meaty version on GitHub with emojis (for readability) and log topics (for consistency):
https://github.com/Gatada/JBits/blob/master/Project/Utility/Log.swift
public enum Log {
/// A date formatter used to create the timestamp in the log.
///
/// This formatter is only created if it is actually used, reducing the
/// overhead to zero.
static var formatter: DateFormatter?
// MARK: - API
/// Call to print message in debug area.
///
/// Asserts are removed in release builds, which make
/// the function body empty, which caused all calls to
/// be removed as well.
///
/// Result is zero overhead for release builds.
public static func da(_ message: String) {
assert(debugAreaPrint(message))
}
// MARK: - Helpers
/// The function that actually does the printing. It returns `true` to
/// prevent the assert from kicking in on debug builds.
private static func debugAreaPrint(_ message: String) -> Bool {
print("\(timestamp) - \(message)")
return true
}
/// Creates a timestamp used as part of the temporary logging in the debug area.
static private var timestamp: String {
if formatter == nil {
formatter = DateFormatter()
formatter!.dateFormat = "HH:mm:ss.SSS"
}
let date = Date()
return formatter!.string(from: date)
}
}
In code:
Log.da("This is only handled in a debug build.")
Seen in the Xcode debug area only when running a debug build:
13:36:15.047 - This is only handled in a debug build.
My Project was developed in Objective C, but from the past year I have started merging new code in Swift, So In Swift below solution worked for me, I have added that code in My Swift constant file :
func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
#if DEBUG
items.forEach {
Swift.print($0, separator: separator, terminator: terminator)
}
#endif
}
This works for me (add this as a global function in the project)
func print(_ items: Any...) {
#if DEBUG
Swift.print(items[0])
#endif
}