6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIでURL付きTextを多言語対応しつつ部分的にスタイルを変えたい!そんな時に実は使えるテクニック

Posted at

はじめに

本田技研工業でiOSアプリ開発を担当している松野です。

SwiftUIでアプリを開発していると、多言語対応(ローカライゼーション)は避けて通れない課題です。特に、String Catalog を使って文字列を管理しているケースは多いでしょう。

そんな中、「ローカライズした文章の一部だけ太字にしたい」「特定の単語に下線付きのリンクを設定したい」といった、部分的なスタイリングの要件が出てくることがあります。

例えば、以下のようなデザインを実現したいケースです。

(日)日本の航空会社には日本航空全日空があります1

(英)Japanese airlines are JAL and ANA.

今回は、このような「多言語対応」と「部分的なリッチテキスト化」を両立させるための実装方法について、いくつかのパターンを比較しながら解説します。

課題:単純な実装ではうまくいかない

まず、多くの人が最初に試すであろういくつかの実装パターンと、その問題点を見ていきます。

パターン1: Text の連結

SwiftUIでは + 演算子で Text を連結できます。これを使えばスタイルの適用は簡単です。

(
    Text("日本の航空会社には")
    + Text(.init("[日本航空](https://www.jal.co.jp/jp/ja/)"))
        .bold()
        .underline()
    + Text("と")
    + Text(.init("[全日空](https://www.ana.co.jp/)"))
        .bold()
        .underline()
    + Text("があります")
)
.tint(Color.gray)

課題: この方法では英語対応 (Japanese airlines are JAL and ANA.) しようとすると、語順が変わるため if/else などで表示を切り替える必要があり、言語が増えるたびに複雑さが増していきます。これでは String Catalog を使うことが少々厳しいです。

パターン2:String Catalog と Markdown

次に、ローカライズを考慮して String Catalog を使う方法を考えます。Text は限定的な Markdown を解釈できるので、以下のように記述できます。

Text("jpAirlines", bundle: .module)
.tint(Color.gray)
{
  "sourceLanguage" : "ja",
  "strings" : {
    "jpAirlines" : {
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Japanese airlines are [JAL](https://www.jal.co.jp/jp/ja/) and [ANA](https://www.ana.co.jp/)."
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "日本の航空会社には[JAL](https://www.jal.co.jp/jp/ja/)と[ANA](https://www.ana.co.jp/)があります"
          }
        }
      }
    }
  }
}

課題: これでリンクにはなりますが、太字下線 といった細かいスタイリングができません。Markdown記法 (**太字** など) は Text の中では解釈されないため、デザイナーの要求に応えられない可能性があります。

パターン3:NSAttributedString と HTML

リッチな表現といえば NSAttributedString です。HTML文字列をパースして表示するカスタムViewを作成する方法が考えられます。

import SwiftUI

// HTMLをパースして表示するカスタムView
public struct HTMLText: View {
    @State private var attributedString: NSAttributedString?
    
    // ... (中略) ...
    // HTMLのaタグを検出し、下線やフォントなどのスタイルを適用する処理
    
    // let linkAttributes: [NSAttributedString.Key: Any] = [
    //     .font: linkFont,
    //     .foregroundColor: UIColor(linkColor),
    //     .underlineColor: UIColor(linkColor),
    //     .underlineStyle: NSUnderlineStyle.single.rawValue
    // ]
    // といった形式でリンク部分のattributesを設定
}

このカスタムViewを使えば、String Catalog 側でHTMLタグを直接記述することで、デザイン要件を満たます。

HTMLText(String(localized: "jpAirlines", bundle: .module), linkColor: .gray)
{
  "sourceLanguage" : "ja",
  "strings" : {
    "jpAirlines" : {
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Japanese airlines are <a href=\"https://www.jal.co.jp/jp/ja/\">JAL</a> and <a href=\"https://www.ana.co.jp/\">ANA</a>."
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "日本の航空会社には<a href=\"https://www.jal.co.jp/jp/ja/\">日本航空</a>と<a href=\"https://www.ana.co.jp/\">全日空</a>があります"
          }
        }
      }
    }
  }
}

課題?: 実現は可能ですが、リンクにスタイルを適用するだけのために、HTMLパーサーを含むカスタムViewを自前で用意するのは、かなり大掛かりでメンテナンスコストも高くなります。

今回紹介するテクニック:LocalizedStringKey の文字列補間を活用する

そこで登場するのが、Text のイニシャライザが持つ 文字列補間(String Interpolation) の機能です。

公式ドキュメントにも記載があるように、Text の文字列リテラル内では \() 構文を使って、数値や日付、そして TextImage インスタンス を埋め込むことができます。

