No ObservableObject of type error when calling variable inside struct - xcode

I am trying to create a crime tracking app, but right now I am struggling to create structs for different tabs of my app using NavigationManager and screens. However when I made a new struct and tried to use my NavigationManager, it displayed this error message:
2022-10-26 02:18:45.468441-0400 TextFieldAndSecureFieldInSwiftUI[50866:2496084] SwiftUI/EnvironmentObject.swift:70: Fatal error: No ObservableObject of type StatsNavigationManager found. A View.environmentObject(_:) for StatsNavigationManager may be missing as an ancestor of this view.
This is the code for my StatsNavigationManager object and my tab.
final class StatsNavigationManager: ObservableObject {
#Published var screen: Screens? {
didSet {
print("📱 \(String(describing: screen))")
}
}
func push(to screen: Screens) {
self.screen = screen
}
func popToRoot() {
self.screen = nil
}
}
struct StatsView: View {
let test = cityClass()
struct Input{
var zip: String = ""
}
#State private var input: Input = .init()
#FocusState private var inputFocused: Bool
#EnvironmentObject var statNavManager: StatsNavigationManager//I define the var I am struggling with here
//I also tried these approaches but each gave me an error
//weak var statNavManager : StatsNavigationManager?
//#State var statNavManager : StatsNavigationManager
var body: some View {
NavigationView {
ZStack {
Color.blue
VStack {
inputTxtVw
submitBtn
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Text("Search")
Button("Retrieve Data") {
statNavManager.push(to: .statsPage)
}
.background(
NavigationLink(destination: CityView(),
tag: .statsPage,
selection: $statNavManager.screen) { EmptyView() }//the "$statNavManager.screen" gives me the error and reads No ObservableObject of type StatsNavigationManager found when I hover
)
}
}
.onSubmit(of: .text, submit)
}
}
.environmentObject(self.statNavManager)
//also tried StatsNavigationManager.environmentObject(statNavManager)
}
}
}

Related

Passing data with NavigationSplitView to the second column

