Recursive SwiftUI views - swift

I'm unable to display a SwiftUI view that calls itself in a ForEach loop for some reason. The app hangs and then crashes when it tries to display this view:
struct LoopView: View {
let loop: Loop
var body: some View {
HStack {
Text(String(loop.multiplier))
.font(.title)
Text("x")
VStack {
// ### THE PROBLEM IS HERE ###
if let loops = loop.loops {
ForEach(loops, id: \.id) { innerLoop in
// Text(String(innerLoop.multiplier)) // << This works
LoopView(loop: innerLoop) // << This causes the system to hang
}
} else {
// This stuff here is fine
if let components = loop.components {
ForEach(components, id: \.id) { component in
ComponentView(component: component)
}
}
}
}
}
}
}
struct LoopView_Previews: PreviewProvider {
static let moc = DataController.preview.container.viewContext
static var previews: some View {
let outterLoop = Loop(context: moc)
outterLoop.id = UUID()
outterLoop.multiplier = 5
outterLoop.addToInternalLoops(DataController.exampleLoop())
// vvv Does not change outcome if commented out vvv
outterLoop.addToInternalLoops(DataController.exampleLoop())
return LoopView(loop: outterLoop)
}
}
As per the comments, I can access the attributes of the looped item just fine, meaning there's nothing wrong with the loop and the elements can be accessed. However, I'm unable to use my loop recursively. innerLoop doesn't contain a loop meaning it will stop recursing after going only one level deep.
I'm using Xcode 13.2.1. Thank you for your help!

Did some more debugging and found the error came from my self-referencing one-to-many entity needed an inverse reference to itself. See this post for details.

Related

Swiftui navigation link to a qlpreview - property initialisers run before 'self' is available

