🐦

SwiftUIでUITextFieldを使うときに、文字数に応じてサイズが変わる問題の対処法メモ

2024/07/11に公開
2

概要

SwiftUIでUITextFieldを使うとき、 .frame(width: N) でサイズを指定しているのに文字数に応じてサイズが可変になってしまう問題の対処方法メモ

結論

  • ❌ UITextFieldをそのままUIViewRepresentableでラップする
  • ✅ 【iOS13+】 UITextFieldを生成 -> UIViewにaddSubView -> Autolayoutを設定 -> UIViewをラップする
  • ✅ 【iOS16+】 sizeThatFits(_:) を実装する

❌ Bad

import SwiftUI

struct BadUITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        return textField
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}

✅ Good - iOS13+

  • UITextFieldを生成
  • 親となるUIViewを生成し、addSubview
  • コード上でAuto Layoutを設定
  • UIViewをラップする
struct Good_iOS13_UITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        
        let view = UIView()
        view.addSubview(textField)
        // AutoresizingMaskからAutoLayoutへの自動変換を無効化
        textField.translatesAutoresizingMaskIntoConstraints = false
        // 対象のViewの四辺にアンカーを貼る
        NSLayoutConstraint.activate([
            textField.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
            textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0),
            textField.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
            textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0)
        ])
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}

✅ Good - iOS16+

追記 2024/07/12

最低サポートOSが iOS16+ である場合、sizeThatFits(_:) を用いることでUIViewをラップせずにすみます。

anz さん、情報提供ありがとうございます 🥰
https://zenn.dev/link/comments/fc0f465f4464e

struct Good_iOS16_UITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        return textField
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) { }

    // iOS16未満では利用不可能
    func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? {
        guard let width = proposal.width, let height = proposal.height else {
            return nil
        }
        return .init(width: width, height: height)
    }
}

再現コード

再現コード (フル)
import SwiftUI

struct BadUITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        return textField
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}

struct Good_iOS16_UITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        return textField
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) { }

    @available(iOS 16.0, *)
    func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? {
        guard let width = proposal.width, let height = proposal.height else {
            return nil
        }
        return .init(width: width, height: height)
    }
}

struct Good_iOS13_UITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        
        let view = UIView()
        view.addSubview(textField)
        // AutoresizingMaskからAutoLayoutへの自動変換を無効化
        textField.translatesAutoresizingMaskIntoConstraints = false
        // 対象のViewの四辺にアンカーを貼る
        NSLayoutConstraint.activate([
            textField.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
            textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0),
            textField.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
            textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0)
        ])
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}


#Preview {
    VStack {
        Spacer()
        VStack {
            Text("可変になってしまう 🤪")
            BadUITextFieldWrapperView()
                .frame(width: 100, height: 40)
        }
        VStack {
            Text("iOS13+ 固定 👍")
            Good_iOS13_UITextFieldWrapperView()
                .frame(width: 100, height: 40)
        }
        VStack {
            Text("iOS16+ 固定 👍")
            Good_iOS16_UITextFieldWrapperView()
                .frame(width: 100, height: 40)
        }
        Spacer()
    }
    .padding(16)
}

備考①

UITextFieldを直接Wrapする方法で解決できる方法があれば教えて頂けると嬉しいです 🙇‍♂️

追記 2024/07/12

iOS16+ で利用可能な sizeThatFits(_:) を用いることで単体でもサイズを固定できるようです。
anzさん、教えて頂きありがとうございます! 🥰

https://zenn.dev/link/comments/fc0f465f4464e6

備考②

以下のようなextensionを用いることでAutoLayoutを簡単に設定でき、コードが見やすくなります。

extension UIView {
    func anchorAll(equalTo: UIView) {
        // AutoresizingMaskからAutoLayoutへの自動変換を無効化
        translatesAutoresizingMaskIntoConstraints = false
        // 対象のViewの四辺にアンカーを貼る
        topAnchor.constraint(equalTo: equalTo.topAnchor, constant: 0).isActive = true
        leftAnchor.constraint(equalTo: equalTo.leftAnchor, constant: 0).isActive = true
        bottomAnchor.constraint(equalTo: equalTo.bottomAnchor, constant: 0).isActive = true
        rightAnchor.constraint(equalTo: equalTo.rightAnchor, constant: 0).isActive = true
    }
}
struct GoodUITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        
        let view = UIView()
        view.addSubview(textField)
-        textField.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            textField.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
-            textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0),
-            textField.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
-            textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0)
-        ])
+        textField.anchorAll(equalTo: view)
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}

宣伝

MENTAはじめました! iOSアプリ開発でお困りの方はぜひご相談ください 👋

https://menta.work/user/100368

XにてiOSアプリ開発周りの情報を発信しています!

https://x.com/k0uhashi

個人開発したアプリが10万DL突破しました!

https://www.privatebrowser.jp/

https://note.com/k0uhashi/n/n6621e26475c8

Discussion

anzanz

気になったので調べてみたら iOS 16+ でよければ sizeThatFits(_:uiView:context:) を使えばできそうでしたー。

https://developer.apple.com/documentation/swiftui/uiviewrepresentable/sizethatfits(_:uiview:context:)-9ojeu

雑に書くとこんな感じです。

struct BadUITextFieldWrapperView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let textField = UITextField()
        textField.borderStyle = .line
        textField.clearButtonMode = .always
        return textField
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) { }

    func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? {
        guard let width = proposal.width, let height = proposal.height else {
            return nil
        }
        return .init(width: width, height: height)
    }
}

まぁ、SwiftUI だけで完結できるのがベストではあるのですけど、、ね 😇

Ryo TakahashiRyo Takahashi

情報提供ありがとうございます!!!追記しました!!
sizeThatFits くんのことをすっかり忘れていました!!

まぁ、SwiftUI だけで完結できるのがベストではあるのですけど、、ね 😇

TextField 2.0 のリリースが楽しみですね! 😇