SwiftUIで日本語テキストが不自然に改行される

2023.12.05

SwiftUIのTextビューで日本語を表示する際、意図しない箇所で改行が入ることがある。改行コードを使わずに改行位置を調整する方法を紹介する。

日本語テキストが不自然に改行される

SwiftUIのTextビューで日本語を表示する時、意図しない箇所で改行が入ることがある。たとえば「あなたに寄り添う究極のイノベーション」というフレーズが、「あなたに寄り添う\n究極のイノベーシ\nョン」と不自然に改行されることがある。

例図では以下のように表示されている。

あなたに寄り添う
究極のイノベーシ
ョン
あなたの未来を形
作る最上のガジェ
ット

英語圏で作られたWebサイトでの日本語の改行問題

英語圏で作られた後に日本語に翻訳されたサイトの例では、「今すぐダウンロード」というフレーズが「今すぐダウンロー\nド」と表示されることがあった。日本人の感覚からするとこれには強い違和感がある。他にも禁則処理が考慮されていないケースで、句読点が行頭に来てしまうのは気になってしまう。

Viewの横幅が固定であれば、テキストに改行コードを入れて調整することもできるが、本記事ではデバイスによって横幅が異なるグリッドでテキストを表示するケースを想定しているため、改行コードを入れる解決策は採用しない。

Word Joiner と Zero Width Space について

この問題を解決するため、「Word Joiner」と「Zero Width Space」という特殊文字を使う方法がある。これらの文字は画面のサイズや解像度に関わらず、一定の改行位置を保持できる。

「Word Joiner (U+2060)」は文字間の結合点を提供し、日本語の長いフレーズや複合語が改行時に不自然な箇所で分割されるのを防ぐ。一方、「Zero Width Space (U+200B)」は、必要に応じて自然な改行ポイントを提供し、長いURLや複雑な単語において適切な折り返しを可能にする。

これらは通常のテキストとは異なる特別な役割を果たしており、視覚的には表示されない文字である。

文字列リテラルを分割する

例として「あなたに寄り添う究極のイノベーション」を単語ごとに分割してみる。このフレーズを「あなた」「に」「寄り添う」「究極」「の」「イノベーション」と分割し、Word Joiner と Zero Width Space を使用して次のように変更する。

// before
"あなたに寄り添う究極のイノベーション"

// after
"あ\u{2060}な\u{2060}た{200B}に{200B}寄\u{2060}り\u{2060}添\u{2060}う{200B}究\u{2060}極\u{200B}の\u{200B}イ\u{2060}ノ\u{2060}ベ\u{2060}ー\u{2060}シ\u{2060}ョ\u{2060}ン"

サンプルコード

以下に、SwiftUIでの実装例を示す。LazyVGridを使用して2つの異なるレイアウトでテキストを表示し、\u{2060}\u{200B}を使用して改行の挙動を制御する。LazyVGridはグリッド状にアイテムを配置するViewで、詳細はリルオッサ氏の記事を参照してほしい。

import SwiftUI

struct ContentView: View {
    private let items = [
        "あなたに寄り添う究極のイノベーション",
        "あなたの未来を形作る最上のガジェット",
        "あ\u{2060}な\u{2060}た{200B}に{200B}寄\u{2060}り\u{2060}添\u{2060}う{200B}究\u{2060}極\u{200B}の\u{200B}イ\u{2060}ノ\u{2060}ベ\u{2060}ー\u{2060}シ\u{2060}ョ\u{2060}ン",
        "あ\u{2060}な\u{2060}た\u{200B}の\u{200B}未\u{2060}来を形\u{2060}作\u{2060}る最\u{2060}上\u{200B}の\u{200B}ガ\u{2060}ジ\u{2060}ェ\u{2060}ッ\u{2060}ト",
    ]
    
    var body: some View {
        let columns2 = [
            GridItem(.flexible()),
            GridItem(.flexible()),
        ]
        
        let columns1 = [
            GridItem(.flexible()),
        ]
        
        ScrollView {
            // 1行に2アイテム表示するレイアウト
            LazyVGrid(columns: columns2, spacing: 16) {
                ForEach(items, id: \.self) { item in
                    Button(action: {}) {
                        Text(item)
                            .font(.system(size: 18))
                            .padding()
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .overlay {
                                RoundedRectangle(cornerRadius: 24)
                                    .stroke()
                            }
                    }
                }
            }
            .padding(.horizontal, 16)
            .padding(.bottom, 8)
            
            // 1行に1アイテム表示するレイアウト
            LazyVGrid(columns: columns1, spacing: 16) {
                ForEach(items, id: \.self) { item in
                    Button(action: {}) {
                        Text(item)
                            .font(.system(size: 18))
                            .padding()
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .overlay {
                                RoundedRectangle(cornerRadius: 24)
                                    .stroke()
                            }
                    }
                }
            }
            .padding(.horizontal, 16)
        }
    }
}

このコードは、Word Joiner と Zero Width Space を使用して自然な改行ポイントを設定し、テキストが画面サイズに応じて適切に改行されるようにするものである。

実行結果

Word Joiner と Zero Width Space を適用したテキストをiOSシミュレータで実行すると、以下のように改善される。1段目が調整前のテキスト、2段目が改行位置を調整した後の表示である。

Word Joiner と Zero Width Space を有効に使えるシーンはあまり多くないと思うが、改行コードの使用を避けたい要件の時に思い出してもらえると幸いだ。

関連情報

本記事では、文字列リテラルに手動でWord JoinerとZero Width Spaceを適用した。

stewie氏によるBudouXのSwift版を使うことで、自動的に日本語の改行位置を最適化できる。ユーザー生成コンテンツなどの場合は、このライブラリの利用を検討すると良いだろう。