How to subclass the #State property wrapper in SwiftUI - swift

I have a #State variable that I that I want to add a certain constraint to, like this simplified example:
#State private var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
However this doesn't look so nice (it seems to be working though) but what I really want to do is to subclass or extend the property wrapper #State somehow so I can add this constraint in it's setter. But I don't know how to do that. Is it even possible?

You can't subclass #State since #State is a Struct. You are trying to manipulate your model, so you shouldn't put this logic in your view. You should at least rely on your view model this way:
class ContentViewModel: ObservableObject {
#Published var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(contentViewModel.positiveInt)")
Button(action: {
self.contentViewModel.positiveInt = -98
}, label: {
Text("TAP ME!")
})
}
}
}
But since SwiftuUI is not an event-driven framework (it's all about data, model, binding and so forth) we should get used not to react to events, but instead design our view to be "always consistent with the model". In your example and in my answer here above we are reacting to the integer changing overriding its value and forcing the view to be created again. A better solution might be something like:
class ContentViewModel: ObservableObject {
#Published var number = 0
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
private var positiveInt: Int {
contentViewModel.number < 0 ? 0 : contentViewModel.number
}
var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.contentViewModel.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}
Or even simpler (since basically there's no more logic):
struct ContentView: View {
#State private var number = 0
private var positiveInt: Int {
number < 0 ? 0 : number
}
var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}

You can't apply multiple propertyWrappers, but you can use 2 separate wrapped values. Start with creating one that clamps values to a Range:
#propertyWrapper
struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}
Next, create an ObservableObject as your backing store:
class Model: ObservableObject {
#Published
var positiveValue: Int = 0
#Clamping(0...(.max))
var clampedValue: Int = 0 {
didSet { positiveValue = clampedValue }
}
}
Now you can use this in your content view:
#ObservedObject var model: Model = .init()
var body: some View {
Text("\(self.model.positiveValue)")
.padding()
.onTapGesture {
self.model.clampedValue += 1
}
}

Related

Dynamically sized #State var

I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a #State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.
As long as this #State var has enough empty spaces everything works fine. But my question is, how do I modify this #State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.
//Structs used in this example
struct MainViewState {
var itemStructures: [ItemStructure]
}
struct ItemStructure: Identifiable, Hashable {
var id: String {name}
var name: String
var parameters: [Parameter]
}
struct Parameter: Identifiable, Hashable {
var id: String {name}
var name: String
var value: Double
var range: [Double]
}
struct ContentView: View {
//In this model json is loaded, this seemed out of scope for this question to include this
#ObservedObject var viewModel: MainViewModel
//This is the #State var which should be dynamically allocated according to the content size of "itemStructures"
//For now 3 items with 10 parameters each are enough
#State var parametersPerItem: [[Float]] = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0]
]
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
var body: some View {
let itemStructures = viewModel.mainState.itemStructures
ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
Text(item.name)
ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
Text(parameter.name)
SliderView(
label: parameter.name,
value: Binding(
get: { self.parametersPerItem[index][i] },
set: { (newVal) in
self.parametersPerItem[index][i] = newVal
//Function to send slider values and ranges to real time processing
//doStuffWithRangesAndValues()
}
),
range: parameter.range,
showsLabel: false
).onAppear {
//Set initial value slider
parametersPerItem[index][i] = Float(parameter.value)
}
}
}
}
}
struct SliderView: View {
var label: String
#Binding var value: Float
var range: [Double]
var showsLabel: Bool
init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
self.label = label
_value = value
self.range = range
self.showsLabel = showsLabel
}
var body: some View {
GeometryReader { geometry in
ZStack{
if showsLabel { Text(label) }
HStack {
Slider(value: $value)
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * 0.8)
//In the real app range calculations are done here
let valueInRange = value
Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
.foregroundColor(.white)
.font(.subheadline)
.frame(width: geometry.size.width * 0.2)
}
}
}
.frame(height: 40.0)
}
}
If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the #State property an initial value.
extension MainViewState {
var parametersPerItem: [[Float]] {
var array: [[Float]] = []
if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
for _ in itemStructures {
array.append(Array(repeating: 0.0, count: max))
}
}
return array
}
}

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

#State with a #Appstorage property does not update a SwiftUI View

I organized some settings to be stored in UserDefauls in a struct like this, because I want to have them in one place and to have getters and Setters.
enum PrefKeys : String {
case KEY1
case KEY2
var key: String { return self.rawValue.lowercased()}
}
struct Preferences {
#AppStorage(PrefKeys.KEY1.key) private var _pref_string_1 = ""
#AppStorage(PrefKeys.KEY1.key) var pref_string_2 = ""
var pref_string_1: String {
set { _pref_string_1 = newValue.lowercased() }
get { return _pref_string_1.lowercased() }
}
}
using it like this works fine:
struct ContentView: View {
var p = Preferences()
var body: some View {
NavigationView{
VStack(alignment: .leading){
Text("pref_string_1: \(p.pref_string_1)")
Text("pref_string_2: \(p.pref_string_2)")
NavigationLink("Sub",destination: SubView())
}
}
.padding()
}
}
If I use p as a #State var, it does not update the view, when the #State var is changed:
struct SubView: View {
#State var psub = Preferences()
#AppStorage("standalone pref") private var standalonePref = ""
var body: some View {
VStack(alignment: .leading){
Text("Preference1 in struct: \(psub.pref_string_1)")
TextField("Preference1 in struct:", text: $psub.pref_string_1)
Text("standalonePref \(standalonePref)")
TextField("standalonePref:", text: $standalonePref)
}
}
}
How can I fix this?

