How to create Generic if #EnvironmentObject? - xcode

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)
}
}

Related

Why won't this simple #State (SwiftUI on MacOS) example update the View?

Xcode 14.1 (14B47b), Ventura 13.0.1, Swift 5
When clicking the button, it prints consecutive numbers in the debug window, but the SwiftUI View does not update. I couldn't get it to work on a much more complicated app and ran into this problem. Then I reduced it to this test project, and it still dosn't work.
This should be a trivial use of #State.
This is for a SwiftUI app running on MacOS.
What am I doing wrong (other than losing my mind)?
import SwiftUI
var globalCounter:Int = 0
#main
struct State_TestApp: App {
init() {
globalCounter = 1
}
var body: some Scene {
WindowGroup {
ContentView(counter:globalCounter)
}
}
}
func addOne() {
globalCounter += 1
print(globalCounter)
}
struct ContentView: View {
#State var counter:Int
var body: some View {
VStack {
Button("Add one") {
addOne()
}
Text("\(counter)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(counter:globalCounter)
}
}
Here is the answer for those that want to do something similar.
#main
struct State_TestApp: App {
var body: some Scene {
WindowGroup {
ContentView(start:3)
}
}
}
func addOne(number:Int) -> Int {
return number + 1
}
struct ContentView: View {
init(start:Int) {
counter = start
}
#State private var counter:Int
var body: some View {
VStack {
Button("Add one") {
counter = addOne(number: counter)
}
Text("\(counter)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(start:4)
}
}

No ObservableObject of type error when calling variable inside struct

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)
}
}
}

Updating SwiftUI View based on Observable in Preview

Trying to implement a Login screen in SwiftUI. Based on other similar questions, I'm going the approach of using an Observable EnvironmentObject and a ViewBuilder in the main ContentView that reacts to that and displays the appropriate screen.
However, even though the property is updating as expecting the view never changes in Preview. Everything works fine when built and run in the Simulator but in Preview the change never happens.
Below is the code reduced to the smallest possible example in a single file (only missing passing the environment object in SceneDelegate, which doesn't affect Preview anyway).
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
#ViewBuilder
var body: some View {
if !userAuth.person.isLoggedin {
FirstView()
} else {
SecondView()
} }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct Person {
var isLoggedin: Bool
init() {
self.isLoggedin = false
}
}
class UserAuth: ObservableObject {
#Published var person: Person
init(){
self.person = Person()
}
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
willChange.send(self)
self.person.isLoggedin = true
didChange.send(self)
}
}
struct SecondView: View {
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
struct FirstView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
VStack {
Button(action: {
self.userAuth.login()
}) {
Text("Login")
}
Text("Logged in: " + String(self.userAuth.person.isLoggedin))
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
EDIT: Based on the answer below, I've added the environment object to the interior views, but unfortunately the view still doesn't change in Preview mode.
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
environment object must be set in PreviewProvider as well
UPDATE
struct ContentView: View {
#ObservedObject var userAuth = UserAuth () // #ObservedObject
var body: some View {
NavigationView{ // Navigation
}.environmentObject(UserAuth) //.environmentObject
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct SecondView: View {
#EnvironmentObject var userAuth: UserAuth // only EnvironmentObject
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
The issue I was having with Canvas not giving previews is that my ObservableObject was reading from User Defaults
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") !
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
So works in simulator and on device but no Canvas Previews. I tried many ways to give Preview data to use since Preview can't read from UserDefaults (not a device), and realized I can put an initial/ default value if the UserDefault is not there:
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") ?? "Sean"
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
Now Preview/ Canvas is showing my view and I can continue coding with my Observable Object. The aim was to put in the Observable Object some default code to use.

structs and classes in ContentView_Previews

I have a struct:
struct Item: Identifiable {
let id = UUID()
var isComplete: Bool = false
}
Also let item: Item
And a class:
class Model: ObservableObject {
#Published var isOn: Bool = false
#Published var arr = [Item(isComplete: true), Item(isComplete: false), Item(isComplete: true), Item(isComplete: false), Item(isComplete: true), Item(isComplete: true)]
}
And #ObservedObject var model: Model
How to properly put item and model into ContentView_Previews?
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(
item: <#Item#>,
model: <#Model#>
)
}
}
I don't know what should I do next :/
It would be nice to add code for the ContentView. But if it looks like this:
struct ContentView: View {
var item: Item
#EnvironmentObject var model: Model
var body: some View {
// no matter what here
}
}
you can write this:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(item: Item(isComplete: false))
.environmentObject(Model())
}
}

SwiftUI : difficulty with dictionary to define a result list of search

I'll try to do one Search View.
I would like to save the result in a dictionary in order to create a list of result but xCode show me this error :
Cannot assign through subscript: 'self' is immutable
My code :
import SwiftUI
struct SearchListView: View {
#State var search: String = "test"
var stringDictionary: Dictionary = [Int: String]()
var body: some View
{
NavigationView
{
ForEach(chapterData) { chapter in
ForEach(chapter.lines) { line in
HStack {
if (self.search == line.text) {
HStack {
stringDictionary[0] = line.text
}
}
}
}
}
}
}
}
struct SearchListView_Previews: PreviewProvider {
static var previews: some View {
SearchListView(search: "test")
}
}
struct Chapter: Codable, Identifiable {
let id:Int
let lines: [Line]
}
struct Line: Codable, Identifiable {
let id: Int
let text: String
}

Resources