Very new to coding and trying to teach myself swift. Running into a problem that I am having trouble understanding. Apologies if my code is a mess or my question is not clear.
I am creating a navigationview from a list I have created. I would like each list item to link to a detail page, that has a button opening a qlpreview of a word document, specific to that list item.
Firstly I have set the structure of my list items:
struct ScriptItem: Identifiable {
let id = UUID()
let ex: String
let phase: String
let doc: String
}
I then have my list and navigationview struct set up to link to a detailed view
struct ScriptsView: View {
#State private var queryString = ""
private let scriptList: [ScriptItem] = [
ScriptItem(ex: "101",
phase: "HMI",
doc: "PILOT NOTES EX 101"),
ScriptItem(ex: "102",
phase: "HMI",
doc: "PILOT NOTES EX 201")].sorted(by: {$0.ex < $1.ex})
var filteredScript: [ScriptItem] {
if queryString.isEmpty {
return scriptList
} else {
return scriptList.filter {
$0.ex.lowercased().contains(queryString.lowercased()) == true || $0.phase.lowercased().contains(queryString.lowercased()) == true
}
}
}
var body: some View {
NavigationView {
List(filteredScript) { scriptItem in
NavigationLink(destination: ScriptDetails(scriptitem: scriptItem)) {
HStack {
ZStack {
Text(scriptItem.ex)
}
Text(scriptItem.phase)
}
}
} .navigationBarTitle("Exercise Scripts")
} .searchable(text: $queryString, placement: .navigationBarDrawer(displayMode: .always), prompt: "Script Search")
}
}
My problem is in the next section - in the detail struct I'm trying to create a url link using forResource: but when I use the string from the previous struct I get the following error:
CANNOT USE INSTANCE MEMBER 'SCRIPTITEM' WITHIN PROPERTY INITIALIZER. PROPERTY INITIALIZERS RUN BEFORE 'SELF' IS AVAILABLE.
struct ScriptDetails: View {
let scriptitem: ScriptItem
let fileUrl = Bundle.main.url(
forResource: scriptitem.doc, withExtension: "docx"
)
ETC ETC ETC
Is there a way around this error that allows me to refer to the string I need from the previous struct?

iOS16 SwiftUI app crashes when trying to create view using loop

I've defined a view in SwiftUI which takes an Int array and is supposed to display the elements of the array in a VStack such that every "full row" contains three elements of the array and then the last "row" contains the remainder of elements. When running the app on iOS16 I get "Fatal error: Can't remove first element from an empty collection" for the call let die = dice.removeFirst() (also when passing in a non-empty array of course). I've tried following the debugger but I don't understand the way it jumps around through the loops.
On iOS15 this code worked fine. In the actual program I don't display the array content as Text but I have images associated with each Int between 1 and 6 which I display. I replaced this with Text for simplicity's sake.
Thanks for any help!
struct DiceOnTableView: View {
let diceArray: [Int]
var body: some View {
let fullRows: Int = diceArray.count / 3
let diceInLastRow: Int = diceArray.count % 3
var dice: [Int] = diceArray
VStack {
ForEach(0..<fullRows, id: \.self) { row in
HStack {
ForEach(0..<3) { column in
let die = dice.removeFirst()
Text("\(die)")
}
}
}
HStack {
ForEach(0..<diceInLastRow, id: \.self) { column in
let die = dice.removeFirst()
Text("\(die)")
}
}
}
}
}
This does kind of work on iOS 15 (but strangely - the order of the dice is unexpected), and crashes on iOS 16. In general, you should not be using vars in SwiftUI view building code.
Your code can be modified to compute the index into the original diceArray from the row, fullRows, and column values:
struct DiceOnTableView: View {
let diceArray: [Int]
var body: some View {
let fullRows: Int = diceArray.count / 3
let diceInLastRow: Int = diceArray.count % 3
VStack {
ForEach(0..<fullRows, id: \.self) { row in
HStack {
ForEach(0..<3) { column in
Text("\(diceArray[row * 3 + column])")
}
}
}
HStack {
ForEach(0..<diceInLastRow, id: \.self) { column in
Text("\(diceArray[fullRows * 3 + column])")
}
}
}
}
}
The ForEach View will crash if you use it like a for loop with dynamic number of items. You need to supply it an array of item structs with ids, e.g. one that is Identifiable, e.g.
struct DiceItem: Identifable {
let id = UUID()
var number: Int
}
#State var diceItems: [DiceItem] = []
ForEach(diceItems) { diceItem in
And perhaps use a Grid instead of stacks.

SwiftUI ForEach with index - "The compiler is unable to type-check this expression in reasonable time"

In a swiftUI view that I'm writing, I need to use a ForEach, accessing each element of a list and its index. Most of the information I could find about this said to use .enumerated() as in ForEach(Array(values.enumerated()), id: \.offset) { index, value in }
However when I try to do that in my view:
/// A popover displaing a list of items.
struct ListPopover: View {
// MARK: Properties
/// The array of vales to display.
var values: [String]
/// Whether there are more values than the limit and they are concatenated.
var valuesConcatenated: Bool = false
/// A closure that is called when the button next to a row is pressed.
var action: ((_ index: Int) -> Void)?
/// The SF symbol on the button in each row.
var actionSymbolName: String?
// MARK: Initializers
init(values: [String], limit: Int = 10) {
if values.count > limit {
self.values = values.suffix(limit - 1) + ["\(values.count - (limit - 1)) more..."]
valuesConcatenated = true
} else {
self.values = values
}
}
// MARK: Body
var body: some View {
VStack {
ForEach(Array(values.enumerated()), id: \.offset) { index, value in
HStack {
if !(index == values.indices.last && valuesConcatenated) {
Text("\(index).")
.foregroundColor(.secondary)
}
Text(value)
Spacer()
if action != nil && !(index == values.indices.last && valuesConcatenated) {
Spacer()
Button {
action!(index)
} label: {
Image(systemName: actionSymbolName ?? "questionmark")
}
.frame(alignment: .trailing)
}
}
.if((values.count - index) % 2 == 0) { view in
view.background(
Color(.systemGray5)
.cornerRadius(5)
)
}
}
}
}
}
I get the error The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions on the line var body: some View {
I've also noticed that this code causes some other problems like making the Xcode autocomplete extremely slow.
Any ideas how I might be able to solve this? It seems like a pretty simple view and I think I'm doing the ForEach how I should.
Thanks!
This is a very misleading error. What it really means is you screwed something up in your body, but the compiler can't figure out the error, so it throws it on the body itself. The easiest way to find it is to comment out portions of your body in matched braces until the error goes away. In your case the issue is with this:
.if((values.count - index) % 2 == 0) { view in
view.background(
Color(.systemGray5)
.cornerRadius(5)
)
}
I am not sure what you are attempting to do, but .if is not valid syntax and I am not sure what view is or where it is supposed to come from.

How to update interface elements from asynchronous stuff from a singleton

I am developing my first app in SwiftUI and my brain have not wrapped around certain things yet.
I have to display prices, titles and descriptions of inapp purchases on the interface.
I have a singleton model like this, that loads as the app starts.
class Packages:ObservableObject {
enum Package:String, CaseIterable {
typealias RawValue = String
case package1 = "com.example.package1"
case package2 = "com.example.package2"
case package3 = "com.example.package3"
}
struct PurchasedItem {
var productID:String?
var localizedTitle:String = ""
var localizedDescription:String = ""
var localizedPrice:String = ""
}
static let sharedInstance = Packages()
#Published var purchasedItems:[PurchasedItem] = []
func getItem(_ productID:String) -> PurchasedItem? {
return status.filter( {$0.productID == productID } ).first
}
func getItemsAnync() {
// this will fill `purchasedItems` asynchronously
}
purchasedItems array will be filled with PurchasedItems, asynchronous, as the values of price, title and description come from the App Store.
Meanwhile, at another part of the interface, I am displaying buttons on a view, like this:
var buttonPackage1String:String {
let item = Packages.sharedInstance.getItem(Packages.Package.package1.rawValue)!
let string = """
\(umItem.localizedTitle) ( \(umItem.localizedPrice) ) \
\(umItem.localizedDescription) )
"""
return string
}
// inside var Body
Button(action: {
// purchase selected package
}) {
Text(buttonPackage1String)
.padding()
.background(Color.green)
.foregroundColor(.white)
.padding()
}
See my problem?
buttonPackage1String is built with title, description and price from an array called purchasedItems that is stored inside a singleton and that array is filled asynchronously, being initially empty.
So, at the time the view is displayed all values may be not retrieved yet but will, eventually.
Is there a way for the button is updated after the values are retrieved?
Calculable properties are not observed by SwiftUI. We have to introduce explicit state for this and update it once dependent dynamic data changed.
Something like,
#ObservedObject var packages: Packages
...
#State private var title: String = ""
...
Button(action: {
// purchase selected package
}) {
Text(title)
}
.onChange(of: packages.purchasedItems) { _ in
self.title = buttonPackage1String // << here !!
}
Below is a small pseudo code, I'm not sure it's suitable for your project but might give you some hint.
Packages.sharedInstance
.$purchasedItems //add $ to get the `Combine` Subject of variable
.compactMap { $0.firstWhere { $0.id == Packages.Package.package1.rawValue } } //Find the correct item and skip nil value
.map { "\($0.localizedTitle) ( \($0.localizedPrice) ) \ \($0.localizedDescription) )" }
.receive(on: RunLoop.main) // UI updates
.assign(to: \.buttonTitleString, on: self) //buttonTitleString is a #Published variable of your View model
.store(in: &cancellables) //Your cancellable collector
Combine framework knowledge is a must to start with SwiftUI.

SwiftUI fails to compile when attempting to filter data using string comparison

hoping someone can help me out. Been trying to figure out what's going on here with no luck. The app I am building contains the SwiftUI View listed below.
This View is embedded in another View which contains other List's, VStack's, etc. It is called when an item is selected to show another list of data based upon the user's selection.
It all looks, acts and works as intended (without data filtering).
For now, I am using a sample dataSet created using a simple Dictionary of data. When I attempt to apply a filter to this data by string comparison it causes a failure to compile with the following messages:
From Xcode:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
From Canvas:
timedOutSpecific(30.0, operation: "a thunk to build")
In addition to these errors, the energy consumption of Xcode skyrockets until failure.
The code listed below will work if I remove the code self.dataSet == aRecord.module in the if statement and replace it with true. Any time I try to filter my dataset it results in these errors.
import SwiftUI
struct DataListView: View {
#State var titleBar = ""
#State private var showFavorites = false
#State private var showPriority = false
#State var dataSet = ""
var body: some View {
List{
ForEach (sampleData) { aRecord in
if (((aRecord.isFavorite && self.showFavorites) ||
(aRecord.isPriority && self.showPriority) ||
(!self.showPriority)) && self.dataSet == aRecord.module ){
VStack {
NavigationLink(destination: DetailView(titleBar: aRecord.title, statuteData: aRecord.statuteData, isFavorite: aRecord.isFavorite)) {
HStack {
Text(aRecord.module)
.font(.subheadline)
VStack(alignment: .leading) {
Text(aRecord.title)
}
.scaledToFit()
Spacer()
if aRecord.isFavorite {
Image(systemName: "star.fill")
.imageScale(.small)
.foregroundColor(.yellow)
}
}
}
}
.navigationBarTitle(self.titleBar)
.navigationBarItems(trailing:
HStack{
Button(action: {
self.showFavorites.toggle()
}) {
if self.showFavorites {
Image(systemName: "star.fill")
.imageScale(.large)
.foregroundColor(.yellow).padding()
} else {
Image(systemName: "star")
.imageScale(.large).padding()
}
}
Button(action: {
self.showPriority.toggle()
}) {
if self.showPriority {
Text("Priority")
} else {
Text("Standard")
}
}
})
}//endif
}
}//end foreach
}
}
struct TempCode_Previews: PreviewProvider {
static var previews: some View {
DataListView(dataSet: "myDataSetID")
}
}
The reason I believe that the string comparison is the culprit is, for one, it crashes as described above. I have also tried placing the conditional in other places throughout the code with the same results. Any time I apply this type of filter it causes this crash to occur.
Any advice is appreciated.
Thank you.
Break out that complex boolean logic into a function outside of the view builder that takes a record and returns a boolean & it should work.
I think the compiler struggles when there is complex logic inside of the body & can't verify return types etc etc.
Record Verification Function:
func verify(_ record: Record) -> Bool {
return (((record.isFavorite && showFavorites) ||
(record.isPriority && showPriority) ||
(!showPriority)) && dataSet == record.module )
}
Usage In Body:
if self.verify(aRecord) {