Timer Onreceive Not Working Inside Navigationview

Timer onReceive not working inside NavigationView

It is better to attach observers to non-conditional views, like

    var body: some View {
NavigationView{
VStack {
if(self.timeRemaining > 0) {
Text("\(timeRemaining)")
} else {
Text("Time is up!")
}
}
.onReceive(timer) { _ in // << to VStack
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
}
}
}

Update: some thoughts added (of course SwiftUI internals are known only for Apple).

.onReceive must be attached to persistently present view in NavigationView, the reason is most probably in conditional ViewBuilder and internal of NavigationView wrapper around UIKit UINavigationControler.

if you remove the condition and have a single Text view with onReceive inside the VStack inside the NavigationView, nothing is ever received

If after condition removed it is attached to Text("\(timeRemaining)") then all works (tested with Xcode 11.4), because there is state dependency in body.

If it is attached to constant Text then there is nothing changed in body, ie. dependent on changed state - timeRemaining, so SwiftUI rendering engine interprets body as static.

onReceive(self.timer) doesn't work inside a NavigationView

Here is a demo of possible solution for your case. Tested with Xcode 11.4 / iOS 13.4 (and it is forward compatible)

The idea is to react not by timer but by view position change that has been read/tracked by view preferences.

Sample Image

struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}

struct DemoView: View {
@State var showHeader: Bool = false

var body: some View {
NavigationView {
ZStack(alignment: .top) {
ScrollView(.vertical) {
Text("User info component")
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll_area")).origin.y) })
VStack {
Text("content")
}.frame(maxWidth: .infinity, minHeight: 1500)
}.coordinateSpace(name: "scroll_area")

if showHeader {
Text("ProfileHeader")
}
}
}
.onPreferenceChange(ViewOffsetKey.self) {
self.showHeader = $0 > 200 // << your condition
}
}
}

SwiftUI : How I can maintain onReceive when the View in closed

Put onReceive on some always-shown view, like

var body: some View {
NavigationView {
ZStack {
Color("BackgroundColor").ignoresSafeArea(.all)
.onReceive(tm.timer) { _ in // << here !!

from handling purpose it does not matter where it is used.

SwiftUI Navigation: why is Timer.publish() in View body breaking nav stack

First, if you remove @StateObject from model declaration in ContentView, it will work.

You should not set the whole model as a State for the root view.

If you do, on each change of any published property, your whole hierarchy will be reconstructed. You will agree that if you type changes in the text field, you don't want the complete UI to rebuild at each letter.

Now, about the behaviour you describe, that's weird.
Given what's said above, it looks like when you type, the whole view is reconstructed, as expected since your model is a @State object, but reconstruction is broken by this unmanaged timer.. I have no real clue to explain it, but I have a rule to avoid it ;)

Rule:

You should not make timers in view builders. Remember swiftUI views are builders and not 'views' as we used to represent before. The concrete view object is returned by the 'body' function.

If you put a break on timer creation, you will notice your timer is called as soon as the root view is displayed. ( from NavigationLink(destination: NavLevel2())

That's probably not what you expect.

If you move your timer creation in the body, it will work, because the timer is then created when the view is created.

    var body: some View {
var timer = Timer.publish(every: 2, on: .main, in: .common)
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}

However, it is usually not the right way neither.

You should create the timer:

  • in the .appear handler, keep the reference,
    and cancel the timer in .disappear handler.

  • in a .task handler that is reserved for asynchronous tasks.

I personally only declare wrapped values ( @State, @Binding, .. ) in view builders structs, or very simple primitives variables ( Bool, Int, .. ) that I use as conditions when building the view.

I keep all functional stuffs in the body or in handlers.

SwiftUI sharing timer through views in navigation view

NavigationView can only push on one detail NavigationLink so to have more levels you need to set .isDetailLink(false) on the link. Alternatively, if you don't expect to run in landscape split view, you could set .navigationViewStyle(.stack) on the navigation.

How to navigate another view in onReceive timer closure SwiftUI iOS

Here is possible approach (of course assuming that TwitterWebView has NavigationView somewhere in parents)

struct TwitterWebView: View {

@State var timerTime : Float
@State var minute: Float = 0.0
@State private var showLinkTarget = false
let timer = Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()

@State private var shouldNavigate = false

var body: some View {

WebView(url: "https://twitter.com/")
.navigationBarTitle("")
.navigationBarHidden(true)

.background(
NavigationLink(destination: BreakView(),
isActive: $shouldNavigate) { EmptyView() }
)

.onReceive(timer) { _ in
if self.minute == self.timerTime {
print("Timer completed navigate to Break view")
self.timer.upstream.connect().cancel()

self.shouldNavigate = true // << here
} else {
self.minute += 1.0
}
}
}
}

Swiftui NavigationView timer delay

This should work:

struct MyView: View {
@State var pushNewView: Bool = false

var body: some View {
NavigationView {
NavigationLink(isActive: $pushNewView) {
Text("Second View")
} label: {
Text("First View") //Replace with some animation content
}
}
.onAppear {
Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
pushNewView = true
}
}
}
}


Related Topics



Leave a reply



Submit