swift 3 issue with CVarArg being passed multiple times
You cannot pass a variable argument list to another function, you
have to pass a CVaListPointer
instead. Also withVaList
should
be used instead of getVaList
:
class StringResourceUtility {
static func Localizer(tableName: String?) -> (_ key: String, _ params: CVaListPointer) -> String {
return { (key: String, params: CVaListPointer) in
let content = NSLocalizedString(key, tableName: tableName, comment: "")
return NSString(format: content, arguments: params) as String
}
}
}
func localizationHelper(tableName: String, key: String, params: CVarArg...) -> String {
let t = StringResourceUtility.Localizer(tableName: tableName)
return withVaList(params) { t(key, $0) }
}
Example:
let s = localizationHelper(tableName: "table", key: "%@ %@", params: "Wells", "Fargo")
print(s) // Wells Fargo
How to properly use VarArgs for localizing strings?
You cannot pass a variable argument list to another function, you
have to pass a CVaListPointer
instead (the Swift equivalent
of va_list
in C):
public extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
func localized(args: CVarArg...) -> String {
return withVaList(args) {
NSString(format: self.localized, locale: Locale.current, arguments: $0) as String
}
}
}
Since NSString.localizedStringWithFormat
has no variant taking aVAListPointer
, the equivalent NSString(format:, locale:, arguments:)
with the current locale is used.
Even simpler (attribution goes to @OOPer): UseString.init(format:locale:arguments:)
which takes a [CVarArg]
argument:
func localized(args: CVarArg...) -> String {
return String(format: self.localized, locale: Locale.current, arguments: args)
}
Now
"grant_gps_access".localized(args: "MyApp")
should work as expected, assuming that the strings file contains
the entry
"grant_gps_access" = "Please grant %@ GPS access";
Swift function with args... pass to another function with args
Similar as in (Objective-)C, you cannot pass a variable argument list
directly to another function. You have to create a CVaListPointer
(the Swift equivalent of va_list
in C) and call a function which
takes a CVaListPointer
parameter.
So this could be what you are looking for:
extension String {
func getLocalizeWithParams(args : CVarArgType...) -> String {
return withVaList(args) {
NSString(format: self, locale: NSLocale.currentLocale(), arguments: $0)
} as String
}
}
withVaList()
creates a CVaListPointer
from the given argument list
and calls the closure with this pointer as argument.
Example (from the NSString
documentation):
let msg = "%@: %f\n".getLocalizeWithParams("Cost", 1234.56)
print(msg)
Output for US locale:
Cost: 1,234.560000
Output for German locale:
Cost: 1.234,560000
Update: As of Swift 3/4/5 one can pass the arguments to
String(format: String, locale: Locale?, arguments: [CVarArg])
directly:
extension String {
func getLocalizeWithParams(_ args : CVarArg...) -> String {
return String(format: self, locale: .current, arguments: args)
}
}
Format string with variadic arguments
String
has two similar initializers:
init(format: String, _ arguments: CVarArg...)
init(format: String, arguments: [CVarArg])
The first one takes a varying number of arguments, the second one
an array with all arguments:
print(String(format: "x=%d, y=%d", 1, 2))
print(String(format: "x=%d, y=%d", arguments: [1, 2]))
In your localized
method, args: CVarArg...
is a variadic parameter
and those are made available within the functions body as an array
of the appropriated type, in this case [CVarArg]
.
Therefore it must be passed to String(format: arguments:)
:
func localized(table: String? = nil, bundle: Bundle = .main, args: CVarArg...) -> String {
return String(
format: NSLocalizedString(
self,
tableName: table,
bundle: bundle,
value: self,
comment: ""
),
arguments: args // <--- HERE
)
}
See also "Variadic Parameters" in the "Functions" chapter of the
Swift reference.
Passing variadic arguments as an array in Swift
I think your problem is that tuples do not support variadic arguments. So
(format: "This is %@ %@", param: ["Swift", "language"])
is a tuple of two things: a string and an array and String(format:,_)
sees the array as only one object. To solve your immediate problem, use String(format:,arguments:)
. e.g.
for index in 0...1 {
let pdata = printData[index]
print (String(index+1) + " " + String(format: pdata.format, arguments: pdata.param))
}
Given how easy it is to create a literal array, I'd question the need to use variadic arguments anywhere in Swift.
Passing variadic args in Swift 4 for os_log
Did some more research on this. Turns out that os_log
is actually a C macro. This created all sorts of problems with how it maps in to Swifts variadic args.
However, that macro also captures other debugging info and is probably not safe to wrap up anyways.
String' does not conform to expected type 'CVarArg'
NSLog
takes as the first argument a format string, which is followed
by a list of arguments, which are substituted for the placeholders
in the format string (compare String Format Specifiers).
On Apple platforms, you can print a String
using the %@
format:
let fileName = "the file"
NSLog("File not found: %@", fileName)
However, this does not work on Linux platforms (such as Vapor).
Here you have to convert the Swift string to a C string in order to pass
it as an argument to NSLog (and use the %s
format for C strings):
let fileName = "the file"
fileName.withCString {
NSLog("File not found: %s", $0)
}
localizeWithFormat and variadic arguments in Swift
This should be pretty simple just change your parameters as follow:
extension String {
func localizeWithFormat(name:String,age:Int, comment:String = "") -> String {
return String.localizedStringWithFormat( NSLocalizedString(self, comment: comment), name, age)
}
}
"My name is %@. I am %d years old".localizeWithFormat("John", age: 30) // "My name is John. I am 30 years old"
init(format:locale:arguments:)
extension String {
func localizeWithFormat(args: CVarArgType...) -> String {
return String(format: self, locale: nil, arguments: args)
}
func localizeWithFormat(local:NSLocale?, args: CVarArgType...) -> String {
return String(format: self, locale: local, arguments: args)
}
}
let myTest1 = "My name is %@. I am %d years old".localizeWithFormat(NSLocale.currentLocale(), args: "John",30)
let myTest2 = "My name is %@. I am %d years old".localizeWithFormat("John",30)
Why does wrapping os_log() cause doubles to not be logged correctly?
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) andvprintf
(takes ava_list
, C's equivalent ofArray<CVarArg>
)NSLog
(variadic) andNSLogv
(takes ava_list
)-[NSString initWithFormat:]
(variadic) and-[NSString WithFormat:arguments:]
(takes ava_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, orTake 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 toos_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 ofArray
to Objective-C by returning anNSArray
.Foundation also makes
Array
conform to theCVarArg
protocol.The
withVaList
function asks eachCVarArg
to convert itself to its_cVarArgEncoding
.The default implementation of
_cVarArgEncoding
, for a type that conforms to both_ObjectiveCBridgeable
andCVarArg
, returns the bridging Objective-C object.The conformance of
Array
toCVarArg
means the compiler won't complain about (silently) converting anArray<CVarArg>
to aCVarArg
and sticking it into anotherArray<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.
Related Topics
Swift. Declaring Private Functions in Internal Protocol
Same Class Extension in Two Different Modules
Testing If a Decimal Is a Whole Number in Swift
Swift Nsusernotification Doesn't Show While App Is Active
iOS Firebase: Firauthuidelegate.Authui Not Being Called
Sort Dictionary Keys by Value, Then by Key
Disable Https Get Certificate Check in Swift 5
How to Rotate an Object Around Only One Axis in Realitykit
JSONencoder Won't Allow Type Encoded to Primitive Value
Swift Generics Error: Cannot Convert Value of Type 'Type<T>' to Expected Argument Type 'Type<_>'
Changing Texteditor Background Color in Swiftui for MACos
Openinmapswithlaunchoptions Not Working
How to Add a Left Bar Button Without Overriding the Natural Back Button
How to Convert Dispatchtimeinterval to Nstimeinterval (Or Double)
MAC App to Switch Between /Etc/Hosts Files, How to Allow Access