How to set up Binding of computed value inside class (SwiftUI)

In my Model I have an array of Items, and a computed property proxy which using set{} and get{} to set and return currently selected item inside array and works as shortcut. Setting item's value manually as model.proxy?.value = 10 works, but can't figure out how to Bind this value to a component using $.
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 1), Item(value: 2), Item(value: 3)]
var proxy: Item? {
get {
return items[1]
}
set {
items[1] = newValue!
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
Text("Value: \(model.proxy!.value)")
Button(action: {model.proxy?.value = 123}, label: {Text("123")}) // method 1: this works fine
SubView(value: $model.proxy.value) // method 2: binding won't work
}.padding()
}
}
struct SubView <B:BinaryFloatingPoint> : View {
#Binding var value: B
var body: some View {
Button( action: {value = 100}, label: {Text("1")})
}
}
Is there a way to modify proxy so it would be modifiable and bindable so both methods would be available?
Thanks!
Day 2: Binding
Thanks to George, I have managed to set up Binding, but the desired binding with SubView still won't work. Here is the code:
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 0), Item(value: 0), Item(value: 0)]
var proxy: Binding <Item?> {
Binding <Item?> (
get: { self.items[1] },
set: { self.items[1] = $0! }
)
}
}
struct ContentView: View {
#StateObject var model = Model()
#State var myval: Double = 10
var body: some View {
VStack {
Text("Value: \(model.proxy.wrappedValue!.value)")
Button(action: {model.proxy.wrappedValue?.value = 555}, label: {Text("555")})
SubView(value: model.proxy.value) // this still wont work
}.padding()
}
}
struct SubView <T:BinaryFloatingPoint> : View {
#Binding var value: T
var body: some View {
Button( action: {value = 100}, label: {Text("B 100")})
}
}
Create a Binding instead.
Example:
var proxy: Binding<Item?> {
Binding<Item?>(
get: { items[1] },
set: { items[1] = $0! }
)
}

How to get data from the Model and update it

I have a data model that I want to use its data in various views. So firstly I have problem polling that information from my data model and secondly I have problem to update the data.
Here in one example of my data model with three views.
Data Model
// A basic resocrd of each exercise in a workout
class WorkoutItem:ObservableObject, Identifiable{
var id:Int = 0
var name: String = "An Exercise"
var sets:Int = 3
var reps:Int = 10
func addSet() {
sets += 1
}
init(){
}
}
// The model for holding a workout
class WorkoutModel:ObservableObject{
#Published var workout:[WorkoutItem] = []
var lastID:Int = -1
/// Creates a newID based on the last known ID
private func newId()->Int{
lastID += 1
return lastID
}
func add(name:String, sets:Int, reps:Int){
let newExercise = WorkoutItem()
workout += [newExercise]
}
init() {
add(name: "Psuh Pp", sets: 1, reps: 8)
add(name: "Pull UPs", sets: 1, reps: 10)
}
}
View 1
struct ContentView: View {
#ObservedObject var myWorkout: WorkoutModel
var body: some View {
List(self.myWorkout.workout) { item in
VStack(alignment: .leading) {
VStack(alignment: .leading) {
Text(item.name).font(.headline)
ExerciseEntryView(exItem: item)
}
}
}
}
}
View 2
struct ExerciseEntryView: View {
var exItem: WorkoutItem
var body: some View {
VStack {
VStack(alignment: .leading){
ForEach(0 ..< exItem.sets, id: \.self){ row in
ExerciseEntryView_Row(setNumber: row)
}
}
Button(action: {
self.exItem.addSet()
}) {
Text("Add Set").foregroundColor(Color.red)
}
}
}
}
View 3
struct ExerciseEntryView_Row: View {
var setNumber: Int
var body: some View {
HStack{
Text("Set Number \(setNumber)")
}
}
}
Firstly when running the code, as you can see in the below image the title is still the default value ('An Exercise') while it should Pull up and Push up. Also when I press on add set the set gets updated in the terminal but it does not update the view.
Any idea why this is not functioning?
Thanks in advance.
First you can make your WorkoutItem a struct:
struct WorkoutItem: Identifiable {
var id: Int = 0
var name: String = "An Exercise"
var sets: Int = 3
var reps: Int = 10
mutating func addSet() {
sets += 1
}
}
Then in the WorkoutModel you weren't using setting any properties for the new exercise:
class WorkoutModel: ObservableObject {
...
func add(name: String, sets: Int, reps: Int) {
var newExercise = WorkoutItem()
newExercise.name = name // <- set properties
newExercise.sets = sets
newExercise.reps = reps
workout += [newExercise]
}
init() {
add(name: "Push Pp", sets: 1, reps: 8)
add(name: "Pull UPs", sets: 1, reps: 10)
}
}