iOS Swift: How to convert String to StaticString? - ios

func generateDescription(_ prefix: String) {
return (prefix + " Some Text Here")
}
let str: String = generateDescription("Some prefix text here")
How do I cast or generate a StaticString from this runtime generated string? I need to pass str to a method from a library that has a StaticString parameter (I have no control over the library).
I am thinking of something like this:
let staticStr = StaticString(str)
But this is not the correct way.
Thanks!
(Btw this is not a duplicate to this question: Convert String to StaticString)

I'm afraid it is a duplicate of Convert String to StaticString - there isn't a mechanism to convert to a static string during the execution of your program, as the whole purpose of static string is to have a string that's fully defined at compile time.
If you want to use a static string, you need to define it entirely up front in your code as a StaticString:
let staticString = "Hello, World!"

Related

is there a good practice to use a localization that is then complemented with String interpolation?

I have a translation whose phrase is static but then it is complemented with a piece of information that varies according to the user. It would be something like this:
"" The person resides in "+ (data that varies)".
"The person resides in" would be the static text for Localization.
Data that varies would be the interpolation of a string.
I know there is %d for numbers in Localization, does this apply to a string? Thanks!
If the variable data doesn't depend on current locale you could just use the regular %# for string arguments. Let's say you have this line in the .strings file:
"stringKey" = "The person resides in %#";
And then you can localize it like this:
String(format: NSLocalizedString("stringKey", comment: ""), data)
You may want to wrap it into an extension:
extension String {
func localized(_ arg: String) -> String {
String(format: NSLocalizedString(self, comment: ""), arg)
}
}
In order to keep your code cleaner:
"stringKey".localized(data)
Or just use the R.swift library which does it for yourself, so you would have it like this:
R.string.localizable.stringKey(data)

Is there a way to make `genstrings` work with `LocalizedStringKey`?

Is there a way get Apple's genstrings command line tool to recognize localizable string keys defined from SwiftUI's LocalizedStringKey initializer?
For this input file (testing-genstrings.swift): ...
import UIKit
import SwiftUI
enum L10n {
static let test0 = NSLocalizedString("TEST0", comment: "")
static let test1 = LocalizedStringKey("TEST1")
static func test2(_ parameter: String) -> LocalizedStringKey {
return LocalizedStringKey("TEST2_\(parameter)")
}
static func test3(_ parameter: String) -> String {
return NSLocalizedString("TEST3_\(parameter)", comment: "")
}
static func test4(_ parameter: String) -> String {
return String.localizedStringWithFormat(NSLocalizedString("TEST4", comment: ""), parameter)
}
}
let test5 = "TEST5"
let test6 = "TEST6"
let test7 = "TEST7"
struct TestView: View {
var body: some View {
VStack {
Text(L10n.test0)
Text(L10n.test1)
Text(L10n.test2("foo"))
Text(L10n.test3("bar"))
Text(test5)
Text(LocalizedStringKey(test6))
Text(NSLocalizedString(test7, ""))
Text("TEST8")
Text("TEST9_\("baz")")
}
}
}
...genstrings generates this output:
$ genstrings -SwiftUI -s LocalizedStringKey testing-genstrings.swift && iconv -c -f utf-16 -t utf-8 Localizable.strings
genstrings: error: bad entry in file testing-genstrings.swift (line = 9): Argument is not a literal string.
genstrings: error: bad entry in file testing-genstrings.swift (line = 11): Argument is not a literal string.
genstrings: error: bad entry in file testing-genstrings.swift (line = 12): Argument is not a literal string.
genstrings: error: bad entry in file testing-genstrings.swift (line = 36): Argument is not a literal string.
genstrings: error: bad entry in file testing-genstrings.swift (line = 37): Argument is not a literal string.
genstrings: error: bad entry in file testing-genstrings.swift (line = 37): Argument is not a literal string.
/* No comment provided by engineer. */
"bar" = "bar";
/* No comment provided by engineer. */
"foo" = "foo";
/* No comment provided by engineer. */
"TEST0" = "TEST0";
/* No comment provided by engineer. */
"TEST3_\(parameter)" = "TEST3_\(parameter)";
/* No comment provided by engineer. */
"TEST4" = "TEST4";
/* No comment provided by engineer. */
"TEST8" = "TEST8";
/* No comment provided by engineer. */
"TEST9_%#" = "TEST9_%#";
You can see that it recognizes the keys defined via NSLocalizedString and Text's initializer Text() initializer that uses ExpressibleByStringInterpolation (TEST9_%# in the example), but fails on all keys defined using LocalizedStringKey.
genstrings is relatively naive. It is looking for a function with two parameters, the first unnamed, the second named "comment".
If you added the following extension:
public extension LocalizedStringKey {
init(_ value: String, comment: String) {
self.init(value)
}
}
and always used that, you'd be able to use LocalizedStringKey by passing -s LocalizedStringKey to genstrings.
Keep in mind that if you declared LocalizedStringKey as a return type or a variable, that would give a genstrings error, too. So you'd need a separate typealias LocalizedStringKeyResult = LocalizedStringKey that you use when reference LocalizedStringKey but don't want genstrings to complain.
And, of course, you wouldn't get the interpolation you want because genstrings applies that only to Text.
The real answer is... don't use LocalizedStringKey. Use Text when you can (to get interpolation). Use NSLocalizedString when you can't.

