I am working on a project that at some point it receives a notification. When that happens, I need to show a View. I am not able to catch notification from any View so I am looking for a way to change to control it from outside of View structs. After the View's purpose is done, I need to dismiss it where the app left off. Think like the native behaviour when there is an active call.
I thought I could use sheet however I could not find any way to trigger it for every View that could be active when the notifications come. Or maybe trying to extend native View class would work but again, no luck finding a tutorial.
Any help will be appreciated.
Just update your model based on notification. There is not necessary to define .sheet (modal view) everywhere in your view hierarchy. Doing it in root view should be enough.
To demonstrate that (copy - paste - run) I create small project where I mimic notification with SwiftUI Toggle.
import SwiftUI
class Model: ObservableObject {
#Published var show = false
}
struct SubView: View {
#EnvironmentObject var model: Model
var tag: Int
var body: some View {
VStack {
NavigationLink(destination: SubView(tag: tag + 1).environmentObject(model)) {
Text("subview \(tag)")
}
if tag == 2 {
Toggle(isOn: $model.show) {
Text("toggle")
}.padding()
}
}.navigationBarTitle("subview \(tag)")
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
SubView(tag: 0).environmentObject(model)
}.sheet(isPresented: $model.show) {
Text("sheet")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the result
Related
I’m developing an iOS app using SwiftUI and I’ve hit a road block and wondering if anyone can help me.
I have a main view(ContentView) with 4 subviews within it. The main view has info coming in from a class where all the data is stored and the data is updated from the subviews.
On one of the subviews, I have a button that calls a function updating the data in the class, though the main view is not picking it up. The function is being called and if I reset the app the data has been updated and shows on the main view.
It’s just not picking it up. I have tried having the data as an ObservableObject but still not picking up the changes when the function is run.
Please see the code below and please help, it’s driving me nuts.
This is the main ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var gameState = GameState()
var body: some View {
ZStack {
VStack {
Text(String(gameState.points))
// Other view content
TabView {
UpgradeView() // This is where the subview appears and looks to be fine
.tabItem {
Image(systemName: "rectangle.and.hand.point.up.left.fill")
Text("Upgrades")
}
// Other tab content
.tabItem {
Image(systemName: "cpu")
Text("Bots")
}
// Other tab content
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
}
}
} // ZStack
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is the class (GameState)
import Foundation
class GameState: ObservableObject {
//This number is showing on the main ContentView but when the function is called from the subview.
It doesn't update on ContentView. It does print in console though when ran so it is working.
#Published var points = 5
#Published var pointsPerSecond = 10
init(){
}
func doublePoints() {
var runCount = 0
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timerEnd in
self.points += self.pointsPerSecond
print(self.points)
runCount += 1
if runCount == 30 {
timerEnd.invalidate()
}
})
}
}
This is the Subview
import SwiftUI
struct UpgradesView: View {
#ObservedObject var gameState = GameState()
var body: some View {
HStack {
Button(action: {
gameState.doublePoints() // This is the function called in the from the gameState class, this works
}) {Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 40))
}
}
}
}
struct UpgradesView_Previews: PreviewProvider {
static var previews: some View {
UpgradesView()
}
}
Your subview is observing a different instance of your class. Note how, in both views, you're doing:
#ObservedObject var gameState = GameState()
This is creating a separate instance each time.
What you want is for your subview to use the same instance as your parent view.
One option is to inject the instance from the parent view into the environment of its view:
UpgradeView()
.environmentObject(gameState)
And then, in UpgradeView, change it from an observed object to
#EnvironmentObject var gameState: GameState
This will now get the instance out of the environment, which will be the same instance as the parent.
When clicking on the navigation link in my SwiftUI application, the screen freezes and I can see the memory doubing every second - almost getting to a 1GB of memory before I terminate the application.
I have a simple navigation link as follows:
NavigationLink {
FeedbackView(viewModel: .init())
} label: {
HStack {
Label("Feedback", systemImage: "bubble.left.circle")
.fontWeight(.semibold)
Spacer()
Image(systemName: "chevron.right")
}
}
Upon clicking on this navigtion link, the screen does not go to the next view and instead freezes. I am unable to tap anything else in the iOS simulator. The memory skyrockets and continues to do so until I stop the application.
The view model is it initializing in the FeedbackView call is as follows.
import Foundation
import Dependencies
class FeedbackViewModel: ObservableObject {
}
The view is below.
import SwiftUI
struct FeedbackView: View {
#ObservedObject var viewModel: FeedbackViewModel
var body: some View {
Text("loaded feedback")
}
}
If I remove .init() from the FeedbackView call within the NavigationLink, and instead initialize the FeedbackViewModel in the FeedbackView itself, I also get the same issue. I am rather new to iOS development and am not sure of which xCode tools to use that could help me diagnose this bug.
First: use #StateObject instead:
struct FeedbackView: View {
#StateObject var viewModel: FeedbackViewModel
var body: some View {
Text("loaded feedback")
}
}
Why: unlike #ObservedObject, #StateObject won't get destroyed and re-instantiated every time the view struct redraws. Never create #ObservedObject from within the view itself (Check this article for more details)
Second: how to initialize the #StateObject?
Really depends on your use case, but you could do this:
struct FeedbackView: View {
...
init(_ providedViewModel: ViewModel = ViewModel()) {
_viewModel = StateObject(wrappedValue: providedViewModel)
}
...
The "special notation" _viewModel refers to actual property wrapped inside StateObject property wrapper.
This way parent can either pass the model in (if there's some data it needs to initialize), or let the default model to be created:
NavigationLink {
FeedbackView()
}
This bug is driving me insane. Sometimes (well most of the time) presented sheet gets dismissed first time it is opened. This is happening only on a device and only the first time the app is launched. Here is how it looks on iPhone 11 running iOS 14.1 built using Xcode 12.1 (can be reproduced on iPhone 7 Plus running iOS 14.0.1 as well):
Steps in the video:
I open app
Swiftly navigate to Details view
Open Sheet
Red Sheed gets dismissed by the system???
I open sheet again and it remains on the screen as expected.
This is SwitUI App project (using UIKIt App Delegate) and deployment iOS 14. Code:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailsView()) {
Text("Open Details View")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#State var sheetIsPresented: Bool = false
var body: some View {
VStack {
Button("Open") {
sheetIsPresented.toggle()
}
}.sheet(isPresented: $sheetIsPresented, content: {
SheetView()
})
}
}
struct SheetView: View {
var body: some View {
Color.red
}
}
I was able to fix this problem by removing line .navigationViewStyle(StackNavigationViewStyle()), but I need StackNavigationViewStyle in my project. Any help will be appreciated.
Updated: There is a similar post on Apple forum with sheet view acting randomly weird.
One solution that I found is to move sheet to the root view outside NavigationLink (that would be ContentView in my example), but that is not ideal solution.
I had the same problem in an app. After a great deal of research, I found that making the variable an observed object fixed the problem in SwiftUI 1, and it seems to be in SwiftUI 2. I do remember that it was an intermittent problem on an actual device, but it always happened in the simulator. I wish I could remember why, maybe when the sheet appears it resets the bound variable?, but this code fixes the problem:
import SwiftUI
import Combine
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailsView()) {
Text("Open Details View")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#ObservedObject var sheetIsPresented = SheetIsPresented.shared
var body: some View {
VStack {
Button("Open") {
sheetIsPresented.value.toggle()
}
}.sheet(isPresented: $sheetIsPresented.value, content: {
SheetView()
})
}
}
struct SheetView: View {
var body: some View {
Color.red
}
}
final class SheetIsPresented: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
static let shared = SheetIsPresented()
#Published var value: Bool = false {
willSet {
objectWillChange.send()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Tested on Xcode 12.1, iOS 14.1 in simulator.
Just add your NavigationView view to bellow code line
.navigationViewStyle(StackNavigationViewStyle())
Example :
NavigationView { }.navigationViewStyle(StackNavigationViewStyle())
Problem will be solved (same issues I was faced)
I have a very simple app which contains two views that are tied together with a NavigationLink. In the first view, ContentView, I can see updates to my ObservedObject as expected. However, when I go to the next View, it seems that the code based on the ObservedObject does not recognize changes.
ContentView.swift (The working view):
import SwiftUI
struct ContentView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
NavigationView {
VStack(spacing: 15) {
Toggle(isOn: self.$toggleObject.isToggled) {
Text("Toggle:")
}
if self.toggleObject.isToggled == true {
Text("ON")
}
else {
Text("OFF")
}
NavigationLink(destination: ShowToggleView()) {
Text("Show Toggle Status")
}
}
}
}
}
ShowToggleView.swift (The view that does not behave as I expect it to):
import SwiftUI
struct ShowToggleView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
Form {
Section {
if self.toggleObject.isToggled {
Text("Toggled on")
}
else {
Text("Toggled off")
}
}
}
}
}
All of this data is stored in a simple file, ToggleObject.swift:
import SwiftUI
class ToggleObject: ObservableObject {
#Published var isToggled = false
}
When I toggle it on in the first View I see the text "ON" which is expected, but when I go into the next view it shows "Toggled off" no matter what I do in the first View... Why is that?
Using Xcode 11.5 and Swift 5
You are almost doing everything correct. However, you are creating another instance of ToggleObject() in your second view, which overrides the data. You basically only create one ObservedObject and then pass it to your subview, so they both access the same data.
Change it to this:
struct ShowToggleView: View {
#ObservedObject var toggleObject : ToggleObject
And then pass the object to that view in your navigation link...
NavigationLink(destination: ShowToggleView(toggleObject: self.toggleObject)) {
Text("Show Toggle Status")
}
If I create a new #State variable, when does it get destroyed? Does it live for the lifetime of the parent UIHostingController?
As far as I can find, it is not documented. This is relevant because I don't understand how to clean up after myself if I create an ObservableObject as State somewhere in the view hierarchy.
import SwiftUI
struct Example: View {
#State private var foo = Foo()
var body: some View {
Text("My Great View")
}
}
class Foo: ObservableObject {
deinit {
// When will this happen?
print("Goodbye!")
}
}
Assuming:
struct Example: View {
#State private var foo = Foo()
var body: some View {
Text("My Great View")
}
}
class Foo: ObservableObject {
init() {
print(#function)
}
deinit {
print(#function)
}
}
The issue is that a View type is a struct, and it's body is not a collection of functions that are executed in real-time but actually initialized at the same time when View's body is rendered.
Problem Scenario:
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
NavigationLink(destination: Example()) {
Text("Test")
}
}
}
}
If you notice, Example.init is called before the navigation even occurs, and on pop Example.deinit isn't called at all. The reason for this is that when ContentView is initialized, it has to initialize everything in it's body as well. So Example.init will be called.
When we navigate to Example, it was already initialized so Example.init is not called again. When we pop out of Example, we just go back to ContentView but since Example might be needed again, and since it is not created in real-time, it is not destroyed.
Example.deinit will be called only when ContentView has to be removed entirely.
I wasn't sure on this but found another article talking about a similar issue here:
SwiftUI and How NOT to Initialize Bindable Objects
To prove this, lets ensure the ContentView is being completely removed.
The following example makes use of an action sheet to present and remove it from the view hierarchy.
Working Scenario:
struct ContentView: View {
#State var isPresented = false
var body: some View {
Button(action: { self.isPresented.toggle() }) {
Text("Test")
}
.sheet(isPresented: $isPresented) {
Example()
.onTapGesture {
self.isPresented.toggle()
}
}
}
}
PS: This applies to classes even if not declared as #State, and does not really have anything to do with ObservableObject.
In iOS 14, the proper way to do this is to use #StateObject. There is no safe way to store a reference type in #State.