キーボードの「次へ」を押して、次のTextField,TextViewに移動する処理を簡単にしたFormChangeableを作ってみた

Saturday, February 20, 2016

ずっと構想はあったものの、うまく形にできずに早2ヶ月経ってしまったのですが、ようやく形にできたので公開してみます。

どういうライブラリかというと、この記事のタイトルにもなっていますが、
「キーボードの「次へ」を押して、次のTextField,TextViewに移動する処理を簡単にした」 ライブラリとなっています。

例えば会員登録画面で、“名前”、“メールアドレス”、“パスワード"と入力項目があった時に、
“名前"の記入が終わって、キーボードの「次へ」(next)を押したら、次の"メールアドレス"に移って入力を継続したい場合があります。
そうなったときにアプローチとしては、

  • if文でチェックして次を指定する
  • 配列とtagを用いる

が考えられます。そうしたアプローチは以下のような感じになります。

class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var mailTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    @IBOutlet private weak var profileTextView: UITextView!

    func textFieldShouldReturn(textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        if textField == nameTextField {
            mailTextField.becomeFirstResponder()
        } else if textField == mailTextField {
            passwordTextField.becomeFirstResponder()
        } else if textField == passwordTextField {
            profileTextView.becomeFirstResponder()
        } else {
            return true
        }
        return false
    }
}

あるいは、

class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var mailTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    private var textfields = [UITextField]()

    override func viewDidLoad() {
        super.viewDidLoad()
        textfields += [nameTextField, mailTextField, passwordTextField]
    }

    func textFieldShouldReturn(textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        let tag = textField.tag
        let nextIndex = tag + 1
        if nextIndex < textfields.count {
            textfields[nextIndex].becomeFirstResponder()
            return false
        }
        return true
    }
}

こんな感じになりますが、

  • if文でやる場合、項目が増えた時にしんどい
  • tagで管理するのはなんとなくスッキリしない
  • 配列を保持しておかないといけないので、余計なプロパティが増える
  • 配列で管理する場合、UITextField→UITextViewに移ったりが面倒になる。配列をAnyObjectにするわけにもいかないし…

といった問題点がでてきます。そこで、それらを解決するFormChangeableを作成しました。

使い方


まずはFormChangeableをimportします。

import FormChangeable

そうすると、UITextFieldまたはUITextViewにnextForm, previousFormというプロパティが追加されているので、
キーボードの「次へ」を押下したときに移動したい移動先のformを指定します。

nameTextField.nextForm = mailTextField
mailTextField.nextForm = passwordTextField
passwordTextField.nextForm = profileTextView

これで、キーボードの「次へ」をおした時に、
“nameTextField"→"mailTextField"→"passwordTextField"→"profileTextView"と移動するように設定されます。
また、UITextField→UITextView→UITextFieldと、 2つが混ざっても問題なく繋げられます

また、これだと複数formがあった時に大変なので、以下のようにFormChangeableの配列を用意して、一気に登録することができます。

let forms: [FormChangeable] = [nameTextField, mailTextField, passwordTextField, profileTextView]
forms.registerNextForm() //"nameTextField"→"mailTextField"→"passwordTextField"→"profileTextView"


あとは、各種TextField、TextViewのdelegateを設定し、
func textFieldShouldReturn(textField:) func textView(textView:, shouldChangeTextInRange:, replacementText:) の2つのメソッドで、以下のように記述します。

    func textFieldShouldReturn(textField: UITextField) -> Bool {
        textField.changeToNextForm()
        return false
    }

    func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
        if text == "\n" {
            textView.changeToNextForm()
            return false
        }
        return true
    }

最初に提示したアプローチよりも簡潔になりました!
また、もしnextFormが設定されていない場合は、そのTextField/TextViewのキーボードを閉じるだけになります。

どう実装したか


単純にUITextFieldのみであれば、UITextFieldにextensionを追加するだけなのですが、UITextViewが混ざっても大丈夫なようにするためには、
どうしてもprotocolを採用させ、nextFormに入れる型がUITextField、UITextViewでも問題ないようにしました。

public protocol FormChangeable {
    var form: UIResponder { get }
    var nextForm: FormChangeable? { get set }
    var previousForm: FormChangeable? { get set }
    func setReturnKeyType(keyType: UIReturnKeyType)
    var returnKeyType: UIReturnKeyType { get set }
}

extension UITextField: FormChangeable {}
extension UITextView: FormChangeable {}

また、内部ではnextForm,previousFormに入れるオブジェクトを弱参照で保持するために、
objc_setAssociatedObject,objc_getAssociatedObjectを用いています。
更に、UITextField、UITextViewにFormChangeableを採用したことで、これらを入れた配列のみに対して、配列操作を行う関数を実装出来るようになります。

public extension CollectionType where Generator.Element == FormChangeable {
    func registerNextForm() {
        var pre: FormChangeable?
        forEach {
            pre?.nextForm = $0
            pre = $0
        }
    }

    func registerPreviousForm() {
        var pre: FormChangeable?
        reverse().forEach {
            pre?.previousForm = $0
            pre = $0
        }
    }

    func setNextReturnKeyType(lastKeyType: UIReturnKeyType = .Done) {
        forEach { $0.setReturnKeyType(.Next) }
        reverse().first?.setReturnKeyType(lastKeyType)
    }
}

この、where Generator.Element == FormChangeableの箇所で、CollectionTypeのelementがFormChangeableを採用した型と限定されます。これで、registerNextForm()によって、一気にnextFormを指定することができます。
ただ、どうにもならない問題としては、UITextField、UITextViewが混ざった配列は、Swiftが [UIView] と推論してしまうので、

let forms: [FormChangeable] = [nameTextField, mailTextField, passwordTextField, profileTextView]
forms.registerNextForm() //"nameTextField"→"mailTextField"→"passwordTextField"→"profileTextView"

と、[FormChangeable]と明示してあげる必要があります。そうでないと、registerNextForm()を呼び出すことができません。

長くなりましたが、以上となります。よかったら使ってみてください!

techSwiftGithub

try?のうまい使い方

travisで特定のブランチ/タグだけ実行するようにする