I have a macOS app with two columns. The left column is a list that presents the filename and date of the unit (file) that I'm working on. The second column, to the right, should present the content of each file when selected.
I have an array that contains that information and I create a list for the left column that presents each item. I added a detail: with a TextEditor that allows the user to see the data and modify it if necessary. I have been trying to set the #State var text to the contents of currentunit.text but I don't know how to pass that the detail:. If I try to assign it (as in text = x) then I get an error saying that it doesn't conform to View.
I tried then to maybe load it by getting the index of the current selected unit, using the selectedUnitId, and using something like this to get the index:
func getIndex(uuid: UUID) -> Int? {
return data.units.firstIndex(where: {$0.id == uuid})
}
But I get nowhere with a collection of different errors.
Regardless, how do I pass data to the detail: part of the code? I have looked into many examples of NavigationSplitView and they are all very similar, just showing the basic usage and that's it.
Thanks!
Code:
struct Unit: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
let df = DateFormatter()
df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return df.string(from: date)
}
var changed: Bool = false
}
final class UnitModel: ObservableObject {
#AppStorage("unit") public var units: [Unit] = []
init() {
self.units = self.units.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.units = self.units.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
struct ContentView: View {
#EnvironmentObject private var data: UnitModel
#State var selectedUnitId: UUID?
#State var text: String = ""
var body: some View {
NavigationSplitView {
List(data.units, selection: $selectedUnitId) { currentunit in
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
} detail: {
// here: how do I preload $text with the text from the unit?
VStack(alignment: .leading) {
TextEditor(text: $text)
}
}
}
}
I also tried:
struct ContentView: View {
#EnvironmentObject private var data: UnitModel
#State var selectedNoteId: UUID?
var body: some View {
NavigationSplitView {
List(data.units, selection: $selectedNoteId) { currentunit in
NavigationLink{
UnitView(unit: currentunit, text: currentunit.text)
} label: {
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
}
} detail: {
Text("Select a unit.")
}
}
}
struct UnitView: View {
#EnvironmentObject private var data: UnitModel
var unit: Unit
#State var text: String
var body: some View {
VStack(alignment: .leading) {
TextEditor(text: $text)
}
}
}
But again, I don't know how to initialize the text variable with the text of the current unit. I only get the initial one selected, and even tho I can see a new unit selected, the text remains the same and doesn't update.
UPDATED if I change the code to use NavigationView then it works as it should, so what's going with the new way that Apple is make us use now? Namely NavigationSplitView and NavigationStack?
Here's the code that work as it should but it's deprecated according to Apple:
NavigationView {
List(data.units, selection: $selectedNoteId) { currentunit in
NavigationLink(
destination: UnitView(unit: currentunit, text: currentunit.text),
label: {
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
)
}
Apple's Defining the source of truth using a custom binding
tutorial covers this. Your code would look something like this:
} detail: {
DetailView(unitID: selectedUnitID) // not sure why they used binding
}
struct DetailView: View {
let unitID: Unit.ID
#EnvironmentObject private var store: UnitModel
private var unitBinding: Binding<Unit> {
Binding {
if let id = unitID {
return store.unit(with: id) ?? Unit.emptyUnit()
} else {
return Unit.emptyUnit()
}
} set: { updatedUnit in
store.update(updatedUnit)
}
}
var body: some View {
if store.contains(unitID) {
VStack(alignment: .leading) {
TextEditor(text: unitBinding.text)
}
}
else {
Text("Select Unit")
}
}
}
Note there currently (as of Xcode 14.2) is a known bug with the text cursor when using a TextField in the detail pane. Check by entering text, move cursor to middle and try to enter a character. The bug is the cursor jumps to the end.

why is a passed parameter displaying the previous content of an EnvironmentObject

this is a Macos app where the parsclass is setup in a previous view that contains the YardageRowView below. That previous view is responsible for changing the contents of the parsclass. This is working is other views that use a NavigationLink to display the views.
When the parsclass is changed, this view is refreshed, but the previous value is put in the text field on the holeValueTestView.
I cannot comprehend how the value is not being passed into the holeValueTestView correctly
This is a view shown as a .sheet, and if I dismiss it and display it again, everything is fine.
if you create a macOS project called YardageSample and replace the ContentView.swift and YardageSampleApp.swift with the two files below, you can see that the display in red changes and the black numbers do not change until you click Done and redisplay the .sheet
//
// YardageSampleApp.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
#main
struct YardageSampleApp: App {
#StateObject var parsclass = parsClass()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(parsclass)
}
}
}
//
// ContentView.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
struct ContentView: View {
#StateObject var parsclass = parsClass()
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
}
}
class parsClass: ObservableObject {
#Published var pars = [parsRec]()
init() {
self.pars = [parsRec]()
self.pars.append(parsRec())
}
func create(newpars: [parsRec]) {
pars.removeAll()
pars = newpars
}
}
class parsRec: Identifiable, Codable {
var id = UUID()
var Hole = 1
var Yardage = 1
}
struct CourseMaintenance: View {
#EnvironmentObject var parsclass: parsClass
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {presentationMode.wrappedValue.dismiss()}, label: {
Text("Done")
})
Button(action: {switchScores(number: 1)}, label: {
Text("Button 1")
})
Button(action: {switchScores(number: 2)}, label: {
Text("Button 2")
})
Button(action: {switchScores(number: 3)}, label: {
Text("Button 3")
})
CourseDetail().environmentObject(parsclass)
}.frame(width: 400, height: 400, alignment: .center)
}
func switchScores(number: Int) {
var newparRecs = [parsRec]()
for i in 0..<17 {
let myrec = parsRec()
myrec.Hole = i
myrec.Yardage = number
newparRecs.append(myrec)
}
parsclass.create(newpars: newparRecs)
}
}
struct CourseDetail: View {
#EnvironmentObject var parsclass: parsClass
var body: some View {
HStack(spacing: 0) {
ForEach(parsclass.pars.indices, id: \.self) { indice in
// this displays the previous value
holeValueTestView(value: String(parsclass.pars[indice].Yardage))
// this displays the correct value after parsclass has changed
Text(String(parsclass.pars[indice].Yardage))
.foregroundColor(.red)
}
}
}
}
struct holeValueTestView: View {
#State var value: String
var body: some View {
//TextField(String(value), text: $value)
Text(value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There are a couple of issues going on:
You have multiple instances of parsClass. One is defined in YardageSampleApp and passed into the view hierarchy as a #EnvironmentObject. The second is defined in ContentView as a #StateObject. Make sure you're only using one.
On holeValueTestView, you defined value as a #State variable. That gets set initially when the view is created by its parent and then it maintains its own state. So, when the environmentObject changed, because it was in charge of its own state at this point, it didn't update the value. You can simply remove #State and see the behavior that you want.
struct ContentView: View {
#EnvironmentObject var parsclass : parsClass //<-- Here
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: .center)
}
}
struct holeValueTestView: View {
var value: String //<-- Here
var body: some View {
Text(value)
}
}
As a side note:
In Swift, normally type names are capitalized. If you want to write idiomatic Swift, you would change your parsClass to ParsClass for example.

SwitfUI: access the specific scene's ViewModel on macOS

In this simple example app, I have the following requirements:
have multiple windows, each having it's own ViewModel
toggling the Toggle in one window should not update the other window's
I want to also be able to toggle via menu
As it is right now, the first two points are not given, the last point works though. I do already know that when I move the ViewModel's single source of truth to the ContentView works for the first two points, but then I wouldn't have access at the WindowGroup level, where I inject the commands.
import SwiftUI
#main
struct ViewModelAndCommandsApp: App {
var body: some Scene {
ContentScene()
}
}
class ViewModel: ObservableObject {
#Published var toggleState = true
}
struct ContentScene: Scene {
#StateObject private var vm = ViewModel()// injecting here fulfills the last point only…
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
.frame(width: 200, height: 200)
}
.commands {
ContentCommands(vm: vm)
}
}
}
struct ContentCommands: Commands {
#ObservedObject var vm: ViewModel
var body: some Commands {
CommandGroup(before: .toolbar) {
Button("Toggle Some State") {
vm.toggleState.toggle()
}
}
}
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel//injecting here will result in window independant ViewModels, but make them unavailable in `ContactScene` and `ContentCommands`…
var body: some View {
Toggle(isOn: $vm.toggleState, label: {
Text("Some State")
})
}
}
How can I fulfill theses requirements–is there a SwiftUI solution to this or will I have to implement a SceneDelegate (is this the solution anyway?)?
Edit:
To be more specific: I'd like to know how I can go about instantiating a ViewModel for each individual scene and also be able to know from the menu bar which ViewModel is meant to be changed.
Long story short, see the code below. The project is called WindowSample this needs to match your app name in the URL registration.
import SwiftUI
#main
struct WindowSampleApp: App {
var body: some Scene {
ContentScene()
}
}
//This can be done several different ways. You just
//need somewhere to store multiple copies of the VM
class AStoragePlace {
private static var viewModels: [ViewModel] = []
static func getAViewModel(id: String?) -> ViewModel? {
var result: ViewModel? = nil
if id != nil{
result = viewModels.filter({$0.id == id}).first
if result == nil{
let newVm = ViewModel(id: id!)
viewModels.append(newVm)
result = newVm
}
}
return result
}
}
struct ContentCommands: Commands {
#ObservedObject var vm: ViewModel
var body: some Commands {
CommandGroup(before: .toolbar) {
Button("Toggle Some State \(vm.id)") {
vm.testMenu()
}
}
}
}
class ViewModel: ObservableObject, Identifiable {
let id: String
#Published var toggleState = true
init(id: String) {
self.id = id
}
func testMenu() {
toggleState.toggle()
}
}
struct ContentScene: Scene {
var body: some Scene {
//Trying to init from 1 windowGroup only makes a copy not a new scene
WindowGroup("1") {
ToggleView(vm: AStoragePlace.getAViewModel(id: "1")!)
.frame(width: 200, height: 200)
}
.commands {
ContentCommands(vm: AStoragePlace.getAViewModel(id: "1")!)
}.handlesExternalEvents(matching: Set(arrayLiteral: "1"))
//To open this go to File>New>New 2 Window
WindowGroup("2") {
ToggleView(vm: AStoragePlace.getAViewModel(id: "2")!)
.frame(width: 200, height: 200)
}
.commands {
ContentCommands(vm: AStoragePlace.getAViewModel(id: "2")!)
}.handlesExternalEvents(matching: Set(arrayLiteral: "2"))
}
}
struct ToggleView: View {
#Environment(\.openURL) var openURL
#ObservedObject var vm: ViewModel
var body: some View {
VStack{
//Makes copies of the window/scene
Button("new-window-of type \(vm.id)", action: {
//appname needs to be a registered url in info.plist
//Info Property List>Url types>url scheme>item 0 == appname
//Info Property List>Url types>url identifier == appname
if let url = URL(string: "WindowSample://\(vm.id)") {
openURL(url)
}
})
//Toggle the state
Toggle(isOn: $vm.toggleState, label: {
Text("Some State \(vm.id)")
})
}
}
}

