How do I deal with the String.Encoding initializer never failing? - swift

if let encoding = String.Encoding(rawValue: 999) {
// ...
}
Produces a compiler error saying "Initializer for conditional binding must have Optional type, not 'String.Encoding'" because despite the docs saying the String.Encoding initializer is failable, it is not and will happily create non-existent encodings.
How do I check if the encoding returned by initializer is an actual encoding?
The two ideas I have are
Check the String.Encoding description is not empty. This assumes that supported encodings must have a description
Encode something - e.g. "abc".data(using: encoding) == nil - which assumes that the string "abc" can be encoded by all supported encodings

The documentation is misleading, String.Encoding has a non-failable
public init(rawValue: UInt)
initializer. A list of all valid string encodings is String.availableStringEncodings, so you can use that to check the validity:
let encoding = String.Encoding(rawValue: 999)
print(String.availableStringEncodings.contains(encoding)) // false

You can use CoreFoundation's CFStringIsEncodingAvailable() function to check if the encoding is valid, and also if it can be used on that particular device.
However, as MartiR pointed out, CFStringIsEncodingAvailable needs a CFStringEncoding to work with, so the String.Encoding needs to be converted first to a CF one.
let encoding = String.Encoding(rawValue: 999)
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(encoding.rawValue)
if CFStringIsEncodingAvailable(cfEncoding) {
// ...
}
, or, as MartinR nicely suggested, the result of CFStringConvertNSStringEncodingToEncoding can also be used:
if CFStringConvertNSStringEncodingToEncoding(encoding.rawValue) != kCFStringEncodingInvalidId {
// ...
}

Related

Is there a mapping in the standard library between Swift's String.Encoding and the IANA character set names?

Does a mapping exist in the standard library between the String.Encoding enumeration and the standard IANA character set names, e.g., "UTF-8" for String.Encoding.utf8? I was not able to find one.
I'm aware that Foundation's CFStringEncoding can be mapped to IANA character set names, but I could not find a way to go from String.Encoding to CFStringEncoding. CFStringEncoding is just a type alias for UInt32, and the String.Encoding enumeration is backed by a UInt, but unless I've made some simple error, they do not seem to correspond.
The raw value of a String.Encoding is an NSStringEncoding and that can be converted to a CFStringEncoding with CFStringConvertNSStringEncodingToEncoding. The IANA charset name is then determined with CFStringConvertEncodingToIANACharSetName. This function returns an optional CFString which can be toll-free bridged to an optional Swift String.
Example:
let enc = String.Encoding.isoLatin2
let cfEnc = CFStringConvertNSStringEncodingToEncoding(enc.rawValue)
if let ianaName = CFStringConvertEncodingToIANACharSetName(cfEnc) as String? {
print(ianaName) // iso-8859-2
}

Can the conversion of a String to Data with UTF-8 encoding ever fail?

In order to convert a String instance to a Data instance in Swift you can use data(using:allowLossyConversion:), which returns an optional Data instance.
Can the return value of this function ever be nil if the encoding is UTF-8 (String.Encoding.utf8)?
If the return value cannot be nil it would be safe to always force-unwrap such a conversion.
UTF-8 can represent all valid Unicode code points, therefore a conversion
of a Swift string to UTF-8 data cannot fail.
The forced unwrap in
let string = "some string .."
let data = string.data(using: .utf8)!
is safe.
The same would be true for .utf16 or .utf32, but not for
encodings which represent only a restricted character set,
such as .ascii or .isoLatin1.
You can alternatively use the .utf8 view of a string to create UTF-8 data,
avoiding the forced unwrap:
let string = "some string .."
let data = Data(string.utf8)

Optional Chaining in one step?

Why does this work (Example 1):
if let numString:String = Model.selectedLocation?.zip{
let callString:String = String(format:"tel:%#",numString)
//more code here
}
But not this (Example 2):
if let numString:String = String(format:"tel:%#",Model.selectedLocation?.zip){
//more code here
}
In the second example, Xcode throws an error and wants zip to be unwrapped like:
String(format:"tel:%#",(Model.selectedLocation?.zip)!)
but if I do that the app will crash when zip is nil.
QUESTION:
Is there a way to make the second example above work or is it not possible/correct?
Avoid redundant type annotations
Avoid String(format:) unless you need it. It's a method of NSString from the Foundation framework, which has several consequences:
It requires Foundation to be imported.
It implicitly bridges your String to NSString.
It won't work in Swift 3, because bridging was made explicit.
The root issue here is that String(format:) returns String? (since the format string could be invalid). You can avoid this entirely by using Swift's string interpolation:
if let numString = Model.selectedLocation?.zip {
let callString = "tel: \(numString)"
//more code here
}
...or simple concatination:
if let numString = Model.selectedLocation?.zip {
let callString = "tel: " + numString
//more code here
}
Strictly spoken Example 2 is neither optional binding nor optional chaining because String(format...) returns a non-optional String and the format parameter must be non-optional, too.
Example 1 is the correct and recommended syntax to handle the optionals.
Edit: I totally agree with Alexander's answer (except that String(format:) returns String?)
UPDATED
It is because in String(format: " ", ), the arguments must be not-nil, hence the !.
when using if-let check for optionals, statements must return optionals
// Assuming Model.selectionLocation.zip is of String type
if let numberString = Model.selectedLocation?.zip {
let formattedString = String(format:"tel:%#", numberString)
}
or use guard
guard let numberString = Model.selectedLocation?.zip else {
return
}
let numberString = String(format:"tel:%#", numberString)

