【SwiftUI】プレースホルダー付きのTextEditorを自作してみた

2022.06.06

SwiftUIのTextEditorにプレースホルダーを付けたかったのですが、標準のAPIではプレースホルダーがありませんでした。良い解決方法がなかなか見当たらず、試行錯誤しながら自作してみることにしました。

作ったもの

textfield-with-placeholder-demo

環境

  • Xcode 13.3
  • iOS 15.4

TextEditorWithPlaceholder

こちらがプレースホルダー付きのTextEditorになります。

import SwiftUI

struct TextEditorWithPlaceholder: View {

    @FocusState private var focusedField: Field?

    enum Field {
        case textEditor
        case placeholder
    }

    @Binding var text: String
    private let placeholderText: String

    init(_ placeholder: String, text: Binding<String>) {
        self._text = text
        self.placeholderText = placeholder
    }

    var body: some View {
        ZStack {

            // テキストが空の時にプレースホルダーを表示する
            if text.isEmpty {

                ZStack {
                    Rectangle()
                        .fill(Color(uiColor: .systemBackground))
                        .onTapGesture {
                            focusedField = .placeholder
                        }

                    VStack {
                        HStack {
                            TextField(placeholderText, text: $text)
                                .focused($focusedField, equals: .placeholder)
                                .onAppear {
                                    focusedField = .placeholder
                                }

                            Spacer()
                        }
                        .padding(.leading, 6)
                        .padding(.top, 8)

                        Spacer()
                    }
                }

                // テキストが空ではない時にTextEditorを表示する
            } else {
                TextEditor(text: $text)
                    .focused($focusedField, equals: .textEditor)
                    .onAppear {
                        focusedField = .textEditor
                    }
            }
        }
    }
}

FocusState

今回はFocusStateを用いて、プレースホルダー用のTextFieldTextEditorのフォーカス状態を管理します。

@FocusState private var focusedField: Field?

enum Field {
    case textEditor
    case placeholder
}

init

TextFieldWithPlaceholder生成時には、プレースホルダー用のテキストと、バインディングするテキストを設定します。

@Binding var text: String
private let placeholderText: String

init(_ placeholder: String, text: Binding<String>) {
    self._text = text
    self.placeholderText = placeholder
}

body

今回はテキストが空の時にはTextFieldを表示して、テキストが空ではない時はTextEditorを表示するようにしました。テキストが空の時にTextFieldを表示するのは、TextFieldのプレースホルダーをそのまま使用する為です。

テキストが空の時にはTextFieldを表示

if text.isEmpty {

    ZStack {
        Rectangle()
        // TextEditorっぽく見せる為に背景を設置
            .fill(Color(uiColor: .systemBackground))
        // 背景がタップされた時にもプレースホルダー用TextFieldにフォーカスを当てる
            .onTapGesture {
                focusedField = .placeholder
            }

        VStack {
            HStack {
                TextField(placeholderText, text: $text)
                // FocusStateで.placeholderでフォーカスが当たるようにする
                    .focused($focusedField, equals: .placeholder)
                //  テキストが空になり、TextFieldが表示された時にフォーカスが当たるようにする
                    .onAppear {
                        focusedField = .placeholder
                    }

                Spacer()
            }
            .padding(.leading, 6)
            .padding(.top, 8)

            Spacer()
        }
    }

まずTextFieldTextEditorっぽく見せる為に背景を設置しています。

TextField.focused($focusedField, equals: .placeholder).placeholderfocusedFieldに代入された時にこのTextFieldにフォーカスが当たるようにしています。そして、こちらの背景がタップされた時にTextFieldにフォーカスが当たるように実装しました。

また、テキストが空ではない状態から空になった時にも、TextFieldにフォーカスが当たる状態にしたかった為、onAppear時にもフォーカスされるようにしました。

.paddingでプレースホルダーの見え方がTextEditorと同じように見えるように調整しています。

テキストが空では無い時にTextEditorを表示

} else {
    TextEditor(text: $text)
    // FocusStateで.textEditorでフォーカスが当たるようにする
        .focused($focusedField, equals: .textEditor)
    //  テキストが空で無くなり、TextEditorが表示された時にフォーカスが当たるようにする
        .onAppear {
            focusedField = .textEditor
        }
}

.focused($focusedField, equals: .textEditor).textEditorfocusedFieldに代入された時にこのTextEditorにフォーカスが当たるようにしています。

また、テキストが空では無くなるとこのTextEditorが表示されるのですが、その時にTextEditorにフォーカスが当たる状態にしたかった為、onAppear時にもフォーカスが当たるようにしています。

使い方

自作したプレースホルダー付きTextEditorは、第一引数にプレースホルダーを渡して、第二引数にバインディングするテキストを渡して使います。

TextEditorWithPlaceholder("Write down...", text: $text)

ContentView

今回のサンプルデモで実装したContentViewは下記の内容になります。

struct ContentView: View {

    @FocusState private var isFocusedTextEditor: Bool
    @State private var text: String = ""

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.yellow)
                .ignoresSafeArea()

            VStack {

                Text("Memo pad")
                    .font(.title)

                TextEditorWithPlaceholder("Write down...", text: $text)
                    .focused($isFocusedTextEditor)
            }
            .padding()
        }
        .onTapGesture {
            isFocusedTextEditor = false
        }
    }
}

今回のコード

GitHubに上げております。

おわりに

プレースホルダー付きのTextEditorを自作することは出来ましたが、かなり強引な方法で作成しました。そもそも標準で提供していないので、TextEditorにはプレースホルダーは必要のないという見解なのか、それとも今後いつか追加されるのか興味津々です。こんなに強引に実装しなくてもより良い方法があるよ等ありましたら教えていただけると幸いです。

さぁ、今年のWWDCもいよいよですね。どんな楽しいニュースが待っているのでしょうか?

モバイルアプリ開発のチームメンバー絶賛募集中!

モバイル事業部では事業会社様と一緒に、数年間にわたり長期でモバイルアプリをグロースさせています。

そんなモバイルアプリ開発のチームメンバーを絶賛募集中です!

もちろんモバイルアプリ開発以外のエンジニアも募集中です!

クラスメソッド採用サイト

参考