To create a localized string key from a string interpolation, use the \() string interpolation syntax. ... The interpolated types can include numeric values, Foundation types, and SwiftUI Text and Image instances.

--- Apple Developer Documentation

これを利用すると、String Catalog にはプレースホルダー (%@, %dなど) を持つテンプレート文字列を定義し、コード側でそのプレースホルダーにスタイルを適用した Text を埋め込むことができます。

実装方法

1. String Catalog の設定

文章のテンプレートをキーとし、%@ をプレースホルダーとして配置します。リンク部分の文字列 (URLofJAL, URLofANA) も個別のキーとして定義しておきます。

{
  "sourceLanguage" : "ja",
  "strings" : {
	  "jpAirlinesInterpolation %@  %@" : {
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Japanese airlines are %1$@ and %2$@."
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "日本の航空会社には%1$@と%2$@があります"
          }
        }
      }
    },
    "URLofANA" : {
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[ANA](https://www.ana.co.jp/)"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[全日空](https://www.ana.co.jp/)"
          }
        }
      }
    },
    "URLofJAL" : {
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[JAL](https://www.jal.co.jp/jp/ja/)"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[日本航空](https://www.jal.co.jp/jp/ja/)"
          }
        }
      }
    }
  }
}

2. SwiftUIコードの実装

Text のイニシャライザ内で、文字列補間 \() を使って、ローカライズした Text.bold().underline() といったモディファイアを適用して埋め込みます。

Text(
    // "jpAirlinesInterpolation %@ %@" というキーを呼び出し、
    // \(...) の部分をプレースホルダーに挿入する
    "jpAirlinesInterpolation \(Text("URLofJAL").bold().underline()) \(Text("URLofANA").bold().underline())",
    bundle: .main
)
.tint(.gray)

これによりString Catalogで文章や翻訳を管理しつつ、SwiftUI標準の Text だけで完結する実装が可能となります。

応用例:様々なスタイルを適用する

この方法はリンクだけでなく、部分的に色を変えたり、特定の単語だけカスタムフォントを適用したりといった様々なケースで役立ちます。

Text("SomeFontsText \(Text("Bold", bundle: .main).bold()) \(Text("Italic", bundle: .main).italic()) \(Text("Red", bundle: .main).foregroundColor(Color.red)) \(Text("Blue", bundle: .main).foregroundColor(Color.blue)) \(Text("HighPitch", bundle: .main).speechAdjustedPitch(1.0)) \(Text("LowPitch", bundle: .main).speechAdjustedPitch(-1.0))", bundle: .main)

スクリーンショット 2025-01-22 9.44.01.png

例えば、「本アプリHogeHugaをご利用いただきありがとうございます」のように、サービス名にだけ特別なフォントを適用する、といった使い方もできるかなと思います。

注意点

1. リンクごとに色を変えたい場合は不向き

「日本航空は」「全日空は」のように、リンクごとに異なる色を付けたい場合はどうでしょうか?

このケースでは、LocalizedStringKey の文字列補間だけでは少し難しくなります。なぜなら、.tint() モディファイアは View 全体に適用され、個別の Text に適用しても View を返してしまうため、+ 演算子や文字列補間が使えなくなるからです。

このような、より複雑なスタイリング要件がある場合は、前述の パターン3:NSAttributedString を使うアプローチが有効になります。NSAttributedString であれば、範囲(Range)を指定して個別に属性(色、フォントなど)を細かく設定できるためです。

2. 実行時の挙動に気をつける必要がある

String Catalogではプレースホルダーの個数と引数で与えた個数が異なっていてもコンパイルエラーが生じません。

そのため、String Catalogが機能せずText内に記載した内容がそのまま描画されてしまったり、実装方法によっては実行時エラーが生じてしまいます。

まとめ

SwiftUIで多言語対応されたテキストの一部にスタイルを適用する方法を解説しました。

LocalizedStringKey を使う方法、NSAttributedString を使う方法、またそもそもswift-markdownなどの外部ライブラリを用いる方法など、今回の要件を実現する方法は色々あります。

個々のプロジェクトの方針に従って適切な手法を選んでみて下さい。

追記

WWDC2025にてAttributedStringの更なる活用方法が紹介されました。
これによって今回紹介した手法を使わずとも柔軟なデザイン変更ができるかもしれません。
調査が完了したら別途記事にしたいと思います。

  1. 筆者が直近で飛行機に搭乗し、記憶に残っていたJAL/ANAを例示に採用させて頂きました。特段こちらの企業名に意味はありません。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?