SwiftUI: ViewBuilder unable to create a collection of Views based on array - swift

I am trying to build a simple List by using SwiftUI. However, I am not able to dynamically create the rows by using an array of data. This is the error message: Cannot convert value of type '(Setlist) -> SetlistRow' to expected argument type '(_) -> _'
I've tried at least the following syntaxes, but I always get the same error.
List(setlists) { }
List(setlists, rowContent: Setlist.init)
ForEach(self.setlists) { setlist in }
Here is my code:
struct Setlist {
var name: String = "New setlist"
var sets = [SongSet]()
}
struct SetlistManagerView : View {
private var setlists: [Setlist] {
// creates an array of dummy items
}
var body : some View {
List {
ForEach(setlists) {
SetlistRow(setlist: $0)
}
}
}
}
struct SetlistRow : View {
var setlist: Setlist
var body : some View {
let numberOfSongs = setlist.sets.map { $0.songs.count }.reduce(0, +)
return NavigationView {
NavigationButton (destination: SetListView(setlist: setlist)) {
// code for displaying the row
}
}
}
}

List items need to conform to Identifiable protocol in order for them to be used as collection data source without the identified(by:) argument.
Xcode error message here is misleading as the software is still in beta.

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 {
...
}
}
}

Deleting items from a CoreData relationship using .onDelete in a list

