Updating SwiftUI View based on Observable in Preview - xcode

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.

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

SwiftUI dismiss() does not work on preview

I am making a TODO app on Xcode (ver 13.4) on mac.
When TodoEditor's "Add" button is tapped, the preview screen should show a ContentView and the ContentView should show updated Todo contents but it shows the ContentView and the updated Todo contents are not in it, then just it stops.
If I use simulator, it works perfectly. So the problem happens only on Xcode' preview screen.
If I delete
data.todos.append(newTodo)
from TodoEditor.swift, dismiss() works, the preview shows a ContentView and the preview does not stops. Of course todo content is not updated.
What can I do for this?
My source codes are followed.
Thank you.
MyApp.swift
import SwiftUI
#main
struct MyApp: App {
#StateObject var data = Todo()
var body: some Scene {
WindowGroup {
NavigationView{
ContentView()
.navigationTitle("My Todo")
}.environmentObject(data)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#EnvironmentObject var data: Todo
var body: some View {
List{
ForEach(data.todos){todo in
NavigationLink{
VStack{
Text(todo.taskContent)
}
}label: {
HStack{
Text(todo.taskContent)
Text(todo.isCompleted ? "done" : "not yet")
}
}
}
}.toolbar{
ToolbarItem{
NavigationLink("Add"){
TodoEditor()
.navigationTitle("Add Todo")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
ContentView()
.environmentObject(Todo())
}
}
}
TodoEditor.swift
import SwiftUI
struct TodoEditor: View {
#State var newTodo:Task = Task(taskContent: "", isCompleted: false)
#EnvironmentObject var data:Todo
#Environment(\.dismiss) var dismiss
var body: some View {
VStack{
Form{
Section("Task Content"){
TextField("Task Content",text: $newTodo.taskContent)
}
Section("Complete?"){
Toggle("Complete?",isOn: $newTodo.isCompleted)
}
}
}.toolbar{
ToolbarItem{
Button("Add"){
data.todos.append(newTodo) //<- New todo is added here
dismiss() //<- Here is dismiss()
}
}
}
}
}
struct TodoEditor_Previews: PreviewProvider {
#Environment(\.dismiss) var dismiss
static var previews: some View {
NavigationView{
TodoEditor(newTodo: Task(taskContent: "Hello", isCompleted:false))
.environmentObject(Todo())
}
}
}
Todo.swift
import SwiftUI
class Todo: ObservableObject{
#Published var todos:[Task] = []
}
struct Task: Identifiable{
var taskContent: String
var isCompleted: Bool
var id = UUID()
}

How do I store the Input of a Textfield and display it in another View in Swift UI?

I am just learning to code and I have a question. How do I store the Input data of a Textfield and display it in another View? I tried it with Binding but it doesn't work that way. I appreciate your help
import SwiftUI
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
// the name should be diplayed here!
Text(name)
}
}
struct DisplayUserName_Previews: PreviewProvider {
static var previews: some View {
DisplayUserName()
}
}
State should always be stored in a parent and passed down to the children. Right now, you're not showing the connection between the two views (neither reference the other), so it's a little unclear how they relate, but there are basically two scenarios:
Your current code would work if DisplayUserName was the parent of SelectUserName:
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
Text(name)
SelectUserName(name: $name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
Or, if they are sibling views, the state should be stored by a common parent:
struct ContentView : View {
#State private var name = ""
var body: some View {
SelectUserName(name: $name)
DisplayUserName(name: name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
var name : String //<-- Note that #State isn't needed here because nothing in this view modifies the value
var body: some View {
Text(name)
}
}

Getting "Out of range error" in a preview SwiftUI

I'm learning SwiftUI and are mapping my app, but I'm having an error with one of the previews.
Hope you can help me figure it out.
Here is my model:
import Foundation
class RecipeModel: ObservableObject {
#Published var recipes = [Recipe]()
init() {
// Parse local included json data
getLocalData()
// Download remote json file and parse data
getRemoteData()
}
Then I have this on Main
import SwiftUI
#main
struct ReallyMexican: App {
var body: some Scene {
WindowGroup {
HomeView()
.environmentObject(RecipeModel())
}
}
}
And this on the view I'm getting the error:
import SwiftUI
struct RecipeSearchView: View {
var recipe:Recipe
#EnvironmentObject var model: RecipeModel
var body: some View {
VStack {
HStack {
Image(recipe.image)
.resizable()
.scaledToFit()
Text(recipe.name)
}
}
.padding()
}
}
struct RecipeSearchView_Previews: PreviewProvider {
static var previews: some View {
let model = RecipeModel()
RecipeSearchView(recipe: model.recipes[0])
}
}
And I use that view here:
import SwiftUI
struct SearchView: View {
#EnvironmentObject var model: RecipeModel
var body: some View {
VStack (alignment: .leading){
Text("Hello")
NavigationView{
List {
ForEach(model.recipes) { item in
RecipeSearchView(recipe: item)
}
}
}
}
.padding()
}
}
struct SearchView_Previews: PreviewProvider {
static var previews: some View {
SearchView()
.environmentObject(RecipeModel())
}
}
The project Build Successfully and I can run it in the simulator.
But on the RecipeSearchView, I'm getting this error:, can't figure out why 🤷‍♂️
"App Crash due to Our of Range Index”
Thanks in advance

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

Resources