Posts Markdown SwiftUI.Textにmarkdownを渡し、リンク作成するときの注意点

SwiftUI.Textにmarkdownを渡し、リンク作成するときの注意点

SwiftUI.Textにmarkdownを渡し、リンク作成するときの注意点

SwiftUIで提供されているTextというViewがあります。 このText、実はmarkdown表記に対応しています。 markdownに対応しているので、もちろんリンクを貼ることも可能です。

Text("これは[リンク](https://google.com)です")

ところで、以下のようなViewを作成してみました Textに対して以下の3種類の型を渡しています

  • String型
  • AttributedString型
  • LocalizedStringKey型(利用するイニシャライザの種類によって複数用意しています)

また、それぞれの型に対して、URLを文字列リテラルとして渡しているものと、変数を参照しているものの2種類を用意しました

さて、以下のTextは、リンクとして正常に動作するものとしないものがあります どれが正常に動作するリンクか、分かりますか?

struct ContentView: View {
    static let url = "https://google.com"
    
    // String型
    let textString1: String = .init("textString1: [リンク](https://google.com)")
    let textString2: String = .init("textString2: [リンク](\(url))")

    // AttributedString型
    let textAttributedString1: AttributedString = .init("textAttributedString1: [リンク](https://google.com)")
    let textAttributedString2: AttributedString = .init(stringLiteral: "textAttributedString2: [リンク](\(url))")

    // LocalizedStringKey型1
    let textLocalizedStringKey1: LocalizedStringKey = "textLocalizedStringKey1: [リンク](https://google.com)"
    let textLocalizedStringKey2: LocalizedStringKey = "textLocalizedStringKey2: [リンク](\(url))"

    // LocalizedStringKey型2
    let textLocalizedStringKey3: LocalizedStringKey = LocalizedStringKey("textLocalizedStringKey3: [リンク](https://google.com)")
    let textLocalizedStringKey4: LocalizedStringKey = LocalizedStringKey("textLocalizedStringKey4: [リンク](\(url))")

    // LocalizedStringKey型3
    let textLocalizedStringKey5: LocalizedStringKey = .init("textLocalizedStringKey5: [リンク](https://google.com)")
    let textLocalizedStringKey6: LocalizedStringKey = .init("textLocalizedStringKey6: [リンク](\(url))")

    // LocalizedStringKey型4
    let textLocalizedStringKey7: LocalizedStringKey = .init(stringLiteral: "textLocalizedStringKey7: [リンク](https://google.com)")
    let textLocalizedStringKey8: LocalizedStringKey = .init(stringLiteral: "textLocalizedStringKey8: [リンク](\(url))")

    var body: some View {
        VStack {
            Text(textString1)
            Text(textString2)

            Text(textAttributedString1)
            Text(textAttributedString2)

            Text(textLocalizedStringKey1)
            Text(textLocalizedStringKey2)

            Text(textLocalizedStringKey3)
            Text(textLocalizedStringKey4)

            Text(textLocalizedStringKey5)
            Text(textLocalizedStringKey6)

            Text(textLocalizedStringKey7)
            Text(textLocalizedStringKey8)
        }
        .padding()
    }
}

1つずつ挙動を確認していきましょう!

正解はこちらからも確認できます
名前スタイリングされるかリンク先に飛べるか
textString1StringXX
textString2StringXX
textAttributedString1AttributedStringXX
textAttributedString2AttributedStringXX
textLocalizedStringKey1LocalizedStringKeyOO
textLocalizedStringKey2LocalizedStringKeyOX
textLocalizedStringKey3LocalizedStringKeyOO
textLocalizedStringKey4LocalizedStringKeyOX
textLocalizedStringKey5LocalizedStringKeyOO
textLocalizedStringKey6LocalizedStringKeyOO
textLocalizedStringKey7LocalizedStringKeyOO
textLocalizedStringKey8LocalizedStringKeyOO

環境

以下の環境で動作確認を行っています

  • OS: macOS Sonoma 14.7.1
  • XCode: 16.3.0

String型を利用した場合

String型をTextに渡した場合、表示は以下のようになります スクリーンショット 2025-04-26 16.01.29.png

リンクにとして扱われず、そのままの文字列として扱われています。 String型を渡した場合、入力したそのままの文字列が表示されるようです。

ここで、違和感を感じた方もいるかも知れません。 実は↓のようにTextに文字列を渡すと、リンクとして正常に機能します。

Text("[リンク](https://google.com)です")

