BLACK FRIDAY SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: How to edit Textfield from the ViewModel, @Publisher?

Forums > SwiftUI

Good afternoon everyone. Guys, tell me how it is possible to edit Еextfield, for example, "date of birth"? for example, to apply a "dataSeparator" or other modifier?

import SwiftUI

struct Tourist {
    var firstName: String
    var lastName: String
    var dateBirth: String
    var citizenship: String
    var numberPassport: String
    var validityPeriodPassport: String
}

extension String {

    func dataSeparator() -> String {
        var currentText = self
        let character = "/" as Character

        if self.count > 2 {
            currentText.insert(
                character, at:
                    currentText.index(currentText.startIndex, offsetBy: 2)
            )
        }
        return currentText
    }
}

class CardTouristViewModel: ObservableObject {

    @Published var tourist: Tourist = Tourist(firstName: "",
                                              lastName: "",
                                              dateBirth: "".dataSeparator(),
                                              citizenship: "",
                                              numberPassport: "",
                                              validityPeriodPassport: "")

}

struct CardTourist: View {

    @ObservedObject var turistVM: CardTouristViewModel = CardTouristViewModel()

    var title: String = ""

    @State var isShow: Bool = false

    var body: some View {
        VStack(spacing: 0) {
            ShowCardTourist(isShow: $isShow, title: title, action: {
                DispatchQueue.main.async {
                    withAnimation(.easeInOut) {
                        self.isShow.toggle()
                        print(turistVM.tourist)
                    }
                }
            })
            .padding(.bottom, isShow ? 17 : 0)
            if isShow {
                VStack(spacing: 8) {
                    TextFieldForTouristWithPlaceholder(textField: $turistVM.tourist.firstName,
                                                       title: "Имя")
                    TextFieldForTouristWithPlaceholder(textField: $turistVM.tourist.lastName,
                                                       title: "Фамилия")
                    TextFieldForTourist(textField: $turistVM.tourist.dateBirth,
                                        title: "Дата рождения")
                    TextFieldForTourist(textField: $turistVM.tourist.citizenship,
                                        title: "Гражданство")
                    TextFieldForTourist(textField: $turistVM.tourist.numberPassport,
                                        title: "Номер загранпаспорта")
                    TextFieldForTourist(textField: $turistVM.tourist.validityPeriodPassport,
                                        title: "Срок действия загранпаспорта")
                }
            }
        }
        .padding(.horizontal, 13)
        .vLeading()
        .padding(.vertical, 16)
        .background(Color.white)
        .cornerRadius(15)
    }
}

struct ShowCardTourist: View {

    @Binding var isShow: Bool

    var title: String = ""
    var action: () -> Void

    var body: some View {
        HStack(spacing: 0) {
            Text(title)
                .modifier(HeightModifier(size: 22, lineHeight: 120, weight: .medium))
                .foregroundColor(.black)
                .frame(maxWidth: .infinity, alignment: .leading)
            Button(action: action) {
                Image(systemName: "chevron.up")
                    .rotationEffect(.degrees(isShow ? 0 : 180))
            }
            .font(Font.system(size: 16))
            .frame(width: 32, height: 32)
            .foregroundColor(.c_0D72FF)
            .background(Color.c_0D72FF_10)
            .cornerRadius(6)
        }
    }
}

struct TextFieldForTouristWithPlaceholder: View {

    @Binding var textField: String
    @State var isValid: Bool = false

    let title: String

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(title)
                .modifier(HeightModifier(size: 12, lineHeight: 120, weight: .regular))
                .tracking(0.1)
                .foregroundColor(.c_A9ABB7)

            TextField("", text: $textField)
                .modifier(HeightModifier(size: 16, lineHeight: 110, weight: .regular))
                .tracking(0.075)
                .foregroundColor(.c_14142B)
                .textContentType(.name)
                .tint(Color.black)
        }
        .padding(.horizontal, 16)
        .vLeading()
        .padding(.vertical, 10)
        .background(isValid ? Color.c_EB5757_15 : Color.c_F6F6F9)
        .cornerRadius(10)
    }

}

struct TextFieldForTourist: View {

    @Binding var textField: String
    @State var isValid: Bool = false

    let title: String

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            TextField(title, text: $textField)
                .modifier(HeightModifier(size: 17,
                                         lineHeight: 110,
                                         weight: .regular))
                .tracking(0.1)
                .foregroundColor(.c_14142B)
                .tint(.black)
        }
        .vLeadingAndBack(isValid)
    }

}

extension View {
    func vLeading() -> some View {
        self.frame(maxWidth: .infinity, alignment: .leading)
    }

