Search code examples
dictionaryswiftuiinitializerobservedobjectswiftui-windowgroup

SwiftUI - share dictionary among views, unclear what arguments to use at @Main / WindowGroup


I'm trying to build an app (macOS, but would be the same for iOS) that creates a number of grids, the outcome of which is to be shown in a second screen. For this, I'm sharing data across these screens, and I'm running into an issue here, I hope someone can help or point me in the right direction. I'll share a simplified version of the code below (working in Xcode 14.0.1)

The code creates a dictionary that can be shown in a grid, on which calculations can be done. The idea is then to add this grid, with some descriptive variables, into another dictionary

The building blocks of the grid are cells

Import Foundation
struct Cell: Comparable, Equatable, Identifiable, Hashable {
    static func == (lhs: Cell, rhs: Cell) -> Bool {
        lhs.randomVarOne == rhs.randomVarOne
    }
    var randomVarOne: Double
    var randomVarTwo: Bool
    // other vars omitted here
    var id: Int { randomVarOne } 
    static func < (lhs: Cell, rhs: Cell) -> Bool {
            return lhs.randomVarOne < rhs.randomVarOne
    }
}

this is also where there are a bunch of funcs to calculate next neighbor cells in the grid etc

then the grid is defined in a class:

class Info: ObservableObject, Hashable {
    static func == (lhs: Info, rhs: Info) -> Bool {
        lhs.grid == rhs.grid
    }
    func hash(into hasher: inout Hasher) {
            hasher.combine(grid)
        }
    @Published var grid = [Cell]()

    var arrayTotal = 900
    @Published var toBeUsedForTheGridCalculations: Double = 0.0
    var toBeUsedToSetTheVarAbove: Double = 0.0
    var rowTotalDouble: Double {sqrt(Double(arrayTotal)) }
    var rowTotal: Int {
        Int(rowTotalDouble) != 0 ? Int(rowTotalDouble) : 10 }

The class includes a func to create and populate the grid with Cells and add these Cells to the grid var. It also includes the formulas to do the calculations on the grid using a user input. The class did not seem to need an initializer.

This is the Scenario struct:

struct Scenario: Comparable, Equatable, Identifiable, Hashable {
    static func == (lhs: Scenario, rhs: Scenario) -> Bool {
        lhs.scenarioNumber == rhs.scenarioNumber
    }
    func hash(into hasher: inout Hasher) {
            hasher.combine(scenarioNumber)
        }
    var scenarioNumber: Int
    var date: Date
    var thisIsOneSnapshot = [Info]()
    var id: Int { scenarioNumber }
    static func < (lhs: Scenario, rhs: Scenario) -> Bool {
        return lhs.scenarioNumber < rhs.scenarioNumber
    }
}

added hashable since it uses the Info class as an input.

Then there is the class showing the output overview

class OutputOverview: ObservableObject {
    @Published var snapshot = [Scenario]()

// the class includes a formula of how to add the collection of cells (grid) and the additional variables to the snapshot dictionary. Again no initializer was necessary.

Now to go to the ContentView.

struct ContentView: View {
    @Environment(\.openURL) var openURL
    var scenarioNumberInput: Int = 0
    var timeStampAssigned: Date = Date.now

    @ObservedObject private var currentGrid: Info = Info()
    @ObservedObject private var scenarios: Combinations = Combinations()

    var usedForTheCalculations: Double = 0.0

