iOS database swiftUI M1 wrap up challenge

Hello!

Could somebody explain me this code:

TextField("Number of Pages", text: $bookPages)
            // Filter the input for only numbers
                .keyboardType(.numberPad)
                .onReceive(Just(bookPages)) { newValue in
                    let filtered = newValue.filter { "0123456789".contains($0) }
                    if filtered != newValue {
                        self.bookPages = filtered
                    }
                }

Thanks

@takipeti90

Hi Peter,

The code essentially only accepts numeric input from the keyboard.

You would think that setting the keyboard type to .numberPad would be all that you need to do but an iPad does not have an exclusively numeric keypad.

To prevent any other characters from being entered, the additional code in .onRecieve has been included.

.onReceive monitors a Published item and in this case bookPages is wrapped with Just which is a Combine publisher which emits (broadcasts) the contents of a property.

newValue is the current contents of the TextField as entered by the user which may include non numeric characters.

The line of code: let filtered = newValue.filter { "0123456789".contains($0) } is doing quite a bit of work.
newValue.filter iterates through the contents of newValue.
The trailing closure: { "0123456789".contains($0)
is saying, "for each iterated value (denoted by $0), does that character exist in the string ‘0123456789’. If it does then add it to filtered".

The test: if filtered != newValue { is saying if the filtered characters does not equal the characters the user entered then set bookPages to be the filtered value (cleaned).

Hope that helps.

2 Likes

Thank you for the detailed explanation.

The only thing what is not totally clear for me is that, the .onReceive will run after every single character (and do the filtering on that character) or only after I entered all number and clicked somewhere else on the screen.

Why does the code need this Just()? What does it do exactly? Could the code work without it?

Thanks

The property bookPages is a State object so what Just (which is a structure from the Combine framework) does is turn it into a Published property so that .onReceive will fire. As I said in my previous reply, .onReceive requires a Published property to react or take action.

Each time you type a character Just detects a change to the property and “emits”. This might all be a bit hard to get your head around but so is a lot of things related to Combine.

Run the following code in a test SwiftUI project to observe the output where the Text view says "Contents of bookPages: " You will see that every character is checked and anything non numeric is rejected. I deliberately added a delay so that you can see that the non numeric characters typed in are initially visible but the filter process removes them. Without that delay you don’t see that you have typed a non numeric character because it is removed as fast as you type it.

import Combine
import SwiftUI

struct ContentView: View {
    @State private var bookPages: String = ""

    var body: some View {
        VStack(spacing: 40) {
            TextField("Number of Pages", text: $bookPages)
            // Filter the input for only numbers
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .keyboardType(.numberPad)
                .onReceive(Just(bookPages)) { newValue in
                    let filtered = newValue.filter { "0123456789".contains($0) }
                    if filtered != newValue {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                            self.bookPages = filtered
                        }
                    }
                }
                .padding(.horizontal, 20)

            Text("Contents of bookPages: \(bookPages)")
        }
    }
}

Exactly the same thing could have been achieved by using .onChange() rather than .onReceive() but you would not get some exposure to using Combine which is always worth while.

import SwiftUI

struct ContentView: View {
    @State private var bookPages: String = ""

    var body: some View {
        VStack(spacing: 40) {
            TextField("Number of Pages", text: $bookPages)
            // Filter the input for only numbers
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .keyboardType(.numberPad)
                .onChange(of: bookPages, perform: { newValue in
                    let filtered = newValue.filter { "0123456789".contains($0) }
                    if filtered != newValue {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                            self.bookPages = filtered
                        }
                    }
                })
                .padding(.horizontal, 20)

            Text("Contents of bookPages: \(bookPages)")
        }
    }
}

Thank you, now I understand more or less :slight_smile:

Do you have tutorial or video or something about this Combine framework?

Chris Ching doesn’t have any Combine videos but there are plenty of resources if you search online. I bought a book “Practical Combine” from Donny Wals. Haven’t even got around to reading it as yet and I bought it months ago. Life is like that sometimes.