본문 바로가기

학습 노트/Swift UI Trick

TextField 입력값 제한하기

입력값을 제한할 때는 보통 세 가지를 고려해야 한다.
숫자만 입력한다고 가정 해 보자.

  • 사용자는 숫자키보드(numeric)가 아닌 SW키보드를 사용할 가능성이 있다.
  • 블루투스나 iPad의 스마트 키보드등의 외장 HW키보드를 사용할 가능성이 있다.
  • 복사, 붙여넣기로 값을 입력할 수 있다.
struct LabelView: View {
    @State private var value = ""
    @State private var input = ""

    let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return formatter
    }()

    var body: some View {
        Form {
            Text("Form")
            TextField("Field_01", text: $value, prompt: Text("Field_01"))
        }

        GeometryReader { geometry in
            VStack {
                Section {
                    TextField("Field_02", text: $input, prompt: Text("Field_02"))
                        .textFieldStyle(.roundedBorder)
                        .padding()
                        .textContentType(.telephoneNumber)
                        .keyboardType(.numberPad)
                } header: {
                    Text("Stack")
                }
            }
            .frame(minHeight: (geometry.size.height) / 2)
        }
    }
}

위의 코드와 같이 TextField의 keyboardType을 변경해 주는 것으로 첫 번째 가능성을 해결할 수 있다.
다만 UIKit 때와 마찬가지로 이외의 가능성을 원천적으로 방지하는 것이 SwiftUI에서도 필요하다.

타이밍

SwiftUI에서 제공하는 View에는 각각이 처리하는 event에 대한 modifier가 존재한다.
TextField에서 사용을 고려해 볼 수 있는 modifier은 대략 다음과 같이 추릴 수 있다.

  • onPasteCommand
    붙여넣기가 발생했을 때
  • onRecieve
    값이 입력 될 때
  • onSubmit
    키보드의 엔터를 눌렀을 때

이외의 modifier를 확인해 보려면 다음 링크가 도움이 된다.

 

Apple Developer Documentation

 

developer.apple.com

전부 문제를 해결하는 데에 도움이 되지만, 일괄적으로 해결할 수 있는 modifier는 onSubmit과 onRecieve로 다시 추릴 수 있다.
이번에는 onRecieve를 사용해 문제를 해결해 보자.

import SwiftUI
import Combine

struct LabelView: View {
    @State private var value = ""
    @State private var input = ""

    @State private var alertStat = false

    var body: some View {
        Form {
            Text("Form")
            TextField("Field_01", text: $value, prompt: Text("Field_01"))
        }

        GeometryReader { geometry in
            VStack {
                Section {
                    TextField("Field_02", text: $input, prompt: Text("Field_02"))
                        .textFieldStyle(.roundedBorder)
                        .padding()
                        .textContentType(.telephoneNumber)
                        .keyboardType(.numberPad)
                        .onReceive(Just(input)) { newValue in
                            let filtered = newValue.filter { "0123456789".contains($0)}
                            if filtered != newValue {
                                self.input = filtered
                                alertStat.toggle()
                            }
                        }
                        .alert("Warning: Input must Digit", isPresented: $alertStat) {
                            Button(role: .cancel) {

                            } label: {
                                Text("Close")
                            }

                        }
                } header: {
                    Text("Stack")
                }
            }
            .frame(minHeight: (geometry.size.height) / 2)
        }
    }
}

핵심은 이 부분이다.

.onReceive(Just(input)) { newValue in
    let filtered = newValue.filter { "0123456789".contains($0)}
    if filtered != newValue {
        self.input = filtered
        alertStat.toggle()
    }
}

이해하기 위해서는 TextField의 동작 구조를 아는 것이 좋다.

TextField는 현재 'input' State Variable로 Binding 된 상태이고.
이 값이 변경되면 TextField에 다시 반영이 되는 방식으로 돼있다.

이 과정 중 State Variable이 변경되면 SwiftUI가 body를 재작성하게 되고,
이 과정 중 Just가 호출 돼 'input'에 대한 단일 처리를 진행한다.

onRecieve 메서드는 선언된 View를 subscriber로 만들게 되고, 이 경우 publisher는 Just가 된다.

간단하게 설명하면 onRecieve modifier는 선언되는 View를 subscriber로 만들 수 있고,
전달된 Just가 publisher 이므로 파라미터로 전달된 State Varibale를 사용해 반환된 Closure의 결과를
View에 전달하게 되는 구조이다.

시간이 지나면서 여러 방식의 구현이 등장했지만,
현시점까지 꽤나 빈틈없이 '완벽하게' 작동하는 최고의 구현이 아닐까 생각한다.