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)
...
Related
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)
}
}
}
I've been following a few tutorials online regarding setting up Storekit in my app. I've gotten as far as successfully requesting all the products and holding them in a products array. The next step of these tutorials is almost always listing out the products using a ForEach like so:
ForEach(products) { product in
Button {
Task.init {
try await purchaseProduct(product)
}
} label: {
HStack {
Text(product.displayName)
Spacer()
Text(product.displayPrice)
}
}
}
This doesn't work for my use case, unfortunately. The design I'm working off has 3 buttons in different parts of the screen, each of which initiate a purchase request for a different product.
I've managed to get some of the way there by doing this:
Button {
Task.init {
try await purchaseProduct(products.first!)
}
} label: {
HStack {
Text("\(products.first?.displayName ?? "No name")")
Spacer()
Text("\(products.first?.displayPrice ?? "No price")")
}
}
But this feels really hacky to me, for the following reasons:
Force unwrapping doesn't feel correct
I can make this work for the .first and .last item in the products array but I don't know how to get the second item, and this also means if the order of items inside products changes, my UI ties the wrong product to their respective button.
Here's my purchaseProduct function:
func purchaseProduct(_ product: Product) async throws -> StoreKit.Transaction {
let result = try await product.purchase()
switch result {
case .pending:
throw PurchaseError.pending
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
return transaction
case .unverified:
throw PurchaseError.failed
}
case .userCancelled:
throw PurchaseError.cancelled
#unknown default:
assertionFailure("Unexpected result")
throw PurchaseError.failed
}
}
Ideally, I'm looking to do something like this:
if let productOne = products.PRODUCTID {
Button {
Task.init {
try await purchaseProduct(productOne)
}
} label: {
HStack {
Text("\(productOne.displayName)")
Spacer()
Text("\(productOne.displayPrice)")
}
}
}
But I'm struggling to wrap my head around how to get there.
In order to achieve your desired if let productOne = products.PRODUCTID, you can use first(where:): https://developer.apple.com/documentation/swift/array/first(where:)
if let productOne = products.first(where: {$0.id == "iap.myapp.ProductOne"}) {
// ...
}
I have a very troubling problem. I have searched for days on how to solve it. I have some code that I want to run every time the app is opened, not just when the app is launched for the first time. I've basically tried everything available. I've tried scenePhase, I've tried AppDelegate, I've tried onAppear, I've tried init and custom extensions to the View class, I've even tried simply running a function in the view, but nothing is working. I'll show my code here.
#main
struct CouponDeckApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
AppContentView()
}
}
}
struct AppContentView: View {
init() {
let userDefaults = UserDefaults.standard
if userDefaults.value(forKey: "hour") == nil { // 1
userDefaults.set(9, forKey: "hour") // 2
}
// 3
if userDefaults.value(forKey: "minute") == nil {
userDefaults.set(30, forKey: "minute")
}
}
#State var currentview: String = "Main"
var body: some View {
Group {
switch currentview {
case "Main":
MainView(currentview: $currentview)
case "Form":
FormView(currentview: $currentview)
case "Settings":
SettingsView(currentview: $currentview)
default:
if currentview.contains("Coupon") {
CouponView(currentview: $currentview)
}
else {
EditView(currentview: $currentview)
}
}
}
}
}
//MainView(), CouponView(), FormView(), etc.
I'm starting to suspect that the problem is with the switch statement in AppContentView that allows you to move between the different views.
Does anyone know:
A. Why this is happening,
B. How to fix it, or
C. Another alternative?
Thanks in advance!
P.S. I'm running my code on the simulator.
Here is a very simple way, using native scenePhase, I did not make it more complicated. You can use Preference method as well for better result! But onChange is good enough for this example:
struct ContentView: View {
#Environment(\.scenePhase) var scenePhase
var body: some View {
Text("Welcome to my App!")
.onAppear() { customFunction() }
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
customFunction()
}
}
}
}
func customFunction() {
print("App is opened!")
}
The simple problem was that it doesn't work when you close out of the app. I realized if you just exit the app but don't completely close out of it, it works just fine.
I also learned about the NotificationCenter's applications to this. By triggering a response when UIApplication sends out the willEnterForegroundNotification by using the onReceive method, you can trigger a response that way.
Do it in your AppDelegate's application(_:didFinishLaunchingWithOptions:).
I'm building an app with SwiftUI with a stop watch functionality.
The TimedSession class has a var timer: Timer computed property like this:
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
guard let time = self.currentTime else {
print("no current time")
return
}
if self.status != .running {
timer.invalidate()
return
}
time.duration += 1
self.objectWillChange.send()
}
this works well until I start to interact with other parts of the UI, like a scrollview. It's not just that the timer UI is locked, but the block: callbck on scheduledTimer isn't being executed.
What's the best way to put the callback block into the background? I've tried with GCD but no good so far.
Scheduled Timer works by default in .default run loop mode, but during user interaction, like scrolling, a run loop works in different tracking mode.
The solution is set up Timer for all standard modes, named .common, like below
// just create non attached timer
let timer = Timer(timeInterval: 0.01, repeats: true) { timer in
guard let time = self.currentTime else {
print("no current time")
return
}
if self.status != .running {
timer.invalidate()
return
}
time.duration += 1
self.objectWillChange.send()
}
...
init() {
// Start timer in run-loop on common modes
RunLoop.main.add(timer, forMode: .common)
}
Assume I have a button which can be used to start and stop (toggle) an action.
let toggleStream: Observable<Bool> = toggleBtn.rx.tap.scan(false) { state, _ in !state }
I have another stream, that emits Integers continuously.
let emitter = Observable<Int>.interval(2.0, scheduler: timerScheduler)
Now I want to use the toggle stream to start and stop the emitting of the second stream. This is my approach:
Observable.combineLatest(toggleStream, emitter) { shouldEmit, evt in
return (shouldEmit, evt)
}.takeWhile{ (shouldEmit, evt:Int) in
return shouldEmit == true
}.map {(_, evt) in
return evt
}
This works great for the first time. I can press the button and the Observable starts emitting its Ints. Also stopping works. Sadly I can't start it for a second time, because the stream is completed. How can I restart/retry/repeat it when the user toggles the button again?
Here's how I did it in the playground. You should be able to extrapolate:
//: Playground - noun: a place where people can play
import RxSwift
let toggleButton = PublishSubject<Void>()
let toggleStream: Observable<Bool> = toggleButton
.scan(false) { state, _ in !state }
.debug()
.shareReplayLatestWhileConnected()
let emit = toggleStream
.filter { $0 }
.flatMapLatest { _ in
Observable<Int>.interval(2.0, scheduler: MainScheduler.instance)
.takeUntil(toggleStream.filter { !$0 })
}
_ = emit.subscribe( {
print($0)
})
toggleButton.onNext()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0) {
toggleButton.onNext()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 11.0) {
toggleButton.onNext()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 17.0) {
toggleButton.onNext()
}
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true