NEW: Learn to build the incredible iOS 15 Weather app today! >>

SOLVED: Source Of Truth - Bug?

Forums > SwiftUI

Hello

I was looking for an example to limit the length of text I can enter into a TextField. I'm sure this has been covered here but doing so lead me to post this.

The solution I found is shown before. Nicely puts my data in it's own model as a source of truth.

It doesn't work and the comments on the page I found it say that it used to and is now broken in IOS15.

Does anyone know if it should work, did work or is this just wrong.

The debug code does print out a truncated version of the string.

Regards

import SwiftUI

class SomeTextModel: ObservableObject {

    var limit: Int = 5

    @AppStorage("sometext") var someText: String = "" {
        didSet {
            if someText.count > limit {
                print("Too long!")
                someText = String(someText.prefix(limit))
                print(someText)
            }
        }
    }
}

struct ContentView: View {

    @ObservedObject private var someTextMode = SomeTextModel()

    var body: some View {
        TextField("", text: $someTextMode.someText)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .font(.title)
            .keyboardType(.numberPad)
            .padding(10)
    }
}

   

hi Matthew,

i'd offer two comments, neither of which quite directly addresses your question of if it should work, did work or is this just wrong.

  • i'd first replace

    @ObservedObject private var someTextMode = SomeTextModel()

    with

    @StateObject private var someTextMode = SomeTextModel()

    @ObservedObject makes sense only for an object that was created outside the View and passed in to it, rather than one owned by the View. @StateObject, on the other hand, means that the View owns the object.

  • i'm surprised that the @AppStorage usage is allowed in your SomeTextModel; @AppStorage is really designed for use with a View. but you are not really persisting the data ... when the app quits and restarts, you are not reading the persisted value from UserDefaults.

perhaps something like this would work better:

class SomeTextModel: ObservableObject {

    private let limit: Int = 5

    var someText: String = "" {
        didSet {
            objectWillChange.send() // essentially, this turns the someText variable into @Published
            if someText.count > limit {
                print("Too long!")
                someText = String(someText.prefix(limit))
                print(someText)
            }
        }
    }
}

and then worry about the persistence problem (storing the value of someText in UserDefaults, if that's really of interest to you) later.

hope that helps,

DMG

   

Hello

Thanks for that though it doesn't seem to fix my issue.

The @AppStorage seems to work fine in the data model. My gut feeling is that is where it should be. Everything I have read says seperate your view from your data. If the data is an observable object it can and maybe required in several different views so therefore cannot be stored from the view.

I'm going on what I watched in https://developer.apple.com/videos/play/wwdc2020/10040/

I shall keep on trying.

Regards

   

Don't do that work in the view model. The view model should store what the limit and the text are, but the work of actually limiting the input text belongs in the View. Think of it like this: The character limit is a property of the TextField, in the same way the field style, font size, or keyboard type is and you wouldn't store those in the view model. (I would even argue that the limit itself probably belongs in the View rather than the view model if it's a static value that isn't read in from somewhere else.)

Here's one way to do it...

//so we can use our limiting function as a ViewModifier
extension View {
    func limitText(_ text: Binding<String>, to characterLimit: Int) -> some View {
        self
            .onChange(of: text.wrappedValue) { _ in
                text.wrappedValue = String(text.wrappedValue.prefix(characterLimit))
                //we have to wrap in String(...) since prefix returns a Substring
            }
    }
}

class SomeTextModel: ObservableObject {
    var limit = 5

    @AppStorage("sometext") var someText: String = ""
    //don't need the didSet stuff
}

struct ObservableTextView: View {

    @StateObject private var someTextModel = SomeTextModel()
    //should be a StateObject since we're creating it here

    var body: some View {
        TextField("", text: $someTextModel.someText)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .limitText($someTextModel.someText, to: someTextModel.limit)
            //limit our TextField based on the limit in the view model
            .font(.title)
            .keyboardType(.numberPad)
            .padding(10)
    }
}

   

Hello

Thank you for that. Does exacty what I need it to do and I can relate how limiting the text length is treated as just another view modifier and the desired length enacpauslated into the data model too.

Cheers

   

Hacking with Swift is sponsored by Sentry

SPONSORED With Sentry’s error and performance monitoring for iOS, you see mobile vitals that actually matter, can solve any latency issues quickly, and learn how each release is performing over time.

Learn More

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.