Getting "Out of range error" in a preview SwiftUI - xcode

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

Related

.navigationBarTitleDisplayMode(.inline) crashes Xcode preview

I am consistently coming across this issue whenever I use .navigationBarTitleDisplayMode(.inline). Is there a problematic use of this method, or is there something incorrect about my code ?
How to reproduce error:
In the preview, hit the "list.bullet.rectangle.fill" and dismiss the dialog. Do it a second time and now Xcode crashes the Preview.
Xcode Version 14.2 (14C18)
import SwiftUI
struct PDFTableContentsView: View {
var body: some View {
Text("PDF Table Contents View")
.bold()
.underline()
}
}
struct PDFContentView: View {
#State private var showContents: Bool = false
var body: some View {
VStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
.navigationTitle("PDF Title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showContents.toggle()
} label: {
Image(systemName: "list.bullet.rectangle.fill")
}
}
}
.sheet(isPresented: $showContents) {
PDFTableContentsView()
}
}
}
struct PDFContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
PDFContentView()
}
}
}

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

Send data to next view - SwiftUI

So I have been trying to get a simple pass of data working with SwiftUI.
Basically the below script prints out a list of items (in a HStack) I then have each one linked to our Podcast() view.
What I am trying to do is pass through the podcast name to the next view. How do I achieve
this? As all the examples are about int which I am not using am using a String.
import SwiftUI
import RemoteImage
struct ContentView: View {
#State private var showAlert = false
#State var posts: [Program] = []
var body: some View {
NavigationView {
if posts.isEmpty {
Text("Loading")
} else {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .bottom, spacing: 10) {
ForEach(posts) { post in
//return
NavigationLink(destination: Podcasts()){
RemoteImage(type: .url(URL(string:post.icon)!), errorView: { error in
Text(error.localizedDescription)
}, imageView: { image in
image
.resizable()
.renderingMode(.original)
/* .clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.red, lineWidth: 5))*/
.aspectRatio(contentMode: .fit)
.frame(width:200, height:200)
}, loadingView: {
Text("Loading ...")
})
}
}
}.frame(height: 200)
}.frame(height: 200)
}
}.onAppear {
Api().getPosts { (posts) in
self.posts = posts
}
}.navigationBarTitle(Text("Home"))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The podcast View
import SwiftUI
struct Podcasts: View {
var body: some View {
NavigationView{
Text("Hello")
}.navigationBarTitle(Text("Podcast"))
}
}
struct Podcasts_Previews: PreviewProvider {
static var previews: some View {
Podcasts()
}
}
Pass post as constructor argument, like
ForEach(posts) { post in
//return
NavigationLink(destination: Podcasts(post: post)){
so now
struct Podcasts: View {
let post: Program
var body: some View {
// !! DON'T ADD SECOND NAVIGATION VIEW IN STACK
// !! - THERE MUST BE ONLY ONE
Text(post.name)
.navigationBarTitle(Text("Podcast"))
}
}

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.

Resources