Way to stop action repeating SwiftUI - swiftui

I am using the following code in a quiz, where once the user answers the question an alert pops up. They then have the option to go to the next question OR review the question again and then after a time period the next question appears.
The issue I am having is that this action carries on to the next question and skips it after the time period, not allowing the user time to answer it! Whats the best way to stop the action repeating automatically in the following questions?
func nextTapped() {
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
nextQuestion()
}
}
func continueTapped() {
nextQuestion()
}
.alert(isPresented: $showingFeedback) {
Alert(
title: Text(scoreTitle),
message: Text(selectedQuestion!.feedback),
primaryButton: .default(Text("Review"), action: nextTapped),
secondaryButton: .default(Text("Continue"), action: continueTapped)
)
}

I found that the function passed on to the next question. So I answer the first question, press review and after 10 sec it would automatically go to the next question. I found that then this would happen to the next quetsion too, before I pressed anything. I found that adding ' -> Void ' to the code stopped it.
func nextTapped() -> Void {
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
nextQuestion()
}
}
func continueTapped() {
nextQuestion()
}

Related

How to jump back "n" seconds from current position in video using the YouTubePlayerKit Package in SwiftUI

I am very new to programming in SwiftUI (Currently running Xcode 14.2). I am using the YouTubePlayerKit 1.3.0 Package (https://swiftpackageindex.com/SvenTiigi/YouTubePlayerKit) to load a YouTube video and play it. One of the abilities I want to build into the app is to jump back "n" seconds in the video when a button is pushed and start playing the video from that point. There is a function called: getCurrentTime() that I believe returns an integer representing the elapsed time in seconds from the beginning of the video. I want to subtract "n" seconds from that value and use the: .seek(to: , allowSeekAhead: true) function to jump to the desired location of the video based on the above calculation. I have the code to a point where I can load the video and when I click the "SEEK" button it will jump to a static value I have hard coded into the script and play from that point. I am struggling with how to retrieve the current time, subtract "n" seconds and use that value in the .seek() function.
This is a link to information on the YouTubePlayerKit Package: https://github.com/SvenTiigi/YouTubePlayerKit
Any help would be greatly appreciated.
Below is the SwiftUI script I have so far. This is the description of the function I believe
will give me the time in seconds up to the current point in the video:
/*
/// Retrieve the elapsed time in seconds since the video started playing
/// - Parameter completion: The completion closure
func getCurrentTime(
completion: #escaping (Result<Double, YouTubePlayerAPIError>) -> Void
)
*/
import SwiftUI
import YouTubePlayerKit
struct ContentView: View {
let youTubePlayer = YouTubePlayer(
source: .url("https://www.youtube.com/watch?v=qL-ry_tz6V0"),
configuration: .init(
autoPlay: true
)
)
#State private var JumpTo: Double = 27
var body: some View {
ZStack {
YouTubePlayerView(self.youTubePlayer) { state in
// Overlay ViewBuilder closure to place an overlay View
// for the current `YouTubePlayer.State`
switch state {
case .idle:
ProgressView()
case .ready:
EmptyView()
case .error(let error):
Text(verbatim: "YouTube player couldn't be loaded")
}
}
HStack {
Button("PLAY") {
//Play video
youTubePlayer.play()
}
Button("PAUSE") {
// Pause video
youTubePlayer.pause()
}
Button("STOP") {
// Stop video
youTubePlayer.stop()
}
Button("SEEK") {
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
}
}
.offset(y: 250)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
According to your comments the culprit seems to be:
Button("SEEK") {
JumpTo = youTubePlayer.getCurrentTime() // this line gives the error asnyc call and try without catch
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
}
You are in an async context here. Swift async / concurrency is a very broad field that cannot be answered in a single answer. I would recommend you put some effort into reading this stuff up.
The reason for the 2 errors are:
youTubePlayer.getCurrentTime() is a throwing function. You need to catch any possible errors and either do something with them or dismiss them. Either way the Swift compiler demands from you a certain syntax. It is called do/catch clause.
do{
try .... //throwing function
} catch{
// handle error here or just print it
print(error)
}
Your function is asyncronous. It means the code you write will proceed without waiting for the result of the function. To handle this you need to await it. But as your Button("SEEK") { method does not support something like that you have to wrap it into a Task:
Task{
await ... // await async function
}
If you combine both aproaches you get....
Button("SEEK") {
Task{
do{
JumpTo = try await youTubePlayer.getCurrentTime()
// now you can manipulate it and do
Jumpto = .....
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
} catch{
// handle error here or just print it
print(error)
}
}
}

SwiftUI Runtime Error from if condition { Text() } in watchOS

As far as I read about conditional Views this code should work. But it doesn't.
struct Consts {
static let myCondition = false //no difference if true or false
}
struct ContentView: View {
#State var toggle: Bool = false
var body: some View {
List() {
Text("Short Text is in first line")
Text("Second Line Text is a little longer but not much")
if Consts.myCondition {
Text("This is a conditional text. As in: when the user hasn't purchased the option he / she don't need a hint how to use this feature.")
// } else {
// Text("else doesn't help either.")
}
Toggle("I also have a toggle but it has nothing to do with this.", isOn: $toggle)
Text("Here we have a longer Text. Dont know what to type any more.")
Text("More text which is longer than a few lines.")
Text("Not so long Text")
}
.navigationTitle("Hints & Settings")
}
}
It compiles without warnings or errors. It loads up and displays fine, on simulator and on device. But every time I scroll the List upwards from the end, as soon as this if condition { Text() } should become visible the app crashes with
Fatal error: file SwiftUI, line 0
2021-03-07 06:36:26.548126+0100 WTFif WatchKit Extension[23163:641812] Fatal error: file SwiftUI, line 0
This is not limited to watchOS. It reproduces the same way in iOS, just the Texts have to be longer so that the if condition { Text() } becomes invisible when scrolling.
I have worked around the error with an array, conditional ranges and two ForEach() blocks.
struct ContentView: View {
let myHints = ["Short Text is in first line",
"Second Line Text is a little longer but not much",
"This is a conditional text. As in: when the user hasn't purchased the option he / she don't need to hint how to use this feature.",
"Here we have a longer Text. Dont know what to type any more.",
"More text which is longer than a few lines.",
"Not so long Text"]
var myUpperRange: ClosedRange<Int> {
if Consts.myCondition {
return 0...1
} else {
return 0...2
}
}
var myLowerRange: ClosedRange<Int> {
return 3...5
}
#State var toggle: Bool = false
var body: some View {
List() {
ForEach (myUpperRange, id: \.self) { i in
Text(myHints[i])
}
Toggle("I also have a toggle but it has nothing to do with this.", isOn: $toggle)
ForEach (myLowerRange, id: \.self) { i in
Text(myHints[i])
}
}
.navigationTitle("Hints & Settings")
}
}
My question basically is: am I not getting it or did I find a bug in Xcode / SwiftUI? Should my code above work? What could I have done different to make it work with the simple list of Texts?
Background: I also have a TabView with an if condition { MyTab() } which works without crashing. Do I have to worry that this might crash in the same way? Should I work around this as well before shipping?
PS: I am using Xcode 12.4 (12D4e)
Apparently this has been fixed by iOS 15 Beta 4: on my test device I had the error prior to the Beta 4 update, using a test app compiled with Xcode 13 Beta 3. After the update to iOS 15 Beta 4 the error is gone.
So I think it’s reasonable to say that the update to iOS 15 Beta 4 did fix the error.

SwiftUI - Fatal error: each layout item may only occur once: file SwiftUI, line 0

OK before anymore marks this as duplicate, I have looked at posts similar to this on StackOverFlow and even the Apple forums, and implemented all recommended solutions, yet the problem persists. I've using a LazyVGrid. My code is as follows:
This is my custom scroll view that allows for pagination once the user reaches the end of the scroll:
if !quickSearchViewModel.isLoading && !quickSearchViewModel.search_result.isEmpty {
DataFetchingScrollView(.vertical, alignment: .center, onOffsetChange: { (off, height) in
offset = off
heightMinusOffset = height
if heightMinusOffset <= UIScreen.main.bounds.height &&
!quickSearchViewModel.search_result.isEmpty {
quickSearchViewModel.paginate_searchterm {
print("Paginated")
} onError: { (error) in
print(error)
}
}
}) {
createGrid()
.id(UUID())
}
And these are my two functions to create the grid and the view inside the grid:
private func createGrid() -> some View {
LazyVGrid(columns: columns) {
ForEach(quickSearchViewModel.search_result, id: \.product_uid) { product in
createProductItemView(product)
.id(product.product_uid)
}
}
}
private func createProductItemView(_ product: ProductModel) -> some View {
ProductItemView(product: product)
.id(product.product_uid)
}
Yes, I know I have spammed the id, but I've added '.id' to all views indivdually and the problem persists. It's as soon as I hit search, the content loads and appears in the grid and thats when my application crashes.
Edit - the product.product_uid is an auto-id generated by Firebase. I am literally using this same method in other views, and some work without issues, others may have a little bug.
Found this issue, it was because I was using orderBy when querying data.

SwiftUI - Why can't I use an Alert within an if statement

I'm currently working through the #100daysofswiftUI and have a quick question regarding the use of alerts within if statements. This particular task is at the end of the second project.
Here is the code:
.alert(isPresented: $showingScore) {
if x == 1 {
Alert(title: Text(scoreTitle), message: Text("The correct answer was \(countries[correctAnswer])"), dismissButton: .default(Text("Restart")) {
self.askQuestion()
})
}
}
I feel like this code should work however I have a yellow alert over the Alert line saying:
Result of 'Alert' initializer is unused
I don't know what this means, how can I fix this?
You need to return an Alert in the .alert modifier, not an if statement.
For this you can create a computed property which returns an Alert:
var alert: Alert {
if x == 1 {
return Alert(
title: Text(scoreTitle),
message: Text("The correct answer was \(countries[correctAnswer])"),
dismissButton: .default(Text("Restart")) {
self.askQuestion()
}
)
} else {
return Alert(...) // return some other `Alert`
}
}
and use it in the .alert modifier:
.alert(isPresented: $showingScore) {
alert
}
Just make sure you only enable showingScore when you want to show an alert.
you should put your if condition with showingScore,
do something like this
if x == 1 {
showingScore = true
}
your alert will look like this
.alert(isPresented: $showingScore) {
Alert(title: Text(scoreTitle), message: Text("The correct answer was \(countries[correctAnswer])"), dismissButton: .default(Text("Restart")) {
self.askQuestion()
})
}

How to stop a method while it's running in swiftui

I have a problem and I have to find a solution. I must immediately stop the "Starting" method if the user presses the Cancel button.
I have not fixed the problem yet because I can not block the method while it is running but only at the end or at the beginning of the execution.
ContentView:
Button(action: {
self.Starting()
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 0), execute: self.workItem)
}) {
Text("START")
}
Button(action: {
self.workItem.cancel()
}) {
Text("CANCEL")
}
Starting method:
func Starting() {
self.workItem = DispatchWorkItem {
DispatchQueue.main.async {
self.impostazioniChek=true
}
DispatchQueue.global().async {
if (self.pm.MarksSwitch)
{
sleep(UInt32(self.MarksT))
self.Marks()
}
if (self.pm.ReadySwitch)
{
sleep(UInt32(self.ReadyT))
self.Ready()
}
self.Start()
sleep(3)
DispatchQueue.main.async {
self.tm.start()
}
}
}
}
You cannot cancel because
1) almost all the time work items runs on main (UI) queue, so block it
2) inside work item there is no check for isCancelled. DispatchWorkItem.cancel() only marks it cancelled, but does not stops execution you have to do this.
So to make this work it needs to redirect everything not-UI related in your work item into .background queue explicitly.
Eg. instead of
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 0), execute: self.workItem)
use
DispatchQueue.global(qos: .background).async { self.workItem }
Instead of
DispatchQueue.global().async {
if (self.pm.MarksSwitch)
Just use (because it is already in background)
if (self.pm.MarksSwitch)
and I'm not sure what is (probably it also should be run on background queue)
self.tm.start()
And on second add inside block interruption on cancellation by regular check, like below
...
}
if self.workItem.isCancelled { // add as many such as needed
return
}
if (self.pm.ReadySwitch)
...