How to modify a user input inside a SwiftUI form loop - swift

I'm developing a simple SwiftUI app in Xcode 11. I want to have a form that loops through multiple user input strings and displays a form with a button. When the user presses the button it modifies the input value - specifically increment or decrement it.
However when passing an array of references like UserInput().foo where UserInput is a published observable object I cannot modify the value inside a ForEach because the ForEach is passed a copy as oppose to the original reference (at least that's my basic understanding). How do I then try to achieve it? I read about inout and everybody says to avoid it but surely this must be a relatively common issue.
I've made an simple example of what I'm trying to do but I can't quite work it out:
import SwiftUI
class UserInput: ObservableObject {
#Published var foo: String = ""
#Published var bar: String = ""
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView?{
var userinputs = [
[UserInput().foo, "Foo"],
[UserInput().bar, "Bar"]
]
var inputs: some View{
VStack(){
ForEach(userinputs, id: \.self){userinput in
Text("\(userinput[1]): \(String(userinput[0]))")
Button(action: {
increment(input: String(userinput[0]))
}){
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String){
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}

As I understood, when adding a value to userinputs, the ForEach values doesn't change.
Well, if that's the case, first of all, you could try creating a struct and in it, you declare foo and bar, then just declare a variable of type the struct. It'll look like this:
struct Input: Identifiable {
var id = UUID()
var foo: String
var bar: String
}
class UserInput: ObservableObject {
#Published var inputs: [Input] = [Input]()
}
//ContentView
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView? {
var inputs: some View {
VStack {
ForEach(input.inputs) { userinput in
Text("\(userinput.bar): \(String(userinput.foo))")
Button(action: {
increment(input: String(userinput.foo))
}) {
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String) {
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
Wouldn't this be easier and more elegant?

Related

SwiftUI memory leak

I'm getting a weird memory leak in SwiftUI when using List and id: \.self, where only some of the items are destroyed. I'm using macOS Monterey Beta 5.
Here is how to reproduce:
Create a new blank SwiftUI macOS project
Paste the following code:
class Model: ObservableObject {
#Published var objs = (1..<100).map { TestObj(text: "\($0)")}
}
class TestObj: Hashable {
let text: String
static var numDestroyed = 0
init(text: String) {
self.text = text
}
static func == (lhs: TestObj, rhs: TestObj) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
deinit {
TestObj.numDestroyed += 1
print("Deinit: \(TestObj.numDestroyed)")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
List(model.objs, id: \.self) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
}
}
Run the app, and press the "Remove half" button. Keep pressing it until all the items are gone. However, if you look at the console, you'll see that only 85 items have been destroyed, while there were 99 items. The Xcode memory graph also supports this.
This seems to be caused by the id: \.self line. Removing it and switching it out for id: \.text fixes the problem.
However the reason I use id: \.self is because I want to support multiple selection, and I want the selection to be of type Set<TestObj>, instead of Set<UUID>.
Is there any way to solve this issue?
If you didn't have to use selection in your List, you could use any unique & constant id, for example:
class TestObj: Hashable, Identifiable {
let id = UUID()
/* ... */
}
And then your List with the implicit id: \.id:
List(model.objs) { obj in
Text(obj.text)
}
This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.
But you need selection in List, so see more below about how to achieve that.
To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.
Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:
extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}
Change your Model object to this:
class Model: ObservableObject {
#Published var objs: OrderedDictionary<UUID, TestObj>
init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}
And rather than model.objs you'll use model.objs.values, but that's it!
See full demo code below to test the selection:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selection: Set<UUID> = []
var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}
print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}
Result:

How to show a value present inside a function inside the body property in SwiftUI?

I am making an app that requires me to use an ml model and is performing its calculation to get the output from the ml model inside a function, now I need to display that constant which stores the final output of that ml model inside the body property of my swift UI view so that I can present it inside Text() maybe and modify the way it looks Lil bit
all of this code is inside a single swift file
here is all the code
after making the changes it's still showing some error
struct newView: View {
let model = AudiCar()
#ObservedObject var values: impData
#State var price = ""
var body: some View {
Text("calculated price is " + price)
.onAppear {
calculatePrice()
}
func calculatePrice() { <- this is where it showing error, saying " Closure containing a declaration cannot be used with function builder 'ViewBuilder' "
do {
let AudiCarOutput = try model.prediction(model: String(values.nameSelection), year: Double(values.yearSelection), transmission: String(values.transmisssionSelec), mileage: Double(values.mileage), fuelType: String(values.fuelSelection))
let price = String(AudiCarOutput.price)
}
catch {
}
}
}
struct newView_Previews: PreviewProvider {
static var previews: some View {
newView(values: impData())
}
}
try something like this:
struct NewView: View {
let model = AudiCar()
#ObservevedObject var values: impData
#State var price = ""
var body: some View {
Text("calculated price is " + price)
.onAppear {
calculatePrice()
}
}
func calculatePrice() {
do {
...
price = String(AudiCarOutput.price)
}
catch {
...
}
}
}

Binding Array of Structs with Strings in an Array of Structs

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}

Published computed properties in SwiftUI model objects

Suppose I have a data model in my SwiftUI app that looks like the following:
class Tallies: Identifiable, ObservableObject {
let id = UUID()
#Published var count = 0
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = []
}
I want to add a computed property to GroupOfTallies that resembles the following:
// Returns the sum of counts of all elements in the group
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
However, I want SwiftUI to update views when the cumulativeCount changes. This would occur either when elements changes (the array gains or loses elements) or when the count field of any contained Tallies object changes.
I have looked into representing this as an AnyPublisher, but I don't think I have a good enough grasp on Combine to make it work properly. This was mentioned in this answer, but the AnyPublisher created from it is based on a published Double rather than a published Array. If I try to use the same approach without modification, cumulativeCount only updates when the elements array changes, but not when the count property of one of the elements changes.
There are multiple issues here to address.
First, it's important to understand that SwiftUI updates the view's body when it detects a change, either in a #State property, or from an ObservableObject (via #ObservedObject and #EnvironmentObject property wrappers).
In the latter case, this is done either via a #Published property, or manually with objectWillChange.send(). objectWillChange is an ObservableObjectPublisher publisher available on any ObservableObject.
This is a long way of saying that IF the change in a computed property is caused together with a change of any #Published property - for example, when another element is added from somewhere:
elements.append(Talies())
then there's no need to do anything else - SwiftUI will recompute the view that observes it, and will read the new value of the computed property cumulativeCount.
Of course, if the .count property of one of the Tallies objects changes, this would NOT cause a change in elements, because Tallies is a reference-type.
The best approach given your simplified example is actually to make it a value-type - a struct:
struct Tallies: Identifiable {
let id = UUID()
var count = 0
}
Now, a change in any of the Tallies objects would cause a change in elements, which will cause the view that "observes" it to get the now-new value of the computed property. Again, no extra work needed.
If you insist, however, that Tallies cannot be a value-type for whatever reason, then you'd need to listen to any changes in Tallies by subscribing to their .objectWillChange publishers:
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = [] {
didSet {
cancellables = [] // cancel the previous subscription
elements.publisher
.flatMap { $0.objectWillChange }
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
private var cancellables = Set<AnyCancellable>
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count } // no changes here
}
}
The above will subscribe a change in the elements array (to account for additions and removals) by:
converting the array into a Sequence publisher of each array element
then flatMap again each array element, which is a Tallies object, into its objectWillChange publisher
then for any output, call objectWillChange.send(), to notify of the view that observes it of its own changes.
This is similar to the last option of #New Devs answer, but a little shorter, essentially just passing the objectWillChange notification to the parent object:
import Combine
class Tallies: Identifiable, ObservableObject {
let id = UUID()
#Published var count = 0
func increase() {
count += 1
}
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
var sinks: [AnyCancellable] = []
#Published var elements: [Tallies] = [] {
didSet {
sinks = elements.map {
$0.objectWillChange.sink( receiveValue: objectWillChange.send)
}
}
}
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
}
SwiftUI Demo:
struct ContentView: View {
#ObservedObject
var group: GroupOfTallies
init() {
let group = GroupOfTallies()
group.elements.append(contentsOf: [Tallies(), Tallies()])
self.group = group
}
var body: some View {
VStack(spacing: 50) {
Text( "\(group.cumulativeCount)")
Button( action: group.elements.first!.increase) {
Text( "Increase first")
}
Button( action: group.elements.last!.increase) {
Text( "Increase last")
}
}
}
}
The simplest & fastest is to use value-type model.
Here is a simple demo. Tested & worked with Xcode 12 / iOS 14
struct TestTallies: View {
#StateObject private var group = GroupOfTallies() // SwiftUI 2.0
// #ObservedObject private var group = GroupOfTallies() // SwiftUI 1.0
var body: some View {
VStack {
Text("Cumulative: \(group.cumulativeCount)")
Divider()
Button("Add") { group.elements.append(Tallies(count: 1)) }
Button("Update") { group.elements[0].count = 5 }
}
}
}
struct Tallies: Identifiable { // << make struct !!
let id = UUID()
var count = 0
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = []
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
}

Why does picker binding not update when using SwiftUI?

I have just begun learning Swift (and even newer at Swift UI!) so apologies if this is a newbie error.
I am trying to write a very simple programme where a user chooses someone's name from a picker and then sees text below that displays a greeting for that person.
But, the bound var chosenPerson does not update when a new value is picked using the picker. This means that instead of showing a greeting like "Hello Harry", "Hello no-one" is shown even when I've picked a person.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson: String? = nil
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach ((0..<people.count), id: \.self) { person in
Text(self.people[person])
}
}
}
Section{
Text("Hello \(chosenPerson ?? "no-one")")
}
}
}
}
}
(I have included one or two pieces of the original formatting in case this is making a difference)
I've had a look at this question, it seemed like it might be a similar problem but adding .tag(person) to Text(self.people[person])did not solve my issue.
How can I get the greeting to show the picked person's name?
Bind to the index, not to the string. Using the picker, you are not doing anything that would ever change the string! What changes when a picker changes is the selected index.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson = 0
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach(0..<people.count) { person in
Text(self.people[person])
}
}
}
Section {
Text("Hello \(people[chosenPerson])")
}
}
}
}
}
The accepted answer is right if you are using simple arrays, but It was not working for me because I was using an array of custom model structs with and id defined as string, and in this situation the selection must be of the same type as this id.
Example:
struct CustomModel: Codable, Identifiable, Hashable{
var id: String // <- ID of type string
var name: String
var imageUrl: String
And then, when you are going to use the picker:
struct UsingView: View {
#State private var chosenCustomModel: String = "" //<- String as ID
#State private var models: [CustomModel] = []
var body: some View {
VStack{
Picker("Picker", selection: $chosenCustomModel){
ForEach(models){ model in
Text(model.name)
.foregroundColor(.blue)
}
}
}
Hope it helps somebody.