SwiftUI passing an observed object into a new view and getting updates - swift

I am very new to swift working on my first app and having trouble having a view update. I am passing an object into a new view, however the new view does not update when there is change in the Firebase Database. Is there a way to get updates on the Gridview? I though by passing the observed object from the StyleboardView it would update the GridView however Gridview does not update. I am having trouble finding a way for the new Gridview to update and reload the images.
struct StyleBoardView: View {
#State private var showingSheet = false
#ObservedObject var model = ApiModel()
#State var styleboardname = ""
let userEmail = Auth.auth().currentUser?.email
var body: some View {
NavigationView {
VStack {
Text("Select Style Board")
List (model.list) {item in
Button(item.styleboardname) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
GridView(item: item)
}
}
struct GridView: View {
var item: Todo
#ObservedObject var model = ApiModel()
#State var newImage = ""
#State var loc = ""
#State var shouldShowImagePicker = false
#State var image: UIImage?
var body: some View {
NavigationView {
var posts = item.styleboardimages
VStack(alignment: .leading){
Text(item.styleboardname)
GeometryReader{ geo in
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 3 ){
ForEach(posts.sorted(by: <), id: \.key) { key, value in
if #available(iOS 15.0, *) {
AsyncImage(url: URL(string: value), transaction: Transaction(animation: .spring())) { phase in
switch phase {
case .empty:
Color.purple.opacity(0.1)
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure(_):
Image(systemName: "exclamationmark.icloud")
.resizable()
.scaledToFit()
#unknown default:
Image(systemName: "exclamationmark.icloud")
}
}
.frame(width: 100, height: 100)
.cornerRadius(20)

You have a few problems with the code. First of all, the original view that creates the view model, or has created for it originally, should own the object. Therefore you declare it as a #StateObject.
struct StyleBoardView: View {
#State private var showingSheet = false
#StateObject var model = ApiModel() // #StateObject here
#State var styleboardname = ""
let userEmail = Auth.auth().currentUser?.email
var body: some View {
NavigationView {
VStack {
Text("Select Style Board")
List ($model.list) { $item in // Change this to pass a Binding
Button(item.styleboardname) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
GridView(item: $item, model: model)
}
}
}
}
}
}
Since you are passing to a .sheet, that will not automatically be re-rendered when StyleBoardView's model changes, so you have to use a #Binding to cause GridView to re-render. Lastly, once you have your #StateObject, you pass that to your next view. Otherwise, you continually make new models, so updates to one will not update the other.
struct GridView: View {
#Binding var item: Todo // Make this a #Binding so it reacts to the changes.
#ObservedObject var model: ApiModel // Pass the originally created view model in.
...
var body: some View {
NavigationView {
...
}
}
}
Lastly, you did not post a Minimal, Reproducible Example (MRE). You also did not post the complete GridView struct. You may not even need your view model in that view as you do not use it in what you have posted.

The problem is that you're initializing the model in an ObservedObject, and passing it down to another initialized Observed Object.
What you actually wanna do is use an #StateObject for where you initialize the model. And then use #ObservedObject with the type of the model you're passing down so that:
struct StyleBoardView: View {
#StateObject var model = ApiModel()
/** Code **/
struct GridView: View {
#ObservedObject var model: ApiModel
Notice the difference, an #ObservedObject should never initialize the model, it should only "inherit" (#ObservedObject var model: ApiModel) a model from a parent View, in this case, ApiModel.

Related

Propertly break down and pass data between views

So I'm still learning Swift and I wanted to cleanup some code and break down views, but I can't seem to figure out how to pass data between views, so I wanted to reach out and check with others.
So let's say that I have MainView() which previously had this:
struct MainView: View {
#ObservedObject var model: MainViewModel
if let item = model.selectedItem {
HStack(alignment: .center, spacing: 3) {
Text(item.title)
}
}
}
Now I created a SecondView() and changed the MainView() content to this:
struct MainView: View {
#ObservedObject var model: MainViewModel
if let item = model.selectedItem {
SecondView(item: item)
}
}
Inside SecondView(), how can I access the item data so that I can use item.title inside SecondView() now?
In order to pass item to SecondView, declare item as a let property and then when you call it with SecondView(item: item), SecondView can refer to item.title.
Here is a complete example expanding on your code:
import SwiftUI
struct Item {
let title = "Test Title"
}
class MainViewModel: ObservableObject {
#Published var selectedItem: Item? = Item()
}
struct MainView: View {
#ObservedObject var model: MainViewModel
var body: some View {
if let item = model.selectedItem {
SecondView(item: item)
}
}
}
struct SecondView: View {
let item: Item
var body: some View {
Text(item.title)
}
}
struct ContentView: View {
#StateObject private var model = MainViewModel()
var body: some View {
MainView(model: model)
}
}

Trouble passing data between views using EnvironmentObject in SwiftUI

I'm having trouble passing data through different views with EnvironmentObject. I have a file called LineupDetails that contains the following:
import SwiftUI
class TeamDetails: ObservableObject {
let characterLimit = 3
#Published var TeamName: String = "" {
didSet {
if TeamName.count > characterLimit {
TeamName = String(TeamName.prefix(characterLimit))
}
}
}
#Published var TeamColor: Color = .blue
#Published var hitter1First: String = ""
#Published var hitter1Last: String = ""
#Published var hitter2First: String = ""
#Published var hitter2Last: String = ""
#Published var hitter3First: String = ""
#Published var hitter3Last: String = ""
#Published var hitter4First: String = ""
#Published var hitter4Last: String = ""
#Published var hitter5First: String = ""
#Published var hitter5Last: String = ""
#Published var hitter6First: String = ""
#Published var hitter6Last: String = ""
#Published var hitter7First: String = ""
#Published var hitter7Last: String = ""
#Published var hitter8First: String = ""
#Published var hitter8Last: String = ""
#Published var hitter9First: String = ""
#Published var hitter9Last: String = ""
#Published var Hitter1Pos = "P"
#Published var Hitter2Pos = "C"
#Published var Hitter3Pos = "1B"
#Published var Hitter4Pos = "2B"
#Published var Hitter5Pos = "3B"
#Published var Hitter6Pos = "SS"
#Published var Hitter7Pos = "LF"
#Published var Hitter8Pos = "CF"
#Published var Hitter9Pos = "RF"
}
These variables are edited through a form in SetHomeLineup. I have excluded the parts of the view not related to the problem, marking them with ...:
struct SetHomeLineup: View {
#EnvironmentObject var HomeTeam: TeamDetails
...
var body: some View {
HStack {
TextField("Name", text: $HomeTeam.hitter1First)
TextField("Last Name", text: $HomeTeam.hitter1Last)
Picker(selection: $HomeTeam.Hitter1Pos, label: Text("")) {
ForEach(positions, id: \.self) {
Text($0)
}
}
}
HStack {
TextField("Name", text: $HomeTeam.hitter2First)
TextField("Last Name", text: $HomeTeam.hitter2Last)
Picker(selection: $HomeTeam.Hitter2Pos, label: Text("")) {
ForEach(positions, id: \.self) {
Text($0)
}
}
}
...
}
// and textfields so on until Hitter9, first and last
Now, when I try to include the inputted values of the above text fields to a different view, with code like this, the view always appears empty to match the default value of the string.
struct GameView: View {
#EnvironmentObject var HomeTeam: TeamDetails
...
var body: some View {
Text(HomeTeam.hitter1First)
}
}
Can someone tell me what I'm doing wrong? I tried using a similar code in a fresh project and it seemed to work just fine, so I'm stumped.
EDIT:
My views are instantiated like so:
The first view of the app is the SetAwayLineup, which includes a NavigationLink to SetHomeLineup like so.
var details = TeamDetails()
NavigationLink(destination: SetHomeLineup().environmentObject(details), isActive: self.$lineupIsReady) {
Similarly, SetHomeLineup includes a navigation link to GameView like so
var details = TeamDetails()
NavigationLink(destination: GameView().environmentObject(details), isActive: self.$lineupIsReady) {
Both screens have an EnvironmentObject of AwayLineup and HomeLineup that I'm trying to call into GameView.
Hopefully this simplifies it
The trunk is injected in the NavigationView and doesn't need to be re-injected. Even if one of the children doesn't use it. Truck belongs to the NavigationView
Then I have created 2 branches A and B that have their own Objects A cannot see Bs and vicecersa.
Each branch has access to their object and the sub-branches (NavigationLink) can be connected to the branch's object by injecting it.
import SwiftUI
struct TreeView: View {
#StateObject var trunk: Trunk = Trunk()
var body: some View {
NavigationView{
List{
NavigationLink(
destination: BranchAView(),
label: {
Text("BranchA")
})
NavigationLink(
destination: BranchBView(),
label: {
Text("BranchB")
})
}.navigationTitle("Trunk")
}
//Available to all items in the NavigationView
//With no need to re-inject for all items of the navView
.environmentObject(trunk)
}
}
///Has no access to BranchB
struct BranchAView: View {
#StateObject var branchA: BranchA = BranchA()
#EnvironmentObject var trunk: Trunk
var body: some View {
VStack{
Text(trunk.title)
Text(branchA.title)
NavigationLink(
destination: BranchAAView()
//Initial injection
.environmentObject(branchA)
,
label: {
Text("Go to Branch AA")
})
}.navigationTitle("BranchA")
}
}
//Has no access to BranchA
struct BranchBView: View {
#StateObject var branchB: BranchB = BranchB()
#EnvironmentObject var trunk: Trunk
var body: some View {
VStack{
Text(trunk.title)
Text(branchB.title)
NavigationLink(
destination: BranchBBView()
//Initial injection
.environmentObject(branchB),
label: {
Text("Go to Branch BB")
})
}.navigationTitle("BranchB")
}
}
struct BranchAAView: View {
#EnvironmentObject var branchA: BranchA
#EnvironmentObject var trunk: Trunk
var body: some View {
VStack{
Text(trunk.title)
Text(branchA.title)
NavigationLink(
destination: BranchAAAView()
//Needs re-injection because it is a NavigationLink sub-branch
.environmentObject(branchA)
,
label: {
Text("Go to AAA")
})
}.navigationTitle("BranchAA")
}
}
struct BranchAAAView: View {
#EnvironmentObject var branchA: BranchA
#EnvironmentObject var trunk: Trunk
var body: some View {
VStack{
Text(trunk.title)
Text(branchA.title)
}.navigationTitle("BranchAAA")
}
}
struct BranchBBView: View {
var body: some View {
VStack{
Text("I don't need to use the trunk or branch BB")
//No need to re-inject it is the same branch
BranchBBBView()
}.navigationTitle("BranchBB")
}
}
struct BranchBBBView: View {
#EnvironmentObject var branchB: BranchB
#EnvironmentObject var trunk: Trunk
var body: some View {
VStack{
Text("BranchBBBView").font(.title).fontWeight(.bold)
Text(trunk.title)
Text(branchB.title)
}.navigationTitle("BranchBB & BranchBBB")
}
}
struct TreeView_Previews: PreviewProvider {
static var previews: some View {
TreeView()
}
}
class Trunk: ObservableObject {
var title: String = "Trunk"
}
class BranchA: ObservableObject {
var title: String = "BranchA"
}
class BranchB: ObservableObject {
var title: String = "BranchB"
}
You need to make sure that you're passing the same environment object to any views that need to share the same data. That means you should create the object and store it in a variable so that you can pass it to both views. From your comments you have:
NavigationLink(destination: GameView(testUIView:
sampleGameViews[0]).environmentObject(TeamDetails()),
isActive: self.$lineupIsReady { ...
That TeamDetails() constructs a new instance of the TeamDetails class, one that you haven't stored. That means you must also be doing the same for your SetHomeLineup view. Instead, you'll need to create a single instance and keep a reference to it, then pass that reference to any views that you want to share the same data:
var details = TeamDetails()
that should be the only place where you use TeamDetails(); use details when you're setting up your GameView and SetHomeLineup views.
Update: Given your edit, the problem is again clear. You're still instantiating the TeamDetails class twice, so that the two views still get their own separate instances of that class. They need to use the same instance if they're to share information. So there should be only one var details = TeamDetails() line, and the resulting details variable should be used as the environmentObject for both views.
For example, instead of:
var details = TeamDetails()
NavigationLink(destination: SetHomeLineup().environmentObject(details), isActive: self.$lineupIsReady) {...
//...
var details = TeamDetails()
NavigationLink(destination: GameView().environmentObject(details), isActive: self.$lineupIsReady) {
you want:
var details = TeamDetails()
NavigationLink(destination: SetHomeLineup().environmentObject(details), isActive: self.$lineupIsReady) {...
NavigationLink(destination: GameView().environmentObject(details), isActive: self.$lineupIsReady) {
In general, make sure that you have a clear understanding of the difference between reference types and value types, and between a class and an instance of that class. If Mazda Miata is a class, and if I buy a Miata, then the specific car that I've is an instance of the class. If I ask you to wash my car, and I ask someone else to change the oil in my car, I'll end up with one car that's clean and has new oil. What's going on in your code is that you're buying a Miata and asking someone to wash it, and buying another car and asking someone to change the oil. They're two different cars, so they have different states.

No ObservableObject of type <TypeName> found. A View.environmentObject(_:) for <TypeName> may be missing as an ancestor of this view

I have three views (AddMatchView, TeamPickerView and TeamsOfCountryView). Everything should work like this: from AddTeamView I go to TeamPickerView, there I select the country and go to TeamsOfCountryView, after tapping on the team I need, both views (TeamPickerView and TeamsOfCountryView) should immediately close thanks to the common Bool variable, and the selected team (an object of the Team type) passed to the parent AddMatchView. But after selecting a team, only TeamsOfCountryView is closed, and an error occurs in TeamPickerView: Fatal error: No ObservableObject of type DBService found. A View.environmentObject (_ :) for DBService may be missing as an ancestor of this view.
Everything worked as it should until I decided to create a ViewModel for my view. i.e. if I transfer #State variables from AddMatchView to TeamPickerView, then errors will not stink, but I use properties from #ObservedObject
Main parent view:
struct AddMatchView: View {
#ObservedObject var viewModel = AddMatchViewModel()
#State isPresented = false //This works!
#State team: Team? //This works!
var body: some View {
//...
Button(action: { viewModel.isPresented.toggle() }){
//...
}.fullScreenCover(isPresented: $viewModel.isPresented) { TeamPickerView(team: $viewModel.home, isPresented: $viewModel.isPresented)}
//.fullScreenCover(isPresented: $isPresented) { TeamPickerView(team: $home, isPresented: $isPresented)} THIS WORKS!!
//...
}
ViewModel after using of which the error began to occur:
class AddMatchViewModel: ObservableObject{
#Published var home: Team?
#Published var isPresented = false
//...
}
TeamsPickerView:
struct TeamPickerView: View {
#EnvironmentObject var db: DBService
#Binding var team: Team?
#Binding var isPresented: Bool
#State private var searchText = ""
var body: some View {
NavigationView {
VStack{
SearchBar(text: $searchText)
Form{
//ERROR in below line after selecting team in child TeamsOfCountryView: No ObservableObject of type DBService found. A View.environmentObject(_:) for DBService may be missing as an ancestor of this view.
List (db.countries.filter({ searchText.isEmpty ? true : $0.name.contains(searchText) })) { country in
NavigationLink(destination: TeamsOfCountryView(countryID: country.documentID, team: $team, isPresented: $isPresented)) {
HStack{
Image(uiImage: Flag(countryCode: country.code)!.image(style: .roundedRect)).resizable().scaledToFit().frame(maxWidth: 30, maxHeight: 30)
Text(country.name)
}
}
}
}
}
.navigationBarTitle(Text("Countries"))
.navigationBarItems(trailing: Button(action: {isPresented = false }){
Text("Close")
})
}
}
}
TeamsOfCountryView:
struct TeamsOfCountryView: View {
#EnvironmentObject var db: DBService
#Binding var team: Team?
#Binding var isPresented: Bool
//...
var body: some View {
VStack{
//...
Form{
List (teams.filter({ searchText.isEmpty ? true : $0.name.contains(searchText) })) { team in
Button(action: {
//After that, an error occurs in the parent TeamPickerView
self.team = team
self.isPresented = false
}){
//...
}
}
}
//...
}
}
LITTLE UPDATE: simplified the example as much as possible
class ViewModel: ObservableObject{
#Published var isPresented = false
}
class EnvObj: ObservableObject{
#Published var foo = "test"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
//#State var isPresented = false no error if we use it instead of viewModel.isPresented
var body: some View {
Button("Open Child View A"){
viewModel.isPresented.toggle()
}.fullScreenCover(isPresented: $viewModel.isPresented){ChildViewA(isPresented: $viewModel.isPresented)}
}
}
struct ChildViewA: View {
#EnvironmentObject var envObj: EnvObj
#Binding var isPresented: Bool
#State var openChildB = false
var body: some View {
NavigationView{
VStack{
//ERROR HERE: No ObservableObject of type EnvObj found. A View.environmentObject(_:) for EnvObj may be missing as an ancestor of this view.
Text(envObj.foo)
NavigationLink(destination: ChildViewB(isPresented: $isPresented)){
Text("Open Child View B")
}
}
}
}
}
struct ChildViewB: View {
#EnvironmentObject var envObj: EnvObj
#Binding var isPresented: Bool
var body: some View {
Button("Close Child View A and B"){
isPresented = false
}
}
}
You need to inject the #EnvironmentObject into each environment (remember that each .sheet or .fullScreenCover creates a new environment):
Button("Open Child View A"){
viewModel.isPresented.toggle()
}
.fullScreenCover(isPresented: $viewModel.isPresented) {
ChildViewA(isPresented: $viewModel.isPresented)
.environmentObject(EnvObj()) // inject here
}
Note: the EnvironmentObject must be created first. You can't access it by #EnvironmentObject var envObj: EnvObj if it isn't created and injected in the first place.
Also, you don't really need to create your dependencies directly in the fullScreenCover closure. You can put them at the root level and inject accordingly.
My best guess is that you need a strong reference to your DBService object when you pass it in to the ContentView. In the SceneDelegate (or App object) where you declare your content view, instead of ContentView(DBService()) do this:
let dbService = DBService() // <- strong reference, stored property at class/struct level
func scene() {
ContentView(dbService) // <- this is the stored instance, not a brand new DBService
}
// (or)
var body: some Scene {
ContentView(dbService) // <- this is the stored instance, not a brand new DBService
}

Reload aync call on view state update

I have the following view:
struct SpriteView: View {
#Binding var name: String
#State var sprite: Image = Image(systemName: "exclamationmark")
var body: some View {
VStack{
sprite
}
.onAppear(perform: loadSprite)
}
func loadSprite() {
// async function
getSpriteFromNetwork(self.name){ result in
switch result {
// async callback
case .success(newSprite):
self.sprite = newSprite
}
}
}
What I want to happen is pretty simple: a user modifies name in text field (from parent view), which reloads SpriteView with the new sprite. But the above view doesn't work since when the view is reloaded with the new name, loadSprite isn't called again (onAppear only fires when the view is first loaded). I also can't put loadSprite in the view itself (and have it return an image) since it'll lead to an infinite loop.
There is a beta function onChange that is exactly what I'm looking for, but it's only in the beta version of Xcode. Since Combine is all about async callbacks and SwiftUI and Combine are supposed to play well together, I thought this sort of behavior would be trivial to implement but I've been having a lot of trouble with it.
I don't particular like this solution since it requires creating a new ObservableObject but this how I ended up doing it:
class SpriteLoader: ObservableObject {
#Published var sprite: Image = Image(systemName: "exclamationmark")
func loadSprite(name: String) {
// async function
self.sprite = Image(systemName: "arrow.right")
}
}
struct ParentView: View {
#State var name: String
#State var spriteLoader = SpriteLoader()
var body: some View {
SpriteView(spriteLoader: spriteLoader)
TextField(name, text: $name, onCommit: {
spriteLoader.loadSprite(name: name)
})
}
}
struct SpriteView: View {
#ObservedObject var spriteLoader: SpriteLoader
var body: some View {
VStack{
spriteLoader.sprite
}
}
}
Old answer:
I think the best way to do this is as follows:
Parent view:
struct ParentView: View {
#State var name: String
#State spriteView = SpriteView()
var body: some View {
spriteView
TextField(value: $name, onCommit: {
spriteView.loadSprite(name)
})
}
And then the sprite view won't even need the #Binding name member.

Passing filtered #Bindable objects to multiple views in SwiftUI

I’m trying to pass a filter array to multiple views, but the filtering is not working. If I remove the filter, you can pass the array to the next view, but that leads to another error during the ForEach loop. I've posted all the code below.
Does anyone know how you can pass a filter version of a #Bindable array? Also why can't I print sport.name and sport.isFavorite.description in the ForEach loop?
I’m using swiftUI on Xcode 11.0 beta 5.
import SwiftUI
import Combine
struct Sport: Identifiable{
var id = UUID()
var name : String
var isFavorite = false
}
final class SportData: ObservableObject {
#Published var store =
[
Sport(name: "soccer", isFavorite: false),
Sport(name: "tennis", isFavorite: false),
Sport(name: "swimming", isFavorite: true),
Sport(name: "running", isFavorite: true)
]
}
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store.filter({$0.isFavorite}))
}
}
}
struct TestingTwo: View {
#Binding var sports : [Sport]
var body: some View {t
NavigationView {
VStack(spacing: 10){
ForEach($sports) { sport in
NavigationLink(destination: TestingThree(sport: sport)){
HStack {
Text(sport.name)
Spacer()
Text(sport.isFavorite.description)
}
.padding(.horizontal)
.frame(width: 200, height: 50)
.background(Color.blue)
}
}
}
}
}
}
struct TestingThree: View {
#Binding var sport : Sport
var body: some View {
VStack {
Text(sport.isFavorite.description)
.onTapGesture {
self.sport.isFavorite.toggle()
}
}
}
}
#if DEBUG
struct Testing_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
#endif
Filtering in your case might be better placed in the navigation view, due to your binding requirements.
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store)
}
}
}
struct TestingTwo: View {
#Binding var sports : [Sport]
#State var onlyFavorites = false
var body: some View {t
NavigationView {
VStack(spacing: 10){
ForEach($sports) { sport in
if !self.onlyFavorites || sport.value.isFavorite {
NavigationLink(destination: TestingThree(sport: sport)){
HStack {
Text(sport.value.name)
Spacer()
Text(sport.value.isFavorite.description)
}
.padding(.horizontal)
.frame(width: 200, height: 50)
.background(Color.blue)
}
}
}
}
}
}
}
Now you can switch the isFavorite state either within the action implementation of a button, or while specifying the integration of you TestingTwo view.
struct Testing: View {
#ObservedObject var sports = SportData()
var body: some View {
VStack {
TestingTwo(sports: $sports.store, onlyFavorites: true)
}
}
}
Regarding the second part of your question: Note the value addendum in the ForEach loop. You're dealing with as binding here (as ForEach($sports) indicates), hence sport is not an instance of Sport.
You can't get a #Binding from a computed property, since the computed property is computed dynamically. A typical way to avoid this is to pass in ids of the sports objects and the data store itself, whereby you can access the sports items via id from the store.
If you really want to pass a #Binding in you have to remove the filter (pass in an actually backed array) and modfy the ForEach like the following:
ForEach($sports.store) { (sport: Binding<Sport>) in