diff --git a/SodaLive/Sources/Extensions/FontModifier.swift b/SodaLive/Sources/Extensions/FontModifier.swift index 7b7e4bcb..33ba84e5 100644 --- a/SodaLive/Sources/Extensions/FontModifier.swift +++ b/SodaLive/Sources/Extensions/FontModifier.swift @@ -41,6 +41,10 @@ enum SodaTypography { } } + func localizedFontName(locale: Locale = .autoupdatingCurrent) -> String? { + fontName(for: .localized, weight: weight, locale: locale) + } + var weight: SwiftUI.Font.Weight { switch self { case .heading1, .heading2, .heading3, .heading4, .body1, .body4, .caption1: diff --git a/SodaLive/Sources/V2/Gift/Components/GiftContactInputRule.swift b/SodaLive/Sources/V2/Gift/Components/GiftContactInputRule.swift new file mode 100644 index 00000000..2fe4628b --- /dev/null +++ b/SodaLive/Sources/V2/Gift/Components/GiftContactInputRule.swift @@ -0,0 +1,33 @@ +enum GiftContactInputRule { + case name + case phoneNumber + + func acceptsEditing(_ text: String) -> Bool { + switch self { + case .name: + return text.count <= 12 && text.unicodeScalars.allSatisfy { scalar in + switch scalar.value { + case 0x41...0x5A, 0x61...0x7A, + 0x1100...0x11FF, 0x3130...0x318F, + 0xA960...0xA97F, 0xAC00...0xD7A3, 0xD7B0...0xD7FF: + return true + default: + return false + } + } + case .phoneNumber: + return text.count <= 11 && text.unicodeScalars.allSatisfy { + (0x30...0x39).contains($0.value) + } + } + } + + func acceptsSubmission(_ text: String) -> Bool { + switch self { + case .name: + return !text.isEmpty && acceptsEditing(text) + case .phoneNumber: + return text.count == 11 && text.hasPrefix("010") && acceptsEditing(text) + } + } +} diff --git a/SodaLive/Sources/V2/Gift/Components/GiftContactTextField.swift b/SodaLive/Sources/V2/Gift/Components/GiftContactTextField.swift new file mode 100644 index 00000000..e18a40e1 --- /dev/null +++ b/SodaLive/Sources/V2/Gift/Components/GiftContactTextField.swift @@ -0,0 +1,91 @@ +import SwiftUI +import UIKit + +struct GiftContactTextField: UIViewRepresentable { + @Binding var text: String + let rule: GiftContactInputRule + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + func makeUIView(context: Context) -> UITextField { + let textField = UITextField(frame: .zero) + textField.delegate = context.coordinator + textField.text = text + textField.backgroundColor = .clear + textField.textColor = .white + textField.font = SodaTypography.body3.localizedFontName() + .flatMap { UIFont(name: $0, size: SodaTypography.body3.size) } + ?? UIFont.systemFont(ofSize: SodaTypography.body3.size, weight: .regular) + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + let placeholder: String + switch rule { + case .name: + placeholder = "이름을 입력해 주세요" + textField.textContentType = .name + case .phoneNumber: + placeholder = "휴대폰 번호를 입력해 주세요" + textField.keyboardType = .numberPad + textField.textContentType = .telephoneNumber + } + textField.attributedPlaceholder = NSAttributedString( + string: placeholder, + attributes: [.foregroundColor: UIColor(Color.gray600)] + ) + textField.setContentHuggingPriority(.defaultLow, for: .horizontal) + textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + textField.addTarget(context.coordinator, action: #selector(Coordinator.textDidChange(_:)), for: .editingChanged) + return textField + } + + func updateUIView(_ uiView: UITextField, context: Context) { + context.coordinator.parent = self + guard uiView.markedTextRange == nil, + text != context.coordinator.lastAcceptedText, + rule.acceptsEditing(text) else { return } + context.coordinator.lastAcceptedText = text + if uiView.text != text { + uiView.text = text + } + } + + final class Coordinator: NSObject, UITextFieldDelegate { + var parent: GiftContactTextField + var lastAcceptedText: String + + init(parent: GiftContactTextField) { + self.parent = parent + lastAcceptedText = parent.rule.acceptsEditing(parent.text) ? parent.text : "" + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString replacement: String) -> Bool { + guard textField.markedTextRange == nil else { return true } + let current = textField.text ?? "" + let prospective = (current as NSString).replacingCharacters(in: range, with: replacement) + return parent.rule.acceptsEditing(prospective) + } + + @objc func textDidChange(_ textField: UITextField) { + guard textField.markedTextRange == nil else { return } + let committed = textField.text ?? "" + guard parent.rule.acceptsEditing(committed) else { + textField.text = lastAcceptedText + return + } + lastAcceptedText = committed + if parent.text != committed { + parent.text = committed + } + } + + func textFieldDidChangeSelection(_ textField: UITextField) { + textDidChange(textField) + } + + func textFieldDidEndEditing(_ textField: UITextField) { + textDidChange(textField) + } + } +}