I created a #propertyWrapper to limit the number a variable can reach. I tried it in a SwiftUI view with a button that increases the value of the variable and it works, the variable stops at the maximum number set in the initializer. However if I try it with a Textflied it doesn't work, if I insert a higher number than the one set nothing happens, it makes me do it. How can I solve this problem, I know the problem has to do with Binding but I don't know exactly what it is, here is the code:
import SwiftUI
struct ContentView: View {
#Maximum(maximum: 12) var quantity: Int
var body: some View {
NavigationView{
Form{
TextField("", value: $quantity, format: .number, prompt: Text("Pizza").foregroundColor(.red))
Button {
quantity += 1
} label: {
Text("\(quantity)")
}
}
}
}
}
#propertyWrapper
struct Maximum<T: Comparable> where T: Numeric {
#State private var number: T = 0
var max: T
var wrappedValue: T {
get { number }
nonmutating set { number = min(newValue, max) }
}
var projectedValue: Binding<T> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
init(maximum: T){
max = maximum
}
}
extension Maximum: DynamicProperty {
}
#Asperi is exactly right as to how to create the property wrapper. However, that does not solve the problem of the TextField(). The issue seems to be that You are not actually using $quantity on your TextField, but rather are using a string from the formatter that is derived from $quantity. That just doesn't seem to allow the update mechanism to work properly.
However, you can fix this simply by feeding a #State string into the TextField, and then updating everything in an .onChange(of:). This allows you to set quantity to the Int value of the TextField, and the maximum prevents the quantity from going too high. You then turn around and set your string to the quantity.description to keep everything in sync.
One last thing, I changed the keyboardType to .decimalPad to make inputting easier.
struct ContentView: View {
#Maximum(maximum: 12) var quantity: Int
#State private var qtyString = "0"
var body: some View {
NavigationView{
Form{
TextField("", text: $qtyString, prompt: Text("Pizza").foregroundColor(.red))
.onChange(of: qtyString) { newValue in
if let newInt = Int(newValue) {
quantity = newInt
qtyString = quantity.description
}
}
.keyboardType(.decimalPad)
Button {
quantity += 1
} label: {
Text("\(quantity)")
}
}
}
}
}
#propertyWrapper
struct Maximum<T: Comparable>: DynamicProperty where T: Numeric {
let number: State<T> = State(initialValue: 0)
var max: T
var wrappedValue: T {
get { number.wrappedValue }
nonmutating set { number.wrappedValue = min(newValue, max) }
}
var projectedValue: Binding<T> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
init(maximum: T){
max = maximum
}
}
Related
Overview
I have a #StateObject that is being used to drive a NavigationSplitView.
I am using the #Published properties on the #StateObject to drive the selection of the view. #StateObject + #Published
When I get to the final selection of the NavigationSplitView I want to create a Selection.
This should publish a property only once.
import SwiftUI
import Combine
struct Consumption: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Frequency: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Selection: Identifiable {
var id: UUID = UUID()
var consumption: Consumption
var frequency: Frequency
}
class SomeModel: ObservableObject {
let consump: [Consumption] = ["Coffee","Tea","Beer"].map { str in Consumption(name: str)}
let freq: [Frequency] = ["daily","weekly","monthly"].map { str in Frequency(name: str)}
#Published var consumption: Consumption?
#Published var frequency: Frequency?
#Published var selection: Selection?
private var cancellable = Set<AnyCancellable>()
init(consumption: Consumption? = nil, frequency: Frequency? = nil, selection: Selection? = nil) {
self.consumption = consumption
self.frequency = frequency
self.selection = selection
$frequency
.print()
.sink { newValue in
if let newValue = newValue {
print("THIS SHOULD APPEAR ONCE")
print("---------------")
self.selection = .init(consumption: self.consumption!, frequency: newValue)
} else {
print("NOTHING")
print("---------------")
}
}.store(in: &cancellable)
}
}
Problem
The #StateObject on the view is publishing the property twice. (See code below)
struct ContentView: View {
#StateObject var model: SomeModel = .init()
var body: some View {
NavigationSplitView {
VStack {
List(model.consump, selection: $model.consumption) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "circle.fill")
}
}
}
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
List(model.freq, id:\.id, selection: $model.frequency) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
}
} detail: {
switch model.selection {
case .none:
Text("nothing selected")
case .some(let selection):
VStack {
Text(selection.consumption.name)
Text(selection.frequency.name)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
My Solution
If I change the View by creating #State properties on the view and then use OnChange modifier to update the model I get the property to update once. Which is what I want. (see code below)
struct ContentView: View {
#StateObject var model: SomeModel = .init()
#State private var consumption: Consumption?
#State private var frequency: Frequency?
var body: some View {
NavigationSplitView {
VStack {
List(model.consump, selection: $consumption) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "circle.fill")
}
}
}.onChange(of: consumption) { newValue in
self.model.consumption = newValue
}
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
List(model.freq, id:\.id, selection: $frequency) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
.onChange(of: frequency) { newValue in
self.model.frequency = newValue
}
}
} detail: {
switch model.selection {
case .none:
Text("nothing selected")
case .some(let selection):
VStack {
Text(selection.consumption.name)
Text(selection.frequency.name)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Question(s) / Help
Is the behaviour of #Binding different between #StateObject + #Published vs View + #State?
I don't understand why in my previous View the property was being published twice.
My solution is a bit more work but it works by using #State and onChange view modifiers.
Well I canĀ“t completely explain why this happens. It seems that List updates its selection twice.
Why? Unknown. ?May be a bug?.
Consider this debugging approach:
} content: {
switch model.consumption {
case .none:
Text("nothing")
case .some(let consumption):
// Create a custom binding and connect it to the viewmodel
let binding = Binding<Frequency?> {
model.frequency
} set: { frequency, transaction in
model.frequency = frequency
// this will print after the List selected a new Value
print("List set frequency")
}
// use the custom binding here
List(model.freq, id:\.id, selection: binding) { item in
NavigationLink(item.name, value: item)
}
.navigationTitle(consumption.name)
}
This will produce the following output:
receive subscription: (PublishedSubject)
request unlimited
receive value: (nil)
NOTHING
---------------
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily")))
THIS SHOULD APPEAR ONCE
---------------
List set frequency
receive value: (Optional(__lldb_expr_7.Frequency(id: D9552E6A-71FE-407A-90F5-87C39FE24193, name: "daily")))
THIS SHOULD APPEAR ONCE
---------------
List set frequency
The reason you are not seeing this while using #State and .onChange is the .onChange modifier. It fires only if the value changes which it does not. On the other hand your Combine publisher fires every time no matter the value changed or not.
Solution:
You can keep your Combine approach and use the .removeDuplicates modifier.
$frequency
.removeDuplicates()
.print()
.sink { newValue in
This will ensure the sink will only get called when new values are sent.
I am trying to open a sheet when tapping on an item. I followed this questions Sheet inside ForEach doesn't loop over items SwiftUI answer. I get this Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions, I don't understand what is causing it. I tried multiple solutions and they all lead to the same Error.
#State var selectedSong: Int? = nil
#State var songList: [AlbumSong] = []
VStack {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = index
}
}
}
}
.sheet(item: self.$selectedSong) { selectedMovie in
SongPickerEdit(songList: $songList, songIndex: selectedMovie)
I also tried setting songIndex to being an AlbumSong and then implemented this sheet:
.sheet(item: self.$selectedSong) {
SongPickerEdit(songList: $songList, songIndex: self.songList[$0])
}
struct SongPickerEdit: View {
#Binding var songList: [AlbumSong]
#State var songIndex: Int?
var body: some View {
}
}
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
How about making selectedSong an AlbumSong?? The item: parameter needs to be an Identifiable binding, but Int is not Identifiable.
#State var selectedSong: AlbumSong? = nil
#State var songList: [AlbumSong] = []
var body: some View {
List {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = song
}
}
}.sheet(item: $selectedSong) { song in
SongPickerEdit(songList: $songList, song: song)
}
}
Note that SongPickerEdit would look like this:
struct SongPickerEdit: View {
#State var song: AlbumSong
var body: some View {
Text("\(song.title), \(song.artist)")
}
}
If you really need the index for some reason, you can add the song list binding back in and use songList.index { $0.id == song.id } to find the index if the list is not too long.
Otherwise, you can make your own Identifiable type SongAndIndex that uses the same id as AlbumSong, but with an extra index property, and use that as the type of selectedSong.
A third way would be to use the sheet(isPresented:) overload, but this way you end up with 2 sources of truth:
#State var selectedSongIndex: Int? = nil {
didSet {
if selectedSongIndex != nil {
isSheetPresented = true
}
}
}
#State var isSheetPresented: Bool = false {
didSet {
if !isSheetPresented {
selectedSongIndex = nil
}
}
}
...
}.sheet(isPresented: $isSheetPresented) {
SongPickerEdit(songList: $songList, songIndex: selectedSongIndex)
}
selectedSongIndex also won't be set to nil when the user dismisses the sheet.
Essentially, I'm nesting #Binding 3 layers deep.
struct LayerOne: View {
#State private var doubleValue = 0.0
var body: some View {
LayerTwo(doubleValue: $doubleValue)
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: $doubleValue)
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue) // Only this print gets run when doubleValue is updated from this struct
}
}
var body: Some view {
// Button here changes doubleValue
}
}
Whichever struct I change doubleValue in is the one where the didSet will get run, so for example if I change it in LayerThree only that one will print, none of the others will.
I am able to watch for changes with .onChange(of: doubleValue) which will then get run when it changes but it's not making sense to me why didSet won't get run except on the struct where it's changed from.
Is #Binding struct specific?
Using property observers like didSet on values wrapped in PropertyWrappers will not have the "normal" effect because the value is being set inside the wrapper.
In SwiftUI, if you want to trigger an action when a value changes, you should use the onChange(of:perform:) modifier.
struct LayerTwo: View {
#Binding var doubleValue: Double
var body: some View {
LayerThree(doubleValue: $doubleValue)
.onChange(of: doubleValue) { newValue
print(newValue)
}
}
}
To see why this happens, we can unveil the syntactic sugar of property wrappers. #Binding var doubleValue: Double translates to:
private var _doubleValue: Binding<Double>
var doubleValue: Double {
get { _doubleValue.wrappedValue }
set { _doubleValue.wrappedValue = newValue }
}
init(doubleValue: Binding<Double>) {
_doubleValue = doubleValue
}
Whatever you do in didSet will be put after the line _doubleValue.wrappedValue = newValue. It should be very obvious why when you update doubleValue in layer 3, The didSet of doubleValue in layer 2 or 1 doesn't get called. They are simply different computed properties!
swiftPunk's solution works by creating a new binding whose setter sets the struct's doubleValue, hence calling didSet:
Binding(get: { return doubleValue },
set: { newValue in doubleValue = newValue }
// ^^^^^^^^^^^^^^^^^^^^^^
// this will call didSet in the current layer
Now all working:
struct ContentView: View {
var body: some View {
LayerOne()
}
}
struct LayerOne: View {
#State private var doubleValue:Double = 0.0 {
didSet {
print("LayerOne:", doubleValue)
}
}
var body: some View {
LayerTwo(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print("LayerTwo:", doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print("LayerThree:", doubleValue)
}
}
var body: some View {
Text(String(describing: doubleValue))
Button("update value") {
doubleValue = Double.random(in: 0.0...100.0)
}
.padding()
}
}
print results:
LayerOne: 64.58963263686678
LayerTwo: 64.58963263686678
LayerThree: 64.58963263686678
Xcode 12. code and lifecycle are Swiftui.
I've looked at other peoples related questions and it seems like they just dump their entire projects code on here; it's a bit overwhelming to pick what I need from it.
So, I've broken the issue down into the simplest example I can.
The goal is to have numberX iterate when I press the + button.
Thanks in advance!
struct InfoData: Identifiable {
var id = UUID()
var nameX: String
var numberX: Int
}
class ContentX: ObservableObject {
#Published var infoX = [
InfoData(nameX: "Example", numberX: 1)
]
}
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List(contentX.infoX) { item in
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
item.numberX += 1 //Eror shows up here <<
}
Text("\(item.numberX)")
}
}
}
}
In the syntax that you're using, item is an immutable value, as the error tells you. You can't mutate it, because it doesn't represent a true connection to the array it comes from -- it's just a temporary readable copy that is being used in the List iteration.
If you can upgrade to Xcode 13, you have access to something called element binding syntax in List and ForEach that lets you do this:
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List($contentX.infoX) { $item in //<-- Here
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
item.numberX += 1
}
Text("\(item.numberX)")
}
}
}
}
This gives you a Binding to item that is mutable, allowing you to change its value(s) and have them reflected in the original array.
Prior to Xcode 13/Swift 5.5, you'd have to define your own way to alter an element in the array. This is one solution:
class ContentX: ObservableObject {
#Published var infoX = [
InfoData(nameX: "Example", numberX: 1)
]
func alterItem(item: InfoData) {
self.infoX = self.infoX.map { $0.id == item.id ? item : $0 }
}
}
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List(contentX.infoX) { item in
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
var newItem = item
newItem.numberX += 1
contentX.alterItem(item: newItem)
}
Text("\(item.numberX)")
}
}
}
}
Or, another option where a custom Binding is used:
class ContentX: ObservableObject {
#Published var infoX = [
InfoData(nameX: "Example", numberX: 1)
]
func bindingForItem(item: InfoData) -> Binding<InfoData> {
.init {
self.infoX.first { $0.id == item.id }!
} set: { newValue in
self.infoX = self.infoX.map { $0.id == item.id ? newValue : $0 }
}
}
}
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List(contentX.infoX) { item in
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
contentX.bindingForItem(item: item).wrappedValue.numberX += 1
}
Text("\(item.numberX)")
}
}
}
}
I'm trying to have the user input two numbers and then have those numbers be displayed and also added together. At the moment in order for the state variable to be updated you have the press return. Is there a way to have the state update like it does with text? I also have had the code inputed as a string but haven't been able to convert that to int so the numbers can be added together correctly. If anyone knows how to have it convert properly I would appreciate all the help I can get.
struct ContentView: View {
#State private var numOne: Int = 0
#State private var numTwo: Int = 0
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Number One", value: $numOne, formatter: NumberFormatter())
.keyboardType(.numberPad)
TextField("Number Two", value: $numTwo, formatter: NumberFormatter())
.keyboardType(.numberPad)
}
NavigationLink(
destination: addedView(numOne: $numOne, numTwo: $numTwo),
label: {
Text("Navigate")
}
)
}
}
}
}
}
struct addedView: View {
#Binding var numOne: Int
#Binding var numTwo: Int
#State private var added: Int = 0
var body: some View {
VStack {
Text("\(numOne)")
Text("\(numTwo)")
}
}
}
This shows how to enforce the TextFields be integers, and how to add them together for a result.
The main difference is that the formatter's numberStyle is .decimal. This means you will only get whole numbers / integers.
The result is added while they are Ints, so the numbers are added. If you add when they are both Strings, they will concatenate together. E.g. you want 5 + 10 to be 15, not 510.
You can then pass the result to a child view or NavigationLink if you wish.
struct ContentView: View {
#State private var numOne: Int = 0
#State private var numTwo: Int = 0
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
private var result: Int {
numOne + numTwo
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Number One", value: $numOne, formatter: Self.formatter)
.keyboardType(.numberPad)
TextField("Number Two", value: $numTwo, formatter: Self.formatter)
.keyboardType(.numberPad)
}
Section {
Text("Result: \(result)")
}
}
}
}
}
}
Note that the NumberFormatter is created statically, and only once. This is because it is quite expensive to create them, especially twice every render.
This uses Combine to make sure that the text entered is a digit, and then changes it to an Int to use in computations. The return value is a closure that you can use to
struct EditableInt : View {
var intString: String = ""
var onChanged: (Int) -> Void
init(_ int: Int?, onChanged: #escaping (Int) -> Void) {
if let int = int,
let formattedInt = Formatter.numberFormatter.string(from: int as NSNumber){
self.intString = formattedInt
} else {
intString = ""
}
self.onChanged = onChanged
}
#State private var editableInt: String = ""
var body: some View {
TextField(" " + intString + " ", text: $editableInt)
.onReceive(Just(editableInt)) { newValue in
let editableInt = newValue.filter { "0123456789".contains($0) }
if editableInt != intString {
if let returnInt = Int(editableInt) {
onChanged(returnInt)
}
}
}
.onAppear { self.editableInt = self.intString }
}
}
The above is reusable, and is called from another view like this:
struct OtherView: View {
#State var otherViewInt = 0
var body: some View {
EditableInt(otherViewInt) { newInt in
otherViewInt = newInt
}
}
}