    func vLeadingAndBack(_ isColor: Bool) -> some View {
        self
        .padding(.horizontal, 16)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.vertical, 16)
        .background(isColor ? Color.c_EB5757_15 : Color.c_F6F6F9)
        .cornerRadius(10)
    }

    func solidBlackground() -> some View {
        self
        .padding(.horizontal, 16)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.vertical, 16)
        .background(Color.c_F6F6F9)
        .cornerRadius(15)
    }
}

#if DEBUG
struct CardTourist_Previews: PreviewProvider {
    static var previews: some View {
        CardTourist()
    }
}
#endif

   

I only came up with the idea of adding via onChange but that’s not good 🙄 I don’t want to screw logic into View.

.onChange(of: turistVM.tourist.dateBirth) { elem in
      let assambly = elem.dataSeparator()
      self.turistVM.tourist.dateBirth = assambly
      print("111 \(assambly)")
}

   

Hi Steven! Did you try to use computed property for that purpose?

Something like:


struct Tourist {
  var lastName: String
  var dateBirth: String
  var citizenship: String
  var numberPassport: String
  var validityPeriodPassport: String

  var dateBirthFormatted: String {
      dateBirth.dataSeparator()
  }

}

and then use dateBirth for binding to textField but dateBirthFormatted for views to use/display it.

But maybe it is much better idea to use Date type instead of String... Then you will have much wider options for displaying data and besides no need for check how the date was entered, and on top you can just bind it to DatePicker and not TextField...

   

You may also try to use propertyWrappers like so. I guess you wanted to implement something like this in textfield. Just copy paste and have a look.

@propertyWrapper
struct BirthDateFormatted {
    private var birthDate: String

    init(wrappedValue: String) {
        birthDate = wrappedValue
    }

    var wrappedValue: String {
        get { birthDate }

        set(value) {
            birthDate = value
            /// Inserting a slash in the third position to differentiate between day and month
            if value.count == 3 {
                let startIndex = value.startIndex
                let thirdPosition = value.index(startIndex, offsetBy: 2)
                let thirdPositionChar = value[thirdPosition]
                if thirdPositionChar != "/" {
                    birthDate.insert("/", at: thirdPosition)
                }
            }

            /// Inserting a slash in the sixth position to differentiate between month and year
            if value.count == 6 {
                let startIndex = value.startIndex
                let sixthPosition = value.index(startIndex, offsetBy: 5)
                let sixthPositionChar = value[sixthPosition]
                if sixthPositionChar != "/" {
                    birthDate.insert("/", at: sixthPosition)
                }
            }

            /// Removing / when going back
            if value.last == "/" {
                birthDate.removeLast()
            }

            /// Limiting string, including two slashes (8 + 2 = 10)
            birthDate = String(birthDate.prefix(10))
        }
    }
}

struct Person {
    @BirthDateFormatted var birthDate: String
}

class PersonViewModel: ObservableObject {
    @Published var person = Person(birthDate: "")
}

struct BirthDateView: View {
    @StateObject var vm = PersonViewModel()

    var body: some View {
        VStack {
            TextField("DD/MM/YYYY", text: $vm.person.birthDate)
        }
        .padding()
    }
}

1      

Good afternoon Mr. @ygeras. made a separator this way, I stole the solution from SwiftBook.

    func dataSeparator() -> String {
            let maxNumberCount = 8
            var number: String = ""
            do {
                let regex = try NSRegularExpression(pattern: "\\D", options: .caseInsensitive)
                let range = NSRange(location: 0, length: self.utf8.count)
                number = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
                if number.count > maxNumberCount {
                    let maxIndex = number.index(number.startIndex, offsetBy: maxNumberCount)
                    number = String(number[number.startIndex..<maxIndex])
                }
                let maxIndex = number.index(number.startIndex, offsetBy: number.count)
                let regRange = number.startIndex..<maxIndex

                var pattern = ""
                var target = ""

                if number.count < 4 {
                    pattern = "(\\d{2})(\\d+)"
                    target = "$1.$2"
                } else if number.count < 5 {
                    pattern = "(\\d{2})(\\d{2})"
                    target = "$1.$2"
                } else if number.count < 6 {
                    pattern = "(\\d{2})(\\d{2})(\\d+)"
                    target = "$1.$2.$3"
                } else {
                    pattern = "(\\d{2})(\\d{2})(\\d+)"
                    target = "$1.$2.$3"
                }
                number = number.replacingOccurrences(of: pattern, with: target, options: .regularExpression, range: regRange)
            } catch {
                return "Error"
            }
            return number
    }

   

Thanks for your help Mr. @ygeras I didn’t know about @propertyWrapper. it's great to learn new things 🧐

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate Black Friday, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.