SwiftUI　UITextField 　キャレット動作

こんにちはフリーランスの永田です。最近は法人化の手続きを開始しました。

SwiftUI案件を1月から実施予定で、現在技術を調査中です。

<!-- wp:paragraph -->
<p>今回はキャレット動作　returnButtonを押下しましたら、水平移動する対応です。</p>
<!-- /wp:paragraph -->

#### 環境
Xcode 11.3
SwiftUI



#### SwiftUIではない場合（オリジナルです。）
https://gist.github.com/daisukenagata/253ae79692234dbf89d042f5010f2387

#### キャレット動作しない場合
https://gist.github.com/daisukenagata/5002c49061f18d72e0a40dfda1290b1a

#### キャレット動作
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">SwiftUI_UITexTField_CaretLogic<br>情報が少ないテクニック。returnボタンを押下すると、カーソルの移動　by SwiftUI<a href="https://t.co/h8EMGgS8za">https://t.co/h8EMGgS8za</a> <a href="https://t.co/ehsFV5qoRj">pic.twitter.com/ehsFV5qoRj</a></p>&mdash; DaisukeNagata (@dbank0208) <a href="https://twitter.com/dbank0208/status/1206900614355009537?ref_src=twsrc%5Etfw">December 17, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

```swift

import UIKit
import SwiftUI

struct ContentView: View {

    @State var text: String = ""

    @State var text2: String = ""
    
    @State var spacing: CGFloat = 0
    
    @State var didTap  = false

    var body: some View {
        VStack {
            HStack(alignment: .bottom, spacing: spacing) {

                SATextField(tag: 0, placeholder: "placeholder", changeHandler: { (newString) in
                    self.text = newString
                }, onCommitHandler: {
                    // write something
                })
                self.text.isEmpty == false ?
                    HorizontalLine(color: self.didTap ? Color.red : Color.black) :
                    HorizontalLine(color: self.didTap ? Color.black : Color.red)

                SATextField(tag: 1, placeholder: "placeholder2", changeHandler: { (newString) in
                    self.text2 = newString
                }, onCommitHandler: {
                    // write something
                })
                text2.isEmpty == false ?
                    HorizontalLine(color: didTap ? Color.red : Color.black) :
                    HorizontalLine(color: didTap ? Color.black : Color.red)
            }
        }.position(.init(x: UIScreen.main.bounds.width/2+50, y: UIScreen.main.bounds.height/2))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct HorizontalLine: View {

    private var color: Color? = nil
    private var height: CGFloat = 1.0
    private var shape: HorizontalLineShape?

    init(color: Color, height: CGFloat = 1.0) {
        self.color = color
        self.height = height
    }

    var body: some View {
        HorizontalLineShape().fill(self.color!).frame(minWidth: 0, maxWidth: .infinity, minHeight: height, maxHeight: height)
    }
}

struct HorizontalLineShape: Shape {

    func path(in rect: CGRect) -> Path {

        let fill = CGRect(x: -rect.size.width, y: 0, width: rect.size.width, height: rect.size.height)
        var path = Path()
        path.addRoundedRect(in: fill, cornerSize: CGSize(width: 2, height: 2))

        return path
    }
}

class Model: ObservableObject {
    @Published var text = ""
    var placeholder = "Placeholder"
}
// check is this
// https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2
struct SATextField: UIViewRepresentable {
    private let tmpView = WrappableTextField()

    //var exposed to SwiftUI object init
    var tag:Int = 0
    var placeholder:String?
    var changeHandler:((String)->Void)?
    var onCommitHandler:(()->Void)?
    
    func makeUIView(context: UIViewRepresentableContext<SATextField>) -> WrappableTextField {
        tmpView.tag = tag
        tmpView.delegate = tmpView
        tmpView.placeholder = placeholder
        tmpView.onCommitHandler = onCommitHandler
        tmpView.textFieldChangedHandler = changeHandler
        return tmpView
    }
    
    func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<SATextField>) {
        uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
    }
}

class WrappableTextField: UITextField, UITextFieldDelegate {
    var textFieldChangedHandler: ((String)->Void)?
    var onCommitHandler: (()->Void)?
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
            nextField.becomeFirstResponder()
        } else {
            textField.resignFirstResponder()
        }
        return false
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let currentValue = textField.text as NSString? {
            let proposedValue = currentValue.replacingCharacters(in: range, with: string)
            textFieldChangedHandler?(proposedValue as String)
        }
        return true
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        onCommitHandler?()
    }
}

```
<p> </p>
#### 参考サイト
<p>こちらのロジックを拝借させていただきました。ロジックの部分は、今まで通りのプログラムになります。</p>
<p><a href="https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2">https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2</a></p>

プログラムがわかる場合は
`changeHandler`を追えば、すぐにわかると思います

#### 起動時に

`makeUIView`メソッドで`tmpView.textFieldChangedHandler = changeHandler`
を代入します。
既存のUITextFieldメソッドで実装しています。`onCommitHandler`もbind処理をしていますが、
Flowは同じです。


このメソッドは文字が１文字づつ変化する事に処理が行われます。 `textFieldChangedHandler?(proposedValue as String)`でbindして`changeHandler`が呼ばれています。

```swift

 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let currentValue = textField.text as NSString? {
            let proposedValue = currentValue.replacingCharacters(in: range, with: string)
            textFieldChangedHandler?(proposedValue as String)
        }
        return true
    }
    
```

return Buttonを押下時　これでキャレット`nextField.becomeFirstResponder()`
UITextFieldのキーボードを開くメソッドです。

```swift

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
            nextField.becomeFirstResponder()
        } else {
            textField.resignFirstResponder()
        }
        return false
    }
```

以上、とても簡単に解説しました
貴重なお時間お読みくださいまして、誠にありがとうございます。