how to use EnvironmentObject in a Swiftui View function? - ios

Resolved
Finally, I found the problem. My questioned View was placed in a .sheet. If I show this View separately, the environment variables will work, but if they are placed in .sheet, the Store must be explicitly injected in.
.sheet(isPresented: $showEditSheet){
EditFavoriteRootView().environmentObject(Store())
}
Now It's worked!
old quesition
Because ForEach couldn't support too complicated logic, I defined a function in the View to implement the content.
In most cases, this is fine. However, I cannot use the EnviromentObject in the function。
build success , the button in View body works fine, but when I click button in function got the runtime error:
Fatal error: No ObservableObject of type Store found.
A View.environmentObject(_:) for Store may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros/Monoceros-
Code sample:
struct FromCategory: View {
#EnvironmentObject var store:Store
var body: some View {
ForEach(AppState.HealthCategoryList.keys.sorted(by: <),id:\.self){
self.ShowList(categoryId: $0)
}
Button(action:{
self.store.dispatch(.updateFavorite(idName:"test"))
}){Text("Test")}
}
func ShowList(categoryId:String)->some View{
let metalist = AppState.HealthCategoryList[categoryId]?.metaData?.sortedArray(using: [NSSortDescriptor(key: "idName", ascending: true)]) as! [HealthMetaData]
return VStack{
VStack(spacing:10) {
ForEach(0..<metalist.count){i in
HStack{
Text(metalist[i].title!)
Spacer()
Button(action:{
self.store.dispatch(.updateFavorite(idName: metalist[i].idName!))
//self.store.appState.updateFavorite(idName: metalist[i].idName!)
}){
if AppState.UserFavoriteList[metalist[i].idName!] != nil {
Image(systemName: "star.fill")
}
else {
Image(systemName: "star")
}
}
}
}
}
}
}
}
Fatal error: No ObservableObject of type Store found. in this line
self.store.dispatch(.updateFavorite(idName: metalist[i].idName!))
store can be use well in normal some View, but not in function.
How to solve this. Or is there any other way?
Thanks

This is happening because you want to update an EnvironmentObject and you're not in the View which was loaded by the SceneDelegate.swift
In case you're using the EnvObject in a correct way, you just need to pass te EnvObject to the next view like this:
DestinationView().environmentObject(self.yourEnvObject)
More information explained in my post:
https://stackoverflow.com/a/58997326/12378791

Related

SwiftUI - Error with content in a view component

I'm trying to build a reusable navigation tab in SwiftUI and I'm facing some challenges.
I come from ReactJS and wanted to create a component that I can pass the tab images and the different views.
This is the code I have so far which works pretty well:
struct Navigation<Content: View>: View {
#State var selectedIndex = 0
var tabBarImageNames: [String]
let content: [Content]
init(tabBarImageNames: [String], _ content: Content...) {
self.content = content
self.tabBarImageNames = tabBarImageNames
}
var body: some View {
VStack{
ZStack {
content[selectedIndex]
}
Spacer()
HStack {
ForEach(0 ..< tabBarImageNames.count) { tabNum in
[ ... ]
}
}
}
}
}
I can call Navigation with these arguments an everything works as expected:
Navigation(
tabBarImageNames: ["house.fill", "chart.bar.xaxis", "target", "bubble.left"],
VStack {
Text("First view")
.navigationTitle("First Tab")
},
VStack {
Text("Second view")
.navigationTitle("Second Tab")
},
VStack {
Text("Third view")
.navigationTitle("Third Tab")
},
VStack {
Text("Fourth view")
.navigationTitle("Fourth Tab")
}
)
The real problem 🚨
The issue with the above code is that if the Views I'm passing as arguments are not exactly the same (when I say exactly I literally mean exactly the same type and number of components), Xcode complains with the following error:
I'm pretty sure it's something not very difficult to solve, but given my short experience with this language I'm struggling to find the right answer. I tried to change the init content protocol to AnyView instead of Content among other things, but no luck so far.
Looking for help 🗜
Anyone with SwiftUI experience that can help me out and explain me what I'm missing? Also open to feedback on improvements on the code.
Can't use any type of library or external package btw, everything has to be built manually.
Thanks Stack overflow!

SwiftUI retuning List or NavigationView in body: difference of return List and List in body