Mocking a String-class method in Swift

I've got some Swift 2.0 code, for which I'm trying to achieve 100% code coverage. I'm doing some JSON handling, part of which looks like so:
guard let jsonData = jsonText.dataUsingEncoding(NSUTF8StringEncoding) else {
throw ErrorCode.JSONEncodingFailure
}
I don't think there's a real-world case in which any string can't be encoded as UTF-8, but I don't want to open myself up to a crashing error either, so that code must remain. How can I mock the jsonText object to return nil for dataUsingEncoding()?
The closest I've come is to subclass NSString like so:
public class BadString: NSString {
public override var length: Int {
get {
return 5
}
}
public override func characterAtIndex(index: Int) -> unichar {
return 0
}
public override func dataUsingEncoding(encoding: NSStringEncoding) -> NSData? {
return nil
}
}
Here's the problem, though. In order for my mock implementation to be used, I have to define jsonText (a function parameter) as an NSString, rather than String, which feels wrong for an all-Swift codebase. With it defined as a Swift String, I have to cast my BadString to that type, and it uses the String implementation instead of my own.
Is there another (clean) way to achieve this?
You will be hard-pressed to find a string that cannot be encoded using UTF-8! As long as you know that is the encoding you will be using, I would suggest that you not worry about testing the "encoding failure" case.
However, if you still desire to test it then I recommend making one of the following changes in order to allow you to do so:
(1) change the way you are thinking about the 'failure': if you know that the string you are encoding will always be non-empty, then broaden the guard to also require that the encoded data has length > 0, e.g. =>
guard let jsonData = jsonText.dataUsingEncoding(NSUTF8StringEncoding)
where jsonData.length > 0 else {
throw ErrorCode.JSONEncodingFailure
}
...using this idea, you can now use an empty string for jsonText and trigger this code path (assuming that an empty string would also satisfy your definition of 'failure' here)
(2) store your string encoding value in a variable (let's call it stringEncoding) that you can access during your test setup, and then test this using incompatible values for jsonText and stringEncoding, e.g. =>
var jsonText = "🙈"
let stringEncoding = NSASCIIStringEncoding
...I guarantee that jsonText.dataUsingEncoding(stringEncoding) will return nil in this case :)
Happy Testing! I hope this helps!

Convert String to UnsafeMutablePointer<char_t> in Swift

I'm working with a third party c API I'm trying to call one of the functions with a simple string. Something like this:
some_c_func("aString");
I get a build error:
Type 'UnsafeMutablePointer<char_t>' does not conform to protocol 'StringLiteralConvertible'
I've seen some suggestions to use utf8 on String or similar conversions, which gets nearly there, but with the following error:
some_c_func("aString".cStringUsingEncoding(NSUTF8StringEncoding));
'UnsafePointer<Int8>' is not convertible to 'UnsafeMutablePointer<char_t>'
How can I create an UnsafeMutablePointer?
It all depends on what char_t is.
If char_t converts to Int8 then the following will work.
if let cString = str.cStringUsingEncoding(NSUTF8StringEncoding) {
some_c_func(strdup(cString))
}
This can be collapsed to
some_c_func(strdup(str.cStringUsingEncoding(NSUTF8StringEncoding)!))
WARNING! This second method will cause a crash if func cStringUsingEncoding(_:) returns nil.
Updating for Swift 3, and to fix memory leak
If the C string is only needed in a local scope, then no strdup() is needed.
guard let cString = str.cString(using: .utf8) else {
return
}
some_c_func(cString)
cString will have the same memory lifecycle as str (well similar at least).
If the C string needs to live outside the local scope, then you will need a copy. That copy will need to be freed.
guard let interimString = str.cString(using: .utf8), let cString = strdup(interimString) else {
return
}
some_c_func(cString)
//…
free(cString)
it may be simpler than that - many C APIs pass strings around as char * types, and swift treats these as unsafe.
try updating the C API (good) or hack it's header files (bad) to declare these as const char * instead.
in my experience this allows you to pass standard swift String types directly to the C API.
apparently a constant is required, in order to conform to the protocol.
I haven't tried passing strings like that, but I have a C function that I call from Swift, that takes a lot more parameters than shown here, among which is a reference to a Swift C typecast buffer to hold an error string. The compiler doesn't complain and the function call works. Hopefully this will steer you closer to the answer and you can provide an update with the final answer or someone else can.
var err = [CChar](count: 256, repeatedValue: 0)
var rv = somefunc((UnsafeMutablePointer<Int8>)(err))
if (rv < 0) {
println("Error \(err)")
return
}