My app has a detail view that displays a list of items in a relationship to the detailed attribute. I am trying to find a way to delete these items without using .sorted() because the order of the items may be indeterminate (this is because the Comparable implementation is indeterminate. Also needs to be fixed but not the subject of this question). My current code is this:
extension MyItem {
var myData: Set<MyData> {
get { myData_ as? Set<MyData> ?? [] }
set { myData_ = newValue as NSSet }
}
extension MyData: Comparable {
public static func < (lhs: MyData, rhs: MyData) -> Bool {
// not guaranteed to be determinate sort algorithm
}
}
currently implemented as:
struct MyItemDetailView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var myItem: MyItem
var body: some View {
List {
ForEach(myItem.myData.sorted(), id: \.self { item in
Text(item)
}
.onDelete(deleteMyData)
}
}
private func deleteMyData(offsets: IndexSet) {
withAnimation {
offsets.map { myItem.myData.sorted()[$0] }.forEach(viewContext.delete)
PersistenceController.shared.saveContext()
}
}
}
The issue is that the 2 instances of .sorted() may not yield the same results. Although this definitely needs to be fixed, I would like to develop a method to guard against this in the future. The results of swiping to delete will delete a different row than the row that was swiped.
Is it better to use myItem.RemoveFromMyData_ in the .map? If so, how to do so without changing the order but making sure the types match up?
Thanks.

Simple way to modify deeply nested struct

I've been becoming more familiar with the "copy on write" behavior of Swift structs. I think it's a really nice way to get around having to manage references for structs, but it's a bit cumbersome when dealing with deeply nested structures.
If you want to update a deeply nested value, you need a direct path to that value so you can modify it on a single line:
myStruct.nestedArray[index].nestedValue = 1
The compiler will copy myStruct.nestedArray[index] and set nestedValue to 1 on that new value. It will then copy myStruct.nestedArray and set the new value at index. It will then copy myStruct and replace the previous value with a new one that has all of the above changes.
This works just fine and it's pretty cool that you can do this with a single line of code without having to worry about anything that was referencing myStruct and its children before. However, if there is more complicated logic involved in resolving the path to the value, the logic becomes much more verbose:
struct MyStruct {
var nestedEnum: MyEnum
}
enum MyEnum {
case one([NestedStruct])
case two([NestedStruct])
}
struct NestedStruct {
var id: Int
var nestedValue: Int
}
var myStruct = MyStruct(nestedEnum: .one([NestedStruct(id: 0, nestedValue: 0)]))
if case .one(var nestedArray) = myStruct.nestedEnum {
if let index = nestedArray.firstIndex(where: { $0.id == 0 }) {
nestedArray[index].nestedValue = 1
myStruct.nestedEnum = .one(nestedArray)
}
}
Ideally you'd be able to do something like this:
if case .one(var nestedArray) = myStruct.nestedEnum {
if var nestedStruct = nestedArray.first(where: { $0.id == 0 }) {
nestedStruct.nestedValue = 1
}
}
But as soon as nestedStruct.nestedValue is set, the new value of nestedStruct is swallowed.
What would be nice is if Swift had a way to use inout semantics outside of functions, so I could take a "reference" to nestedArray and then nestedStruct within it and set the inner nestedValue, causing the copy to propagate back up to myStruct the same way as it would if I'd been able to do it in one line.
Does anyone have any nice ways to deal with deeply nested structs that might be able to help me out here? Or am I just going to have to put up with the pattern from my second example above?
The solution I ended up arriving at was pretty SwiftUI specific, but it may be adaptable to other frameworks.
Basically, instead of having a single top-level method responsible for deeply updating the struct, I arranged my SwiftUI hierarchy to mirror the structure of my struct, and passed Bindings down that just manage one node of the hierarchy.
For example, given my struct defined above:
struct MyStruct {
var nestedEnum: MyEnum
}
enum MyEnum {
case one([NestedStruct])
case two([NestedStruct])
}
struct NestedStruct {
var id: Int
var nestedValue: Int
}
I could do this:
struct MyStructView: View {
#Binding var myStruct: MyStruct
var body: some View {
switch myStruct.nestedEnum {
case .one: OneView(array: oneBinding)
case .two: TwoView(array: twoBinding)
}
}
var oneBinding: Binding<[NestedStruct]> {
.init(
get: {
if case .one(array) = myStruct.nestedEnum {
return array
}
fatalError()
},
set: { myStruct.nestedEnum = .one($0) }
)
}
var twoBinding: Binding<[NestedStruct]> { /* basically the same */ }
}
struct OneView: View {
#Binding var array: [NestedStruct]
var body: some View {
ForEach(0..<array.count, id: \.self) {
NestedStructView(nestedStruct: getBinding($0))
}
}
func getBinding(_ index: Int) -> Binding<NestedStruct> {
.init(get: { array[index] }, set: { array[index] = $0 })
}
}
struct NestedStructView: View {
#Binding var nestedStruct: NestedStruct
var body: some View {
NumericInput(title: "ID: \(nestedStruct.id)", value: valueBinding)
}
var valueBinding: Binding<Int> {
.init(get: { nestedStruct.value }, set: { nestedStruct.value = $0 })
}
}
The only annoying bit is that it can be a bit verbose to construct a Binding manually. I wish SwiftUI had some syntax for getting nested Bindings from a Binding containing an array or struct.

SwiftUI row binding using ForEach?

I'm trying to bind an array of strings into their corresponding text fields in a scrolling list. The number of rows is variable, and corresponds to the number of elements in the string array. The user can add or delete rows, as well as changing the text within each row.
The following Playground code is a simplified version of what I'm trying to achieve.
import SwiftUI
import PlaygroundSupport
struct Model {
struct Row : Identifiable {
var textContent = ""
let id = UUID()
}
var rows: [Row]
}
struct ElementCell: View {
#Binding var row: Model.Row
var body: some View {
TextField("Field",text: $row.textContent)
}
}
struct ElementList: View {
#Binding var model: Model
var body: some View {
List {
ForEach($model.rows) {
ElementCell(row: $0)
}
}
}
}
struct ContentView: View {
#State var model = Model(rows: (1...10).map({ Model.Row(textContent:"Row \($0)") }))
var body: some View {
NavigationView {
ElementList(model: $model)
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
The issue is that I can't seem to get the "cell" to bind correctly with its corresponding element. In the example code above, Xcode 11.1 failed to compile it with error in line 26:
error: Text-Field Row.xcplaygroundpage:26:13: error: cannot invoke initializer for type 'ForEach<_, _, _>' with an argument list of type '(Binding<[Model.Row]>, #escaping (Binding<Model.Row>) -> ElementCell)'
ForEach($model.rows) {
^
Text-Field Row.xcplaygroundpage:26:13: note: overloads for 'ForEach<_, _, _>' exist with these partially matching parameter lists: (Data, content: #escaping (Data.Element) -> Content), (Range<Int>, content: #escaping (Int) -> Content)
ForEach($model.rows) {
^
What would be the recommended way to bind elements that are a result of ForEach into its parent model?