SwiftUI Picker with Cloud Firestore

I was wondering whether I was able to get some help on this one, I've been trying a while to get things working and functioning properly and have been able to pass the Firestore data into the picker view, but I'm unable to select the data to view in the 'selected' area. I have added my code and my Firestore setup.
Thanks in advance.
import SwiftUI
import Firebase
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State var selectedSchool: String!
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(self.schoolData.datas) {i in
Text(self.schoolData.datas.count != 0 ? i.name : "No Schools Available").tag(i.name)
}
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("Select your school")
}
}
}
struct SchoolPicker_Previews: PreviewProvider {
static var previews: some View {
SchoolDetailsView()
}
}
class getSchoolData : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as! String
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable {
var id : String
var name : String
}
Firestore Setup Image
To solve the issue with the code above the you can cast the tag to be the same type as the selectedSchool variable. This should then allow it to be selectable and is also safer as it uses optionals and allows the picker to be initially set to nil.
Example Code:
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State var selectedSchool: String?
var body: some View {
NavigationView {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(self.schoolData.datas.sorted(by: { $0.name < $1.name } )) {i in
Text(self.schoolData.datas.count != 0 ? i.name : "No Schools Available").tag(i.name as String?)
}
}
Text("Selected School: \(selectedSchool ?? "No School Selected")")
}
}.navigationBarTitle("Select your school")
}
}
}
}
As an alternative to the example above, you could also change the selectedSchool variable to be a schoolName type and cast the tag to be schoolName and this will also work. The only caveat with this approach is that the schoolName type must conform to the Hashable protocol.
Example Alternative Code:
struct schoolName: Identifiable, Hashable {
var id: String
var name: String
}
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State var selectedSchool: schoolName?
var body: some View {
NavigationView {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(self.schoolData.datas.sorted(by: { $0.name < $1.name } )) {i in
Text(self.schoolData.datas.count != 0 ? i.name : "No Schools Available").tag(i as schoolName?)
}
}
Text("Selected School: \(selectedSchool?.name ?? "No School Selected")")
}
}.navigationBarTitle("Select your school")
}
}
}
}
Either of these code examples should result in a working picker as follows:
Finally, as a side note for anyone when working with SwiftUI's default list view picker style, it must be enclosed within a NavigationView somewhere within the view hierarchy. This tripped me up when I first started using them :)

