Hey there) I recently created a Phone Number Input View. To create a List of countries, I used json file.
Okay first of all create file CountryNumbers.json, full file you can find here
[{
"id": "0232",
"name": "Ukraine",
"flag": "πΊπ¦",
"code": "UA",
"dial_code": "+380",
"pattern": "## ### ## ##",
"limit": 17
}, {
"id": "0234",
"name": "United Kingdom",
"flag": "π¬π§",
"code": "GB",
"dial_code": "+44",
"pattern": "## #### ####",
"limit": 17
}, {
"id": "0235",
"name": "United States",
"flag": "πΊπΈ",
"code": "US",
"dial_code": "+1",
"pattern": "### ### ####",
"limit": 17
}]
Next we need to define a CountryNumbers structure that can be loaded from JSON file. Create a new Swift file called CPData.swift, then give it this code:
import Foundation
struct CPData: Codable, Identifiable {
let id: String
let name: String
let flag: String
let code: String
let dial_code: String
let pattern: String
let limit: Int
static let allCountry: [CPData] = Bundle.main.decode("CountryNumbers.json")
static let example = allCountry[0]
}
For more understanding visit hackingwithswift.com
Now we need to load an array of codes and flags from JSON stored in app bundle, for that create new Swift file called Decoder.swift, then give it this code:
import Foundation
extension Bundle {
func decode<T: Decodable>(_ file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
And we are ready to make View. To do this, create a new Swift file named PhoneNumberView.swift and generate the code:
import SwiftUI
import Combine
struct PhoneNumberView: View {
@State var presentSheet = false
@State var countryCode : String = "+1"
@State var countryFlag : String = "πΊπΈ"
@State var countryPattern : String = "### ### ####"
@State var countryLimit : Int = 17
@State var mobPhoneNumber = ""
@State private var searchCountry: String = ""
@Environment(\.colorScheme) var colorScheme
@FocusState private var keyIsFocused: Bool
let counrties: [CPData] = Bundle.main.decode("CountryNumbers.json")
var body: some View {
GeometryReader { geo in
let hasHomeIndicator = geo.safeAreaInsets.bottom > 0
NavigationStack {
VStack {
Text("Confirm country code and enter phone number")
.multilineTextAlignment(.center)
.font(.title).bold()
.padding(.top, hasHomeIndicator ? 70 : 20)
HStack {
Button {
presentSheet = true
keyIsFocused = false
} label: {
Text("\(countryFlag) \(countryCode)")
.padding(10)
.frame(minWidth: 80, minHeight: 47)
.background(backgroundColor, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.foregroundColor(foregroundColor)
}
TextField("", text: $mobPhoneNumber)
.placeholder(when: mobPhoneNumber.isEmpty) {
Text("Phone number")
.foregroundColor(.secondary)
}
.focused($keyIsFocused)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(mobPhoneNumber)) { _ in
applyPatternOnNumbers(&mobPhoneNumber, pattern: countryPattern, replacementCharacter: "#")
}
.padding(10)
.frame(minWidth: 80, minHeight: 47)
.background(backgroundColor, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
}
.padding(.top, 20)
.padding(.bottom, 15)
Button {
// Move to the next step
} label: {
Text("Next")
}
.disableWithOpacity(mobPhoneNumber.count < 1)
.buttonStyle(OnboardingButtonStyle())
}
.animation(.easeInOut(duration: 0.6), value: keyIsFocused)
.padding(.horizontal)
Spacer()
}
.onTapGesture {
hideKeyboard()
}
.sheet(isPresented: $presentSheet) {
NavigationView {
List(filteredResorts) { country in
HStack {
Text(country.flag)
Text(country.name)
.font(.headline)
Spacer()
Text(country.dial_code)
.foregroundColor(.secondary)
}
.onTapGesture {
self.countryFlag = country.flag
self.countryCode = country.dial_code
self.countryPattern = country.pattern
self.countryLimit = country.limit
presentSheet = false
searchCountry = ""
}
}
.listStyle(.plain)
.searchable(text: $searchCountry, prompt: "Your country")
}
.presentationDetents([.medium, .large])
}
.presentationDetents([.medium, .large])
}
.ignoresSafeArea(.keyboard)
}
var filteredResorts: [CPData] {
if searchCountry.isEmpty {
return counrties
} else {
return counrties.filter { $0.name.contains(searchCountry) }
}
}
var foregroundColor: Color {
if colorScheme == .dark {
return Color(.white)
} else {
return Color(.black)
}
}
var backgroundColor: Color {
if colorScheme == .dark {
return Color(.systemGray5)
} else {
return Color(.systemGray6)
}
}
func applyPatternOnNumbers(_ stringvar: inout String, pattern: String, replacementCharacter: Character) {
var pureNumber = stringvar.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
for index in 0 ..< pattern.count {
guard index < pureNumber.count else {
stringvar = pureNumber
return
}
let stringIndex = String.Index(utf16Offset: index, in: pattern)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacementCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
stringvar = pureNumber
}
}
extension View {
func placeholder<Content: View>(
when shouldShow: Bool,
alignment: Alignment = .leading,
@ViewBuilder placeholder: () -> Content) -> some View {
ZStack(alignment: alignment) {
placeholder().opacity(shouldShow ? 1 : 0)
self
}
}
}
extension View {
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
}
extension View {
func disableWithOpacity(_ condition: Bool) -> some View {
self
.disabled(condition)
.opacity(condition ? 0.6 : 1)
}
}
struct OnboardingButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous )
.frame(height: 49)
.foregroundColor(Color(.systemBlue))
configuration.label
.fontWeight(.semibold)
.foregroundColor(Color(.white))
}
}
}
struct PhoneNumberView_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberView(countryCode: "+1", countryFlag: "πΊπΈ", countryPattern: "### ### ####", countryLimit: 17)
}
}
Hope itβs was useful. Thanks for reading.