글로벌 서비스를 개발하며, 국가와 언어 별로 상이한 숫자 표기 체계를 경험했습니다. 유저의 다양한 입력 환경에 대응할 수 있는 구현 방식이 무엇인지 고민하고, 이에 대해 정리한 공간입니다.
- iOS 14.1
- Swift 5.0
- SwiftUI
- Swift Package Manager
- 숫자 변환 필드 리스트 구현
- from locale, to locale 설정 -> textfield input -> Button -> to locale 숫자 포맷으로 변환
en_US(English, United States)
숫자에서ar(Arabic)
숫자로 변경
final class SampleFieldModel: ObservableObject {
@Published var fieldViewModels: [LocalizedNumberFieldViewModel] = [
LocalizedNumberFieldViewModel(
index: 0,
placeHolder: "숫자를 입력해주세요",
text: "",
formatter: LocalizedNumberFormatter(from: .en_US, to: Locale.ko_KR),
keyboardType: .default
),
LocalizedNumberFieldViewModel(
index: 1,
placeHolder: "숫자를 입력해주세요",
text: "",
formatter: LocalizedNumberFormatter(from: .en_US, to: Locale.fr_GP),
keyboardType: .default
),
LocalizedNumberFieldViewModel(
index: 2,
placeHolder: "숫자를 입력해주세요",
text: "",
formatter: LocalizedNumberFormatter(from: .en_US, to: Locale.ne),
keyboardType: .default
),
LocalizedNumberFieldViewModel(
index: 3,
placeHolder: "숫자를 입력해주세요",
text: "",
formatter: LocalizedNumberFormatter(from: .en_US, to: Locale.ar),
keyboardType: .default
),
LocalizedNumberFieldViewModel(
index: 4,
placeHolder: "숫자를 입력해주세요",
text: "",
formatter: LocalizedNumberFormatter(from: .en_US, to: Locale.it_CH),
keyboardType: .default
)
]
}
- 사용자 input과 숫자 변환에 따라 결과가 공유되어야 합니다.
- Sample Data Array는
@Published
Attributed로 지정합니다. - Sample Data
- from locale :
en_US
- to locale:
ko_KR
,fr_GP
,ne
,ar
,it_CH
- from locale :
import SwiftUI
@main
struct LocalizedNumberFieldApp: App {
@StateObject var sampleModel = SampleFieldModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sampleModel)
}
}
}
- 메인에서 샘플데이터를 environmentObject로 설정
struct ContentView: View {
@EnvironmentObject var modelData: SampleFieldModel
var body: some View {
SampleList()
.environmentObject(modelData)
}
}
- 메인에서 넘긴 SampleModel을 최상위뷰에서
@EnvironmentObject
Attribute로 설정.
import SwiftUI
struct SampleList: View {
@EnvironmentObject var modelData: SampleFieldModel
var body: some View {
List {
ForEach(modelData.fieldViewModels) {
SampleRow(dataSource: $0)
.environmentObject(modelData)
.buttonStyle(PlainButtonStyle())
}
}
}
}
import SwiftUI
struct SampleRow: View {
var dataSource: LocalizedNumberFieldViewModel
/// textfield endEditing 일 때 상태 받는 곳
@State private var endEditing: Bool = false
private var title: String {
return "🌎 from \(dataSource.formatter.fromLocale.identifier) to \(dataSource.formatter.toLocale.identifier)"
}
var body: some View {
VStack {
Spacer()
Text(title)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 20.0, weight: .bold, design: .rounded))
HStack {
Spacer()
// ⭐️ use it!
LocalizedNumberFieldView(dataSource: dataSource, endEditing: $endEditing)
Button(
action: {
hideKeyboard() // extension 참고
},
label: {
Text("Button")
.font(.body)
.fontWeight(.bold)
.padding(5.0)
}
)
.background(Color.yellow)
Spacer()
}
// textfield endEditing되면 변환 결과 보여주기
Text(" result: \(endEditing ? dataSource.result.description : "none")")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 18.0, weight: .light, design: .rounded))
Spacer()
}
}
}
- Button 누르면 Hide Keyboard (Finish Editing)
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
import SwiftUI
struct LocalizedNumberFieldView: View {
@EnvironmentObject var modelData: SampleFieldModel
var dataSource: LocalizedNumberFieldViewModel
/// textfield endEditing 일 때 상태 변환
@Binding var endEditing: Bool
@State private var textfieldBorderColor: UIColor = .systemFill
var body: some View {
VStack {
HStack {
TextField(
dataSource.placeHolder,
text: $modelData.fieldViewModels[dataSource.index].text,
onEditingChanged: { onEditing in
if false == onEditing {
try? validate(text: dataSource.text)
}
endEditing = !onEditing
}
)
.keyboardType(dataSource.keyboardType)
.border(Color(textfieldBorderColor), width: 1)
}
}
}
/// 변환 결과값
private func validate(text: String) throws {
do {
let localizedNumber = try dataSource.formatter.localizedNumberString(from: text, style: .decimal)
setResult(text: localizedNumber, result: .success(from: text, to: localizedNumber))
} catch {
let formatterError = error as? LocalizedNumberFormatterError
setResult(text: text, result: .error(formatterError ?? .unknown))
throw error
}
}
private func setResult(text: String, result: LocalizedNumberFormatterResult) {
dataSource.result = result
textfieldBorderColor = result.isError ? .systemRed : .systemFill
}
}
- 유저가 textfield editing을 끝냈을 때, input 값을
func validate(text: String) throws
를 통해 변환
onEditingChanged The action to perform when the user begins editing text and after the user finishes editing text. The closure receives a Boolean value that indicates the editing status: true when the user begins editing, false when they finish.
- DataSource에 변환결과(
LocalizedNumberFormatterResult
) 업데이트 - SampleRow에서
@State private var endEditing: Bool
이 업데이트 - SampleRow에서 Text로 업데이트
- SwiftUI Tutorials