これは、一件文字列を渡しているように見えるのですが、LocalizedStringKey.init(stringLiteral value: String)として扱われるようです。 つまり、一件String型を渡しているように見えて、実はLocalizedStringKeyを渡していることになります。 この場合の扱いはLocalizedStringKeyを使った場合の章に記すような挙動をします

AttributedString型を利用した場合

こちらの場合も、基本的にはString型の場合と同様です。 今回はAttributedStringに対して装飾などを全く施していないので、String型と同様の見え方になります。

LocalizedStringKeyを利用した場合

LocalizedStringKeyを利用するパターンが、少しややこしいです。 まず、見た目は全てリンクとして表示されます。 ただし、見た目がリンクのようになっていても、リンクをタップしたときに正常にリンク先に飛べないパターンがあります。 URLを文字列で直接指定した場合と、URLを変数で指定した場合に分けて見ていきます。

URLを文字列リテラルで直接指定した場合

この場合には、どのイニシャライザを用いてLocalizedStringKeyを作成しても正常にリンク先に飛ぶことができます。 また、String型の章で紹介したText("[リンク](https://google.com)です")のような書き方も、このパターンに外とするため、正常に動作します。

URLを変数で指定した場合

URLを変数でしたいした場合には、リンクが正常に動作するパターンとしないパターンがあります。

let textLocalizedStringKey6: LocalizedStringKey = .init("textLocalizedStringKey6: [リンク](\(url))")
let textLocalizedStringKey8: LocalizedStringKey = .init(stringLiteral: "textLocalizedStringKey8: [リンク](\(url))")

この場合、textLocalizedStringKey6はLocalizedStringKey.init(_ value: String)が、textLocalizedStringKey8はLocalizedStringKey.init(stringLitelar value: String) が利用されています。

この2種のイニシャライザを利用する場合には、URLを変数で指定した場合にもリンクが正常に貼れるようです。

逆に、正常に動作しないのは、以下の2パターンです

let textLocalizedStringKey2: LocalizedStringKey = "textLocalizedStringKey2: [リンク](\(url))"
let textLocalizedStringKey4: LocalizedStringKey = LocalizedStringKey("textLocalizedStringKey4: [リンク](\(url))")

この場合、textLocalizedString2はinit(stringInterpolation: LocalizedStringKey.StringInterpolation)が、 textLocalizedString4はどのイニシャライザが動作しているのか追うことができませんでした(有識者の方いましたら、コメントにて細くいただけるとありがたいです)

これらの場合ではURLを変数で指定するとリンクが正常に動作しませんでした。

結果の総括

ここまでの結果をまとめるとSwiftUI.Textでmarkdowmでのリンクを貼るには、以下の条件が必要そうです

  • SwiftUI.TextにLocalizedStringKeyを渡す必要がある
  • リンク先URLを変数として渡す場合には、LocalizedStringKey.init(_ value: String)もしくはLocalizedStringKey.init(stringLitelar value: String)を利用して作成したLocalizedStringを渡す必要がある

この挙動の根本原因?

LocalizedStringKeyは、stringsファイルで指定したキーを動的に変更してくれるものです。 Textは、LocalizedStringKeyを受け取ると自動的にローカライズを実行してくれます。 Textの公式ドキュメントに、次のような記述がありました。

変数の値でテキストビューを初期化すると、ビューはinit(_:)イニシャライザを使用し、このイニシャライザは文字列をローカライズしません。しかし、最初にLocalizedStringKeyインスタンスを作成することでローカライズを要求することができ、これにより代わりにinit(_:tableName:bundle:comment:)イニシャライザが呼ばれます。(日本語訳)

おそらく、Textのmarkdownからリンクを表示するロジックは、ローカライズのロジックと同じような挙動をするのだと思います。 StringAttributedStringを直接渡した場合にはうまくマークダウンを解釈してくれませんが、LocalizedStringKeyを渡すとマークダウンを解釈してくれます。 LocalizedStirngKeyのイニシャライザによる挙動の差は、SwiftUI自体のソースコードが公開されていないため詳細な調査を断念しました。 もし詳しい方がいらっしゃれば、コメントなど頂けますとありがたいです。

まとめ

SwiftUI.Textはmarkdownを自動的に解釈し、記述に応じてリンクの形式などを表示できるが、変数を利用する場合にはうまくリンク先に飛べない場合もある。 その場合には、明示的にLocalizedStringKeyによる初期化を行ったあとにTextに入れてあげることで解消できる。 ただし、利用されるイニシャライザによっては正常に動作しないので、LocalizedStringKey.init(_ value: String)もしくはLocalizedStringKey.init(stringLitelar value: String)を利用して初期化しましょう