I have question about returning NavigationView and List inside of SwiftUI body
There are code snippets like this:
var body: some View {
let trailingItem = HStack {
Button(action: { print("Button 1") }) {
Image(systemName: "bell")
}
Button(action: { print("Button 2") }) {
Image(systemName: "square.and.arrow.up")
}
}
return NavigationView {
Image("SwiftUI")
.navigationBarItems(trailing: trailingItem)
.navigationBarTitle("Title")
}
}
and this
var body: some View {
let numbers = [1, 2, 3, 4, 5]
return List(numbers, id: \.self) {
Text("\(String(describing: $0))")
}
}
In these two code snippets, I can find return keyword for NavigationView and List
To figure out what does that grammar do, I tried deleting return keyword but nothing happened
I want to know:
What does that return keyword do?
What is difference between using return for NavigationView and List
Thanks in advance for any help you are able to provide.
Some of the features in SwiftUI's syntax are made possible by something called a View Builder which is a type of Result Builder
When using a View Builder, functions and computed properties have an implicit return (ie the last statement is returned even without the return keyword). There are also other specific rules about what kinds of implicit code can be included in a View Builder, but let declarations like your examples contain are fine. See https://swiftwithmajid.com/2019/12/18/the-power-of-viewbuilder-in-swiftui/ and https://swiftontap.com/viewbuilder for more information.
SwiftUI Views are an interesting special case because the var body property of the View is interpreted to be a ViewBuilder even though we don't have to explicitly annotate it with #ViewBuilder. However, if you were to try the same thing in a regular function, you would need to use that #ViewBuilder to get a similar result if the function had more than one statement (one-line functions in Swift have implicit returns as well).
#ViewBuilder func myView() -> some View {
Text("test")
Text("Another item")
}
So, in your code examples (since you were in var body), there is no difference between using return and omitting it.

SwiftUI View Code is seemingly getting called twice. What is the issue here?

I have a view that is being called using .sheet in SwiftUI. However, when this sheet is presented, I'm getting the debug "Tapped" print in console followed by this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c9bdf5c22SheetHostingControllerVS_7AnyView_: 0x7f8f1d7400f0> on <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8f17e0dae0> which is already presenting (null)
I'm not exactly sure what is causing this error, but from what I understand it's due to the view getting called twice. I'm not sure how the view is being called twice, or even if it is, which is why I'm asking here. Below is the main view that actually houses the NavigationView in which my List is being housed view
struct AllTablesView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State private var tableTapped: Table = Table()
#State private var isModal = false
var body: some View {
NavigationView {
List {
ForEach(self.tables) { table in
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
.onTapGesture {
self.tableTapped = table
self.isModal = true
print("tapped")
}
}
.onDelete { indexSet in
let deletedTable = self.tables[indexSet.first!]
self.managedObjectContext.delete(deletedTable)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.sheet(isPresented: $isModal){
TableStatusView(table: self.tableTapped)
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
.navigationBarTitle("All Tables")
.navigationBarItems(leading: EditButton(), trailing:
HStack {
DeleteAllTablesButton()
AddTableButton()
})
}
}
}
And, if for whatever reason this is the issue, here is the code for my view titled "TableStatusView"
struct TableStatusView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State var table: Table
var body: some View {
VStack {
Form {
Section {
Text("Table \(table.number)")
}.padding(.all, 10.0)
Section {
Toggle(isOn: $table.inUse) {
Text("In Use")
}.padding(.all, 10.0)
Toggle (isOn: $table.clean) {
Text("Clean")
}.padding(.all, 10.0)
}
}
}
}
}
Is there a reason I'm getting this error, or why my view is being called twice? Is it actually that it's being called twice or am I doing something wrong here? I know certain parts of my code could probably be shrunk down or written better but this is just my first dive into SwiftUI, I haven't had much experience with it outside of basic stuff. Thanks!
I never did find a solution to the question, but I did solve the NavigationLink issue and swapped back to using that. I was using ".onTapGesture" to get a tap gesture and then generate a NavigationLink, thinking it was an action. NavigationLink, I've learned, is actually more or less a container to house content, so I replaced the .onTapGesture and TableCellView() function call with the following:
NavigationLink(destination: TableStatusView(table: table)) {
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
}
And fixed the issue for me. I'm still seeing some errors but from some google-fu found out that they are current bugs of SwiftUI. SwiftUI 2.0 may fix some of these issues

Swiftui seems not to work correctly with Cocoapods and Firebase

I'm trying to create an iOS App using SwiftUI. My data is stored within a Google Firebase realtime database. I fetch my data to an ObservableObject so that the UI can be loaded dynamically, if data get's added or removed.
But for some reason I always getting the error "Type of expression is ambiguous without more context" for a Text().
I've already tried to remove the if statements and the Text() but then I'm getting errors in other places. E.g. I also have to write
HStack{ ... }.padding(.leading, CGFloat(20))
If I do not cast 20 to a CGFloat I'm getting the error "'CGFloat' is not convertible to 'CGFloat?'".
I know that SwiftUI's error messages aren't that good yet since almost 90% of code failures leads to some ambiguous bla errors. But here I really don't know where these errors are coming from.
import SwiftUI
struct IngredientsTab: View {
#ObservedObject var ingredientsVM: IngredientViewModel = IngredientViewModel()
var body: some View {
NavigationView {
List {
ForEach(ingredientsVM.ingredients, id: \.self){ (ingredient) in
NavigationLink(destination: IngredientDetailView(ingredient: ingredient)){
VStack {
HStack {
Text(ingredient.name)
Spacer()
if (Type.NONALC == ingredient.type) {
self.modifier(LabelViewModifier(label: "Non-alcoholic", backgroundColor: .green))
} else {
self.modifier(LabelViewModifier(label: "Alcoholic", backgroundColor: .red))
}
}
if (ingredient.pieceGood) {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color.green)
Text("Piece good") // Here I'm getting the error
Spacer()
}
.padding(.leading, 20)
}
}
}
}
}
}
}
}
So it seems to be somehow related to SwiftUI (since it is obviously not working perfectly yet) but also to Cocoapods/ Firebase. In another project I have a similar approach but without using any Pods and it is just working fine.
I've finally found the issue.
My Ingredient inherited from Identifiable but also from NSObject. In combination with some faulty constructor and writing
id: \.self
in the ForEach loop led to this strange behavior. I updated my Ingredient to a correct model and removed the above line from the ForEach loop and then it worked.
Thank you for your help!
PS: It is neither related to any Pods nor to Firebase ;)

