In my top level view, I have declared a timer like so:
Struct ContentView: View {
#State var timer = Timer.publish(every: 1, on: .main, in:
.common).autoconnect()
var body: some View {
ZStack {
if self.timerMode == .warmup {
WarmupView(
timer: $timer
)
if self.timerMode == .work {
WorkView(
timer: $timer
)
}
}
}
In a child view, I want to be able to access and update this timer, which will serve as the single source of truth.
Struct WarmupView: View {
#Binding var timer: Publishers.Autoconnect<Timer.TimerPublisher>
#Binding var timeRemaining: Int
var body: some View {
VStack {
Text("\(self.timeRemaining)").font(.system(size: 160))
.onReceive(self.timer) { _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
}
}
}
}
The timer is publishing to the warmup view without issues, but when timerMode is updated to .work (which has nearly identical code) and the view changes, the timer stops publishing.
Simple as changing the type of your #Binding var timer in your WarmupView to Publishers.Autoconnect<Timer.TimerPublisher>. The .autoconnect() wraps the timer publisher in another publisher, which changes the type.
Here's a simplified version of your code:
struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var remaining = 100
var body: some View {
Text("\(remaining)")
.font(.system(size: 160))
.onReceive(timer) { _ in
if self.remaining > 0 {
self.remaining -= 1
}
}
}
}
Related
I would like to pass a timer from ContentView to SecondView, but I don't know how to manage it because I never used it before.
Can someone figure this out for me?
ContentView
struct ContentView: View {
#State private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var timeRemaining = 10
var body: some View {
NavigationView {
VStack {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
NavigationLink {
SecondView(timer: ???) // <-- What should i pass here?
} label: {
Text("Change View")
}
}
}
}
}
SecondView
struct SecondView: View {
#Binding var timer: ??? // <-- What type?
#State private var timeRemaining = 5
var body: some View {
Text("Hello")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView(timer: ???) // <-- Same thing here in SecondView preview
}
}
With this timer declaration you are in the Combine world. Combine is the reactive framework from Apple.
First you would need to import it:
import Combine
I have commented the code but Combine is a far field and it probably would be best to read the documentation about it, read some tutorials and try some things out.
documentation
struct ContentView: View {
// The typ here is Publishers.Autoconnect<Timer.TimerPublisher>
// But we can erase it and the result will be a Publisher that emits a date and never throws an error: AnyPublisher<Date,Never>
#State private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
#State private var timeRemaining = 10
var body: some View {
NavigationView {
VStack {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
NavigationLink {
// pass the publisher on
SecondView(timer: timer)
} label: {
Text("Change View")
}
}
}
}
}
struct SecondView: View {
//You donĀ“t need binding here as this view never manipulates this publisher
var timer: AnyPublisher<Date,Never>
#State private var timeRemaining = 5
var body: some View {
Text("Hello")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
print(timeRemaining)
}
}
}
}
struct SecondView_Previews: PreviewProvider {
// Creating a static private var should work here !not tested!
#State static private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
static var previews: some View {
SecondView(timer: timer)
}
}
You could simply inject the timer publisher, as suggested above, but there may be an even simpler solution:
FirstView is already updating with every tick of the timer. You could simply pass a timeRemaning binding to your second view and then it too would just update with every tick of the timer (because timeRemaining changes on each tick). You can then observe and react to changes of timeRemaining using .onChange(of:):
struct SecondView: View {
#Binding var timeRemaining: TimeInterval
var body: some View {
Text("Hello")
.onChange(of: timeRemaining) {
if $0 < 0 {
timeRemaining = -1
}
}
}
}
You don't need to pass a binding, Since you are not mutating timer of contentview from the second view. You can just pass the reference to the timer publisher and then subscribe to it using .onReceive().
import Combine // here
struct ContentView: View {
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect().eraseToAnyPublisher() //<= Here
#State private var timeRemaining = 10
var body: some View {
NavigationView {
VStack {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
NavigationLink {
SecondView(timer: timer)
} label: {
Text("Change View")
}
}
}
}
}
struct SecondView: View {
let timer: AnyPublisher<Date, Never> // Here
#State private var timeRemaining = 5
var body: some View {
VStack {
Text("Hello")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
Text("time remaining \(timeRemaining)")
}
}
}
I want the view to be updated every time #State list gets updated.
But when I use Button to trigger a consecutive update, the list gets updated only after all the consecutive updates are finished.
This code updates the list 10 seconds after the button is pressed.
What should I do to make this code update every second?
#State var i_list: [Int] = []
var body: some View {
VStack {
Button(action: {
for i in 0..<10 {
i_list.append(i)
do {
sleep(1)
}
}
}) {
Text(button_text)
}
List (i_list, id: \.self){ i in
Text(String(i))
}
}
}
Solved:
I solved this problem by updating the list in a different thread.
let globalQueue = DispatchQueue.global()
globalQueue.async {
for i in 0..<10 {
DispatchQueue.main.async {
i_list.append(i)
}
do {
sleep(1)
}
}
}
If you want to see your view updates immediately, instead of using a for loop to update your array, use a publisher. Here I use a timer and add a onReceive(timer) on the List
struct ContentView : View {
#State var i_list: [Int] = []
#State var currentNumber = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
List (i_list, id: \.self) { i in
Text(String(i))
}.onReceive(timer) { _ in
guard currentNumber < 10 else {
return
}
i_list.append(currentNumber)
currentNumber += 1
}
}
}
}
Edit: changed currentNumber to #State
I am trying to achieve a navigation to another view when timer hits a specific time. For example I want to navigate to another view after 5 minutes. In swift i can easily achieve this but I am new to SwiftUI and little help will be highly appreciated.
My code:
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()
var body: some View {
WebView(url: "https://twitter.com/")
.navigationBarTitle("")
.navigationBarHidden(true)
.onReceive(timer) { _ in
if self.minute == self.timerTime {
print("Timer completed navigate to Break view")
NavigationLink(destination: BreakView()) {
Text("Test")
}
self.timer.upstream.connect().cancel()
} else {
self.minute += 1.0
}
}
}
}
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
}
}
}
}
I want to start a timer of 60 seconds to test how many words a user can type within that minute. I started counting the characters within the TextField. But Now I need to decrement a timer so I can do the math and print out the answer to the user. I can't seem to figure out how to use the timer when it's not in the Content View struct though. Can I do that?
import SwiftUI
struct ContentView: View {
#State var userInput = ""
#State var modalview = false
#State var getstarted = false
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
ZStack() {
modalView(modalview: $modalview, userInput: userInput)
}.sheet(isPresented: $modalview) {
modalView(modalview: self.$modalview)
}
}
}
struct modalView : View {
#ObservedObject var durationTimer = TimerHolder()
#Binding var modalview : Bool
#State var userInput: String = ""
var body: some View {
VStack{
Button(action: {
self.modalview = true
}) {
TextField("Get Started", text:$userInput)
.background(Color.gray)
.foregroundColor(.white)
.frame(width: 300, height: 250)
.cornerRadius(20)
Text("\(userInput.count)")
Text("\(durationTimer.count) Seconds")
}
}
}
}
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
self.count += 1
print(self.count)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
The simples one, as you hold it as property, is to start in .onAppear... (supposing, of course, that you pass it in ContentView().environmentObject(TimerHolder()) on ContentView creation)
struct ContentView: View {
#State var userInput = ""
#State var modalview = false
#State var getstarted = false
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
ZStack() {
modalView(modalview: $modalview, userInput: userInput)
}.sheet(isPresented: $modalview) {
modalView(modalview: self.$modalview)
}
.onAppear {
self.timerHolder.start()
}
}
}
I'd like to implement a simple view transition through SwiftUI and Timer.
I have a primary View, it's content View. If I call func FireTimer() from in the View, the function fires timer. Then after 5 seconds, I would have a View transition.
I tried NavigationLink, but it has a button. Timer can't push the button so now I'm confused.
I'll show my code below.
TimerFire.swift
import Foundation
import UIKit
import SwiftUI
let TIME_MOVENEXT = 5
var timerCount : Int = 0
class TimerFire : ObservableObject{
var workingTimer = Timer()
#objc func FireTimer() {
print("FireTimer")
workingTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(TimerFire.timerUpdate),
userInfo: nil,
repeats: true)
}
#objc func timerUpdate(timeCount: Int) {
timerCount += 1
let timerText = "timerCount:\(timerCount)"
print(timerText)
if timerCount == TIME_MOVENEXT {
print("timerCount == TIME_MOVENEXT")
workingTimer.invalidate()
print("workingTimer.invalidate()")
timerCount = 0
//
//want to have a transition to SecondView here
//
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Button(action: {
// What to perform
let timerFire = TimerFire()
timerFire.FireTimer()
}) {
// How the button looks like
Text("Fire timer")
}
}
}
SecondView.swift
import Foundation
import SwiftUI
struct SecondView: View {
var body: some View {
Text("Second World")
}
}
How could I simply show this SecondView?
Ok, if you want to do this w/o NavigationView on first screen (for any reason) here is a possible approach based on transition between two views.
Note: Preview has limited support for transitions, so please test on Simulator & real device
Here is a demo how it looks (initial white screen is just Simulator launch)
Here is single testing module:
import SwiftUI
import UIKit
let TIME_MOVENEXT = 5
var timerCount : Int = 0
class TimerFire : ObservableObject{
var workingTimer = Timer()
#Published var completed = false
#objc func FireTimer() {
print("FireTimer")
workingTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(TimerFire.timerUpdate),
userInfo: nil,
repeats: true)
}
#objc func timerUpdate(timeCount: Int) {
timerCount += 1
let timerText = "timerCount:\(timerCount)"
print(timerText)
if timerCount == TIME_MOVENEXT {
print("timerCount == TIME_MOVENEXT")
workingTimer.invalidate()
print("workingTimer.invalidate()")
timerCount = 0
//
//want to have a transition to SecondView here
//
self.completed = true
}
}
}
struct SecondView: View {
var body: some View {
Text("Second World")
}
}
struct TestTransitionByTimer: View {
#ObservedObject var timer = TimerFire()
#State var showDefault = true
var body: some View {
ZStack {
Rectangle().fill(Color.clear) // << to make ZStack full-screen
if showDefault {
Rectangle().fill(Color.blue) // << just for demo
.overlay(Text("Hello, World!"))
.transition(.move(edge: .leading))
}
if !showDefault {
Rectangle().fill(Color.red) // << just for demo
.overlay(SecondView())
.transition(.move(edge: .trailing))
}
}
.onAppear {
self.timer.FireTimer()
}
.onReceive(timer.$completed, perform: { completed in
withAnimation {
self.showDefault = !completed
}
})
}
}
struct TestTransitionByTimer_Previews: PreviewProvider {
static var previews: some View {
TestTransitionByTimer()
}
}
There is no code snippet for ContentView, so I tried to build simple example by myself. You can use NavigationLink(destination: _, isActive: Binding<Bool>, label: () -> _) in your case. Change some State var while receiving changes from Timer.publish and you'll go to SecondView immediately:
struct TransitionWithTimer: View {
#State var goToSecondWorld = false
let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondWorld(), isActive: self.$goToSecondWorld) {
Text("First World")
.onReceive(timer) { _ in
self.goToSecondWorld = true
}
}
}
}
}
}
// you can use ZStack and opacity/offset of view's:
struct TransitionWithTimerAndOffset: View {
#State var goToSecondWorld = false
let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Text("First world") // here can be your first View
.opacity(self.goToSecondWorld ? 0 : 1)
.offset(x: goToSecondWorld ? 1000 : 0)
Text("Second world") // and here second world View
.opacity(self.goToSecondWorld ? 1 : 0)
.offset(x: goToSecondWorld ? 0 : -1000)
}
.onReceive(timer) { _ in
withAnimation(.spring()) {
self.goToSecondWorld = true
}
}
}
}
struct SecondWorld: View {
var body: some View {
Text("Second World")
}
}