    var rows =
    [
        GridItem(.flexible()),
     // whole list of GridItems, I do not know how to calculate these: 
     // var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal) 
     //gives error "Cannot use instance member 'currentGrid' within property initializer;
     // property iunitializers run before 'self' is available
    ]
    var body: some View {
        
        GeometryReader { geometry in
            VStack {
                ScrollView {
                    LazyHGrid(rows: rows, spacing: 0) {
                        ForEach(0..<currentGrid.grid.count, id :\.self) { w in
                            let temp = currentGrid.grid[w].varThatAffectsFontColor
                            let temp2 = currentGrid.grid[w].varThatAffectsBackground
                            Text("\(currentGrid.grid[w].randomVarOne, specifier: "%.2f")")
                                .frame(width: 25, height: 25)
                                .border(.black)
                                .font(.system(size: 7))
                                .foregroundColor(Color(wordName: temp))
                                .background(Color(wordName: temp2))
                        }
                    }
                    .padding(.top)
                }
                VStack{
                    HStack {
                        Button("Start") {
                   
                        }
                // then some buttons to do the calculations
                        Button("Add to collection"){
                            scenarios.addScenario(numbering: scenarioNumberInput, timeStamp:
                         Date.now, collection: currentGrid.grid)
                        } // this should add the newly recalculated grid to the dictionary
                        Button("Go to Results") {
                           guard let url = URL(string: "myapp://scenario") else { return }
                           openURL(url)         
                        } // to go to the screen showing the scenarios

Then the second View, the ScenarioView:

struct ScenarioView: View {
    @State var selectedScenario = 1
    @ObservedObject private var scenarios: OutputOverview
    
    var pickerNumbers = [ 1, 2, 3, 4 , 5] 
    // this is to be linked to the number of scenarios completed,this code is not done yet.
    var rows =
    [
        GridItem(.flexible()),
        GridItem(.flexible()),
     // similar list of GridItems here....

    var body: some View {
        Form {
            Section {
                Picker("Select a scenario", selection: $selectedScenario) { 
                  ForEach(pickerNumbers, id: \.self) {
                    Text("\($0)")
                    
                }
                }
            }
            Section {
                ScrollView {
                    if let idx = scenarios.snapshot.firstIndex(where: 
                      {$0.scenarioNumber == selectedScenario}) {
                        LazyHGrid(rows: rows, spacing: 0) {
                            
                            ForEach(0..<scenarios.snapshot[idx].thisIsOneSnapshot.count, 
                                id :\.self) { w in
                                let temp =
                         scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsFontColor
                                let temp2 = 
                         scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsBackground
                                Text("\(scenarios.snapshot[idx].thisIsOneSnapshot[w].randomVarOne, specifier: "%.2f")")
                                    .frame(width: 25, height: 25)
                                    .border(.black)
                                    .font(.system(size: 7))
                                    .foregroundColor(Color(wordName: temp))
                                    .background(Color(wordName: temp2))
                            }
                        }
                    }
                }
            }
        }
    }
}

Now while the above does not (for the moment..) give me error messages, I am not able to run the PreviewProvider in the second View. The main problem is in @main:

import SwiftUI

@main
struct ThisIsTheNameOfMyApp: App {
 
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .handlesExternalEvents(matching: ["main"])
        
        WindowGroup("Scenarios") {
            ScenarioView()

       // error messages here: 'ScenarioView' initializer is inaccessible due to "private"
       // protection level - I don't know what is set to private in ScenarioView that could
       // cause this
       // second error message: missing argument for parameter 'scenarios' in call
        }
        .handlesExternalEvents(matching: ["scenario"])
    }
}

I am at a loss on how to solve these 2 error messages and would be very grateful for any tips or guidance. Apologies if this question is very long, I scanned many other forum questions and could not find any good answers.

I have tried adding pro forma data in @main as follows


@main
struct FloodModelScenarioViewerApp: App {
    @State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .handlesExternalEvents(matching: ["main"])
        
        WindowGroup("Scenarios") {
            ScenarioView(scenarios: scenarios)

        }
        .handlesExternalEvents(matching: ["scenario"])
    }
}

This still gives 2 error messages:

  • same issue with regards to ScenarioView initialiser being inaccessible due to being 'private'
  • Cannot convert value of type 'Scenario' to expected argument type 'OutputOverview'

Solution

  • Just remove the private from

    @ObservedObject private var scenarios: OutputOverview
    

    The value is coming from he parent so the parent needs access. So put

    @StateObject private var scenarios: OutputOverview = .init()
    

    in FloodModelScenarioViewerApp

    @StateObject is for initializing ObservableObjects and @ObservedObject is for passing them around.