How could I simply have a View transition to SwiftUI View? - ios

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")
}
}

Related

How can I pass Binding<Timer> in SwiftUI?

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)")
}
}
}

SwiftUI: stuck on infinite page view implementation

I'm trying to create a PageView in pure SwiftUI. There's my test code below. And everything works as expected but the DragGesture. It just doesn't call 'onEnded' function. Never. How can I fix it?
struct PageView<V: View>: Identifiable {
let id = UUID()
var content: V
}
struct InfinitePageView: View {
#State private var pages: [PageView] = [
PageView(content: Text("Page")),
PageView(content: Text("Page")),
PageView(content: Text("Page"))
]
#State private var selectedIndex: Int = 1
#State private var isDragging: Bool = false
private var drag: some Gesture {
DragGesture()
.onChanged { _ in
self.isDragging = true
}
.onEnded { _ in
self.isDragging = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
resolvePages()
}
}
}
var body: some View {
NavigationView {
TabView(selection: $selectedIndex) {
ForEach(pages) { page in
page.content
.tag(pages.firstIndex(where: { $0.id == page.id })!)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(drag)
.onChange(of: selectedIndex, perform: { value in
guard !isDragging else { return }
DispatchQueue.main.async {
resolvePages()
}
})
}
}
private func resolvePages() {
if selectedIndex > 1 {
addNextPage()
}
if selectedIndex < 1 {
addPreviousPage()
}
}
private func addNextPage() {
pages.append(PageView(content: Text("Page")))
pages.removeFirst()
selectedIndex = 1
}
private func addPreviousPage() {
pages.insert(PageView(content: Text("Page")), at: 0)
pages.removeLast()
selectedIndex = 1
}
}
This is a known issue with SwiftUI
the DragGesture that you setup likely gets overridden by the DragGesture within the TabView.
Can detect onEnded with the setup in this post Detect DragGesture cancelation in SwiftUI but that deactivates the TabView/Page gestures.
Your onChange code for selectedIndex runs once the new page is selected and would act should act the same as onEnded.

SwiftUI ActionSheet does not dismiss when timer is running

I have the following simple SwiftUI setup. A timer that is running and updating a Text. If the timer is not running (stopped or paused) I can easily show an ActionSheet (by tapping on Actions) and dismiss it by choosing either "Cancel" or "Action 1" option. But if the timer is running, I have a really hard time dismissing the ActionSheet by choosing one of the "Cancel" or "Action 1" options. Do you know what's going on?
I am using Xcode 11.5.
import SwiftUI
struct ContentView: View {
#ObservedObject var stopWatch = StopWatch()
#State private var showActionSheet: Bool = false
var body: some View {
VStack {
Text("\(stopWatch.secondsElapsed)")
HStack {
if stopWatch.mode == .stopped {
Button(action: { self.stopWatch.start() }) {
Text("Start")
}
} else if stopWatch.mode == .paused {
Button(action: { self.stopWatch.start() }) {
Text("Resume")
}
} else if stopWatch.mode == .running {
Button(action: { self.stopWatch.pause() }) {
Text("Pause")
}
}
Button(action: { self.stopWatch.stop() }) {
Text("Reset")
}
}
Button(action: { self.showActionSheet = true }) {
Text("Actions")
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Actions"), message: nil, buttons: [.default(Text("Action 1")), .cancel()])
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
class StopWatch: ObservableObject {
#Published var secondsElapsed: TimeInterval = 0.0
#Published var mode: stopWatchMode = .stopped
var timer = Timer()
func start() {
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1
}
}
func stop() {
timer.invalidate()
secondsElapsed = 0
mode = .stopped
}
func pause() {
timer.invalidate()
mode = .paused
}
enum stopWatchMode {
case running
case stopped
case paused
}
}
Works fine with Xcode 12 / iOS 14, but try to separate button with sheet into another subview to avoid recreate it on timer counter refresh.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#ObservedObject var stopWatch = StopWatch()
// #StateObject var stopWatch = StopWatch() // << used for SwiftUI 2.0
#State private var showActionSheet: Bool = false
var body: some View {
VStack {
Text("\(stopWatch.secondsElapsed)")
HStack {
if stopWatch.mode == .stopped {
Button(action: { self.stopWatch.start() }) {
Text("Start")
}
} else if stopWatch.mode == .paused {
Button(action: { self.stopWatch.start() }) {
Text("Resume")
}
} else if stopWatch.mode == .running {
Button(action: { self.stopWatch.pause() }) {
Text("Pause")
}
}
Button(action: { self.stopWatch.stop() }) {
Text("Reset")
}
}
ActionsSubView(showActionSheet: $showActionSheet)
}
}
}
struct ActionsSubView: View {
#Binding var showActionSheet: Bool
var body: some View {
Button(action: { self.showActionSheet = true }) {
Text("Actions")
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Actions"), message: nil, buttons: [.default(Text("Action 1")), .cancel()])
}
}
}

How to navigate another view in onReceive timer closure SwiftUI iOS

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
}
}
}
}

Im trying to make a an app that calculates the WPM

Im trying to make a an app that calculates the WPM. In the end of the game I would like to use a timer to stop the app after 60 seconds. I can't figure out how to stop it. I'm trying to stop it with conditional statement. But I don't know how to implement it with SwiftUI. If anyone had any other ideas that would be great.
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)")
if durationTimer == 60 {
.alert(isPresented: $showAlert) {
Alert(title: Text("Reminder"), message: Text("You wrote"), primaryButton: .default(Text("Yes"), action: { self.presentationMode.wrappedValue.dismiss() })
, secondaryButton: .cancel(Text("No")))
}; else {
}
}
}
}
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()
}
}
}
I changed some codes to give you a clue.
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)").alert(isPresented: self.$durationTimer.count) {
Alert(title: Text("Reminder"),
message: Text("You wrote"),
primaryButton: .default(Text("Yes"), action: { self.presentationMode.wrappedValue.dismiss()
print(123)
})
,
secondaryButton: .cancel(Text("No")))
}
}
}
}
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = false
init(){
self.start()
}
func start() {
self.timer?.invalidate()
self.count = false
self.timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
_ in
self.count = true
print(self.count)
}
}
}

Resources