How to determine if a passed argument is a string literal?

In SwiftUI the Text view comes with multiple initializers, my question is about one of them:

public init<S>(_ content: S) where S : StringProtocol

..which has the following documentation:

Creates a text view that displays a stored string without localization.

Use this intializer to create a text view that displays — without
localization — the text in a string variable.

    Text(someString) // Displays the contents of `someString` without localization.

SwiftUI doesn't call the `init(_:)` method when you initialize a text
view with a string literal as the input. Instead, a string literal
triggers the ``Text/init(_:tableName:bundle:comment:)`` method — which
treats the input as a ``LocalizedStringKey`` instance — and attempts to
perform localization.
...

In short:

  • this initializer is used when the passed parameter is not a string literal
  • if we pass a string literal, another initializer is called instead.

My question is: how?

If I try to mimic the same API in a new view, the StringProtocol initializer above always takes priority:

struct MyView: View {
  let title: Text

  init<S: StringProtocol>(_ content: S) {
    self.title = Text(content)
  }

  init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) {
    self.title = Text(key, tableName: tableName, bundle: bundle, comment: comment)
  }

  var body: some View {
    title
  }
}

struct ContentView: View {
  var aString: String = "aString"
  var body: some View {
    VStack {
      MyView(aString) // displays "aString"
      MyView("my_localized_title") // displays "my_localized_title"
      MyView("my_localized_title" as LocalizedStringKey) // displays the actual localization
      MyView(LocalizedStringKey("my_localized_title")) // displays the actual localization
    }
  }
}

As shown, the only way to trigger MyView's "localized" initializer is to explicitly pass a LocalizedStringKey, which is not the case for Text:

struct ContentView: View {
  var aString: String = "aString"
  var body: some View {
    VStack {
      Text(aString) // displays "aString"
      Text("my_localized_title") // displays the actual localization
      Text("my_localized_title" as LocalizedStringKey) // displays the actual localization
      Text(LocalizedStringKey("my_localized_title")) // displays the actual localization
    }
  }
}

My theory is that even in SwiftUI's Text the init<S: StringProtocol>(_ content: S) is called, and internally it's checked whether content is a string literal or not, and then swift proceeds from there (hence this discussion title).

Is this even possible? How can we mimic SwiftUI's Text behavior?
Thank you in advance!

1 Like

There must be something special going on:

import SwiftUI

func foo(_ s: LocalizedStringKey) {
    print("literal")
}

func foo<S: StringProtocol>(_ s: S) {
    print("String")
}

let n = 123.456
Text("Some \(n, specifier: "%.2f")")   // this works
foo("Some \(n, specifier: "%.2f")")    // compile error: extra argument 'specifier' in call
let s = "some other string"
foo(s)

LocalizedStringKey is ExpressibleByStringInterpolation, but only Text is "aware" of this capability. Calling my foo, it's treating it as a plain string literal so of course it do not know the interpolation that take a specifier.

My guess is some magic preprocessor thing parses the Text("some string literal") and turn these into Text(LocalizedStringKey("some string literal")).

For sure @luca_bernardi knows the answer!

1 Like

I'm not 100% sure about how SwiftUI is actually doing it since I'm not seeing any explicit attribute on those initializers, but you can achieve it using the currently private @_disfavoredOverload attribute:

struct MyView: View {
  let title: Text

  @_disfavoredOverload
  init<S: StringProtocol>(_ content: S) {
    title = Text("not localized")
  }

  init(_ key: LocalizedStringKey) {
    title = Text("localized")
  }

  var body: some View {
    title
  }
}

Within your example, it will output:

not localized
localized
localized
localized
2 Likes

:astonished: so this is the secret sauce!

Using this attribute solves another overload resolution of generic vs. "existential" problem.

So this attribute is for when the compiler cannot decide which overload it should "favor". I wonder why the compile not just favor generic over "existential" in the case I just linked to?

Another question is with this attribute, it can break tie of two, what happen if there are more than two overloads? Say three? Seems it needs to take a "prirority" value to work this out?

1 Like

That's very neat, thank you Stefano!
Yesterday I was looking for something exactly like this :smiley:

When generating the interface for MyView, we actually don't see the @_disfavoredOverload attribute:

Our code:

public struct MyView: View {
  let title: Text

  @_disfavoredOverload
  public init<S: StringProtocol>(_ content: S) {
    print(content)
    self.title = Text(content)
  }

  public init(key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) {
    self.title = Text(key, tableName: tableName, bundle: bundle, comment: comment)
  }

  public var body: some View {
    title
  }
}

The generated interface:

public struct MyView : View {

    internal let title: Text

    public init<S>(_ content: S) where S : StringProtocol

    public init(key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

    /// The content and behavior of the view.
    public var body: some View { get }
}

Therefore I believe @_disfavoredOverload might be how Text accomplishes this as well!

I'm very open for more leads/suggestions/ideas, but this seems a very good start! :100:

@xAlien95 is correct. The way Text achieve that is by annotating the initializer generic over StringProtocol to be @_disfavoredOverload.

That said I should also mention the usual words of warning: that is a private attribute for a reason :blush:.

2 Likes

Many thanks for the insight! :raised_hands:t2:

@zntfdr 's code doesn't work in Xcode 16.1 and Swift 5.