How to create Generic if #EnvironmentObject?

I've recently come across the need to write a Mock of a Class, as it causes the SwiftUI preview from working. Unfortunately, I get the error:
Property type 'T' does not match that of the 'wrappedValue' property of its wrapper type 'EnvironmentObject'
In the View struct:
struct ContentView<T>: View {
#EnvironmentObject var mockFoobar: T
...
}
And also the error:
Type of expression is ambiguous without more context
For the Preview struct:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let mockFoobar: MockFoobar = MockFoobar()
return ContentView<MockFoobar>()
.environmentObject(mockFoobar)
}
}
The MockFoobar class is:
class MockFoobar: ObservableObject {
...
}
As the user #Asperi kindly provided, tested the following as suggested:
class Foobar: ObservableObject {
#Published var param: Bool = false
func start() {
self.param = true
}
}
struct MyFoobarView<T: ObservableObject>: View {
#EnvironmentObject var foobar: T
var body: some View {
VStack {
Text("Hello Foobar")
}
.onAppear {
self.foobar.start()
}
}
}
struct MyFoobarView_Previews: PreviewProvider {
static var previews: some View {
let foobar: Foobar = Foobar()
return MyFoobarView()
.environmentObject(foobar)
}
}
But I get the following errors (the first in the .onAppear and the second in the PreviewProvider):
Cannot call value of non-function type 'Binding<Subject>'
Generic parameter 'T' could not be inferred
The EnvironmentObject must be ObservableObject, so here is fix
struct ContentView<T: ObservableObject>: View {
#EnvironmentObject var mockFoobar: T
// .. other code here
Update: added demo with introduced model protocol
protocol Foobaring {
var param: Bool { get set }
func start()
}
class Foobar: ObservableObject, Foobaring {
#Published var param: Bool = false
func start() {
self.param = true
}
}
struct MyFoobarView<T: ObservableObject & Foobaring>: View {
#EnvironmentObject var foobar: T
var body: some View {
VStack {
Text("Hello Foobar")
}
.onAppear {
self.foobar.start()
}
}
}
struct MyFoobarView_Previews: PreviewProvider {
static var previews: some View {
let foobar: Foobar = Foobar()
return MyFoobarView<Foobar>()
.environmentObject(foobar)
}
}

Resources