Cannot Access .size withAttributes: from String

I have a String extension:
func iconName(identifier: String) -> String
{
return self.library()[identifier]
}
and I'm doing this inside a UIImage extension:
let icon = String.iconName("myiconname")
var iconSize = icon.size(withAttributes: someAttributes)
I get an error:
Value of type '(String) -> String' has no member 'size'
I have tried to switch this around many different ways and I get the same (or similar) error each time.
The thing is it does have a member size. I can do this with no error:
var myString = "testing"
myString.size(withAttributes: attributes)
Anyone know what is going on with the Value of type '(String) -> String'?
The method
size(withAttributes:)
is an instance method inside NSString not String
see here in Docs
So , you can change signature of the method to
func iconName(identifier: String) -> NSString {....}
//
You should not use
let icon = String.iconName("myiconname") // here String is a generic type
as extension is designated to be used with an object like
let icon = "someStringValue".iconName("myiconname")
I would not use a String extension here because the return is of type (String) -> String which is not the same as type String and the extension will not allow you to cast its type. I assume you chose an extension to make it globally accessible but there are other options. You can make the function itself global (which I don't find attractive), you can make the function static within a regular class or struct, you can make the struct static, you can do the singleton pattern, you can store the function in the app delegate and call it there--you have a list of options to make properties and methods "global".
Because all that this method does is take a String and convert it into a modified String, no need for singleton, and so I would most likely create a global class with static properties.
class Globals {
static func iconName(_ identifier: String) -> String {
return "slickDaddy"
}
}
let icon = Globals.iconName("myIconName")
var iconSize = icon.size(withAttributes: [NSAttributedStringKey.backgroundColor: UIColor.blue])
print(iconSize) // prints (58.01953125, 13.8)
You can call this method from anywhere in the app. Certainly consider all of the other iterations I mentioned above and use the one that suits you best. Furthermore, even if you could do this in an extension, I personally wouldn't. This is a business method used in very special cases, not a method that modifies the style of a String that is used all over the app (which is what I think extensions should be primarily used for).

How to create a string with format? Not working on string

Lots of details on this throughout SO and online. Unfortunately, I cannot get any of it to work to with my string. I have this string
https://maps.googleapis.com/maps/api/directions/json?origin=%#,%#&destination=%#,%#&sensor=false&units=metric&mode=driving
And all I'm trying to do is insert the necessary values into the string by doing
let url = String(format: Constants.GoogleDirectionsUrl, road.FromCoordinates.Latitude, road.FromCoordinates.Longitude, road.ToCoordinates.Latitude, road.ToCoordinates.Longitude)
The string though always prints out as
https://maps.googleapis.com/maps/api/directions/json?origin=(null),(null)&destination=(null),(null)&sensor=false&units=metric&mode=driving
Although all the coordinates are valid. When I do string interpolation I get the correct value to show up
print("coord -- \(road.FromCoordinates.Latitude)")
coord -- 29.613929
I've tried %l, %f and %# in the string all with the same results. Anyone see what it is I'm doing incorrect here?
Update
For anyone else, here is what I ended up doing to overcome the above. I followed the answer below a bit and created a class func that I have in one of the global classes in the app. This allows me to call it from any where. Here is the function
class func createUrlDrivingDiretions (sLat: Double, sLon: Double, eLat: Double, eLon: Double) -> String {
return "https://maps.googleapis.com/maps/api/directions/json?origin=\(sLat),\(sLon)&destination=\(eLat),\(eLon)&sensor=false&units=metric&mode=driving"
}
Why don't you use the following syntax (use Swift... not legacy obj-c coding):
let var1 = "xxx"
let var2 = "yyy"
let var3 = "zzz"
let var4 = "www"
let var5 = "kkk"
let s = "https://maps.googleapis.com/maps/api/directions/json?origin=\(var1),\(var2)&destination=\(var4),\(var5)&sensor=false&units=metric&mode=driving"
Make sure var1... var5 are not optional otherwise you have to unwrap them or you'll get something like this in the output string: Optional(xxx)... instead of xxx
If you need special formatting use NumberFormatter (see this: Formatters)

Formatting strings in Swift

In some languages, like C# for example, you can create a string in the following way:
"String {0} formatted {1} "
And then format it with String.format by passing in the values to format.
The above declaration is good, because you don't have to know of what type its parameters are when you create the string.
I tried to find similar approach in Swift, but what I found out was something like the following format:
"String %d formatted %d"
which requires you to format the string with String(format: , parameters). This is not good because you would also have to know parameter types when declaring the string.
Is there a similar approach in Swift where I wouldn't have to know the parameter types?
Use this one:
let printfOutput = String(format:"%# %2.2d", "string", 2)
It's the same as printf or the Obj-C formatting.
You can also mix it in this way:
let parm = "string"
let printfOutput = String(format:"\(parm) %2.2d", 2)
Edit: Thanks to MartinR (he knows it all ;-)
Be careful when mixing string interpolation and formatting. String(format:"\(parm) %2.2d", 2) will crash if parm contains a percent character. In (Objective-)C, the clang compiler will warn you if a format string is not a string literal.
This gives some room for hacking:
let format = "%#"
let data = "String"
let s = String(format: "\(format)", data) // prints "String"
In contrast to Obj-C which parses the format string at compile time, Swift does not do that and just interprets it at runtime.
In Swift, types need to conform to the CustomStringConvertible protocol in order to be used inside strings. This is also a requirement for the types used in string interpolation like this:
"Integer value \(intVal) and double value \(doubleVal)"
When you understand the CustomStringConvertible, you can create your own function to fulfill your needs. The following function formats the string based on the given arguments and prints it. It uses {} as a placeholder for the argument, but you can change it to anything you want.
func printWithArgs(string: String, argumentPlaceHolder: String = "{}", args: CustomStringConvertible...) {
var formattedString = string
// Get the index of the first argument placeholder
var nextPlaceholderIndex = string.range(of: argumentPlaceHolder)
// Index of the next argument to use
var nextArgIndex = 0
// Keep replacing the next placeholder as long as there's more placeholders and more unused arguments
while nextPlaceholderIndex != nil && nextArgIndex < args.count {
// Replace the argument placeholder with the argument
formattedString = formattedString.replacingOccurrences(of: argumentPlaceHolder, with: args[nextArgIndex].description, options: .caseInsensitive, range: nextPlaceholderIndex)
// Get the next argument placeholder index
nextPlaceholderIndex = formattedString.range(of: argumentPlaceHolder)
nextArgIndex += 1
}
print(formattedString)
}
printWithArgs(string: "First arg: {}, second arg: {}, third arg: {}", args: "foo", 4.12, 100)
// Prints: First arg: foo, second arg: 4.12, third arg: 100
Using a custom implementation allows you to have more control over it and tweak its behavior. For example, if you wanted to, you could modify this code to display the same argument multiple times using placeholders like {1} and {2}, you could fill the arguments in a reversed order, etc.
For more information about string interpolation in Swift: https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html#//apple_ref/doc/uid/TP40014097-CH7-ID292

Resources