SwiftUI: NavigationDestinationLink deprecated

After installing Xcode 11 beta 5 this morning, I noticed that NavigationDestinationLink was deprecated in favor of NavigationLink.
Also, that's what Apple says about it in the release notes:
NavigationDestinationLink and DynamicNavigationDestinationLink are deprecated; their functionality is now included in NavigationLink. (50630794)
The way I use NavigationDestinationLink is to programmatically push a new view into the stack via self.link.presented?.value = true. That functionality doesn't seem to be present in NavigationLink.
Any idea anyone?
I would rather not use NavigationDestinationLink anymore as it's deprecated...
Thank you!
UPDATE:
Actually, the NavigationDestinationLink way does not work anymore so I guess we have no way of pushing programmatically anymore?
UPDATE 2:
NavigationLink(destination: CustomView(), isActive: $isActive) {
return Text("")
}
This works but when you pass isActive to true, any state update will trigger this code and push over and over... Also, if you pass it back to false, it will pop the view.
Not only updates, if you set isActive to true, it will push the view (good) and if we press the back button, it will go back then immediately push again since it's still true.
Playing with onAppear was my hope but it's not called when going back to it...
I'm not sure how we're supposed to use this.
After spending some time with NavigationLink(destination:isActive), I am liking it a lot more than the old NavigationDestinationLink. The old view was a little confusing, while the new approach seems much more elegant. And once I figure out how to push without animations, it would make state restoration at application launch very easy.
There is one problem though, a big and ugly bug. :-(
Pushing a view programatically works fine, and popping it programatically does too. The problem starts when we use the BACK button in the pushed view which behaves oddly every other time. The first time the view is popped, the view pops and pushes again immediately. The second time around it works fine. Then the third time it starts all over again.
I have created a bug report (number here). I recommend you do the same and reference my number too, to help Apple group them together and get more attention to the problem.
I designed a workaround, that basically consists of replacing the default Back button, with our own:
class Model: ObservableObject {
#Published var pushed = false
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
Button("Push") {
// view pushed programmatically
self.model.pushed = true
}
NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}
struct DetailView: View {
#EnvironmentObject var model: Model
var body: some View {
Button("Bring me Back (programatically)") {
// view popped programmatically
self.model.pushed = false
}
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.pushed = false
})
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
To improve the workaround without replacing the back button with a custom one, you can use the code above :
NavigationLink(destination: Test().onAppear(perform: {
self.editPushed = nil
}), tag: 1, selection: self.$editPushed) {
Button(action: {
self.editPushed = 1
}) {
Image(systemName: "plus.app.fill")
.font(.title)
}
}
The onAppear block will erase the selection value preventing to display the detail view twice
You can also use NavigationLink(destination:tag:selection)
NavigationLink(destination: MyModal(), tag: 1, selection: $tag) {
EmptyView()
}
So programmatically you can set tag to 1 in order to push MyModal. This approach has the same behaviour as the one with the Bool binding variable, so when you pop the first time it pushes the view immediately, hopefully they'll fix it in next beta.
The only downside I see with this approach, compared to DynamicNavigationDestinationLink is that you need to provide a View to NavigationLink, even if you don't need one. Hopefully they'll find a cleaner way to allow us to push programmatically.
The solution is to create custom back button for your detail view and pop detail view manually.
.navigationBarItems(leading:
Button(action: {
self.showDetail = false
}) {
Image(systemName: "chevron.left").foregroundColor(.red)
.font(.system(size: 24, weight: .semibold))
Text("Back").foregroundColor(.red)
.font(.system(size: 19))
}
)
The method used in the selected answer has been deprecated again. Here's the solution copied from this answer in this post.
#State private var readyToNavigate : Bool = false
var body: some View {
NavigationStack {
VStack {
Button {
//Code here before changing the bool value
readyToNavigate = true
} label: {
Text("Navigate Button")
}
}
.navigationTitle("Navigation")
.navigationDestination(isPresented: $readyToNavigate) {
MyTargetView()
}
}
}

Resources