I'm subclassing InputStream from iOS Foundation SDK for my needs. I need to implement functionality that worker thread can sleep until data appear in the stream. The test I'm using to cover the functionality is below:
func testStreamWithRunLoop() {
let inputStream = BLEInputStream() // custom input stream subclass
inputStream.delegate = self
let len = Int.random(in: 0..<100)
let randomData = randData(length: len) // random data generation
let tenSeconds = Double(10)
let oneSecond = TimeInterval(1)
runOnBackgroundQueueAfter(oneSecond) {
inputStream.accept(randomData) // input stream receives the data
}
let dateInFuture = Date(timeIntervalSinceNow: tenSeconds) // time in 10 sec
inputStream.schedule(in: .current, forMode: RunLoop.Mode.default) //
RunLoop.current.run(until: dateInFuture) // wait for data appear in input stream
XCTAssertTrue(dateInFuture.timeIntervalSinceNow > 0, "Timeout. RunLoop didn't exit in 1 sec. ")
}
Here the overriden methods of InputStream
public override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
self.runLoop = aRunLoop // save RunLoop object
var context = CFRunLoopSourceContext() // make context
self.runLoopSource = CFRunLoopSourceCreate(nil, 0, &context) // make source
let cfloopMode: CFRunLoopMode = CFRunLoopMode(mode as CFString)
CFRunLoopAddSource(aRunLoop.getCFRunLoop(), self.runLoopSource!, cfloopMode)
}
public func accept(_ data: Data) {
guard data.count > 0 else { return }
self.data += data
delegate?.stream?(self, handle: .hasBytesAvailable)
if let runLoopSource {
CFRunLoopSourceSignal(runLoopSource)
}
if let runLoop {
CFRunLoopWakeUp(runLoop.getCFRunLoop())
}
}
But calling CFRunLoopSourceSignal(runLoopSource) and CFRunLoopWakeUp(runLoop.getCFRunLoop()) not get exit from runLoop.
Does anybody know where I'm mistaking ?
Thanks all!
PS: Here the Xcode project on GitHub
Finally I figured out some issues with my code.
First of all I need to remove CFRunLoopSource object from run loop CFRunLoopRemoveSource(). In according with documentation if RunLoop has no input sources then it exits immediately.
public func accept(_ data: Data) {
guard data.count > 0 else { return }
self.data += data
delegate?.stream?(self, handle: .hasBytesAvailable)
if let runLoopSource, let runLoop, let runLoopMode {
CFRunLoopRemoveSource(runLoop.getCFRunLoop(), runLoopSource, runLoopMode)
}
if let runLoop {
CFRunLoopWakeUp(runLoop.getCFRunLoop())
}
}
Second issue is related that I used XCTest environment and it's RunLoop didn't exit for some reasons (Ask the community for help).
I used real application environment and created Thread subclass to check my implementation. The thread by default has run loop without any input sources attached to it. I added input stream to it. And using main thread emulated that stream received data.
Here the Custom Thread implement that runs and sleep until it receive signal from BLEInputStream
class StreamThread: Thread, StreamDelegate {
let stream: BLEInputStream
init(stream: BLEInputStream) {
self.stream = stream
}
override func main() {
stream.delegate = self
stream.schedule(in: .current, forMode: RunLoop.Mode.default)
print("start()")
let tenSeconds = Double(10)
let dateInFuture = Date(timeIntervalSinceNow: tenSeconds)
RunLoop.current.run(until: dateInFuture)
print("after 10 seconds")
}
override func start() {
super.start()
}
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
if eventCode == .errorOccurred {
print("eventCode == .errorOccurred")
}
else if eventCode == .hasBytesAvailable {
print("eventCode == .hasBytesAvailable")
}
}
}
Here the some UIViewController methods which runs from main thread
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let baseDate = Date.now
let thread = StreamThread(stream: stream, baseDate: baseDate)
thread.start()
print("main thread pauses at \(Date.now.timeIntervalSince(baseDate))")
Thread.sleep(forTimeInterval: 2)
print("stream accepts Data \(Date.now.timeIntervalSince(baseDate))")
stream.accept(Data([1,2,3]))
}
Here the result:
Everything works as expected - the thread sleeps until input stream receive data. No processor resources consuming.
Although it's allowed to subclass InputStream, there is no good explanation in the documentation how to correctly implement custom InputStream
Related
I'm a student studying iOS development currently working on a simple AI project that utilizes SNAudioStreamAnalyzer to classify an incoming audio stream from the device's microphone. I can start the stream and analyze audio no problem, but I've noticed I can't seem to get my app to stop analyzing and close the audio input stream when I'm done. At the beginning, I initialize the audio engine and create the classification request like so:
private func startAudioEngine() {
do {
// start the stream of audio data
try audioEngine.start()
let snoreClassifier = try? SnoringClassifier2_0().model
let classifySoundRequest = try audioAnalyzer.makeRequest(snoreClassifier)
try streamAnalyzer.add(classifySoundRequest,
withObserver: self.audioAnalyzer)
} catch {
print("Unable to start AVAudioEngine: \(error.localizedDescription)")
}
}
After I'm done classifying my audio stream, I attempt to stop the audio engine and close the stream like so:
private func terminateNight() {
streamAnalyzer.removeAllRequests()
audioEngine.stop()
stopAndSaveNight()
do {
let session = AVAudioSession.sharedInstance()
try session.setActive(false)
} catch {
print("unable to terminate audio session")
}
nightSummary = true
}
However, after I call the terminateNight() function my app will continue using the microphone and classifying the incoming audio. Here's my SNResultsObserving implementation:
class AudioAnalyzer: NSObject, SNResultsObserving {
var prediction: String?
var confidence: Double?
let snoringEventManager: SnoringEventManager
internal init(prediction: String? = nil, confidence: Double? = nil, snoringEventManager: SnoringEventManager) {
self.prediction = prediction
self.confidence = confidence
self.snoringEventManager = snoringEventManager
}
func makeRequest(_ customModel: MLModel? = nil) throws -> SNClassifySoundRequest {
if let model = customModel {
let customRequest = try SNClassifySoundRequest(mlModel: model)
return customRequest
} else {
throw AudioAnalysisErrors.ModelInterpretationError
}
}
func request(_ request: SNRequest, didProduce: SNResult) {
guard let classificationResult = didProduce as? SNClassificationResult else { return }
let topClassification = classificationResult.classifications.first
let timeRange = classificationResult.timeRange
self.prediction = topClassification?.identifier
self.confidence = topClassification?.confidence
if self.prediction! == "snoring" {
self.snoringEventManager.snoringDetected()
} else {
self.snoringEventManager.nonSnoringDetected()
}
}
func request(_ request: SNRequest, didFailWithError: Error) {
print("ended with error \(didFailWithError)")
}
func requestDidComplete(_ request: SNRequest) {
print("request finished")
}
}
It was my understanding that upon calling streamAnalyzer.removeAllRequests() and audioEngine.stop() the app would stop streaming from the microphone and call the requestDidComplete function, but this isn't the behavior I'm getting. Any help is appreciated!
From OP's edition:
So I've realized it was a SwiftUI problem. I was calling the startAudioEngine() function in the initializer of the view it was declared on. I thought this would be fine, but since this view was embedded in a parent view when SwiftUI updated the parent it was re-initializing my view and as such calling startAudioEngine() again. The solution was to call this function in on onAppear block so that it activates the audio engine only when the view appears, and not when SwiftUI initializes it.
I don't believe you should expect to receive requestDidComplete due to removing a request. You'd expect to receive that when you call completeAnalysis.
I've consulted many variations of background app refresh for Apple Watch so that I can update the complications for my app. However, the process seems very much hit or miss and most of the time it doesn't run at all after some time.
Here is the code I currently have:
BackgroundService.swift
Responsibility: Schedule background refresh and handle download processing and update complications.
import Foundation
import WatchKit
final class BackgroundService: NSObject, URLSessionDownloadDelegate {
var isStarted = false
private let requestFactory: RequestFactory
private let logManager: LogManager
private let complicationService: ComplicationService
private let notificationService: NotificationService
private var pendingBackgroundTask: WKURLSessionRefreshBackgroundTask?
private var backgroundSession: URLSession?
init(requestFactory: RequestFactory,
logManager: LogManager,
complicationService: ComplicationService,
notificationService: NotificationService
) {
self.requestFactory = requestFactory
self.logManager = logManager
self.complicationService = complicationService
self.notificationService = notificationService
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(handleInitialSchedule(_:)),
name: Notification.Name("ScheduleBackgroundTasks"),
object: nil
)
}
func updateContent() {
self.logManager.debugMessage("In BackgroundService updateContent")
let complicationsUpdateRequest = self.requestFactory.makeComplicationsUpdateRequest()
let config = URLSessionConfiguration.background(withIdentifier: "app.wakawatch.background-refresh")
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
self.backgroundSession = URLSession(configuration: config,
delegate: self,
delegateQueue: nil)
let backgroundTask = self.backgroundSession?.downloadTask(with: complicationsUpdateRequest)
backgroundTask?.resume()
self.isStarted = true
self.logManager.debugMessage("backgroundTask started")
}
func handleDownload(_ backgroundTask: WKURLSessionRefreshBackgroundTask) {
self.logManager.debugMessage("Handling finished download")
self.pendingBackgroundTask = backgroundTask
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
processFile(file: location)
self.logManager.debugMessage("Marking pending background tasks as completed.")
if self.pendingBackgroundTask != nil {
self.pendingBackgroundTask?.setTaskCompletedWithSnapshot(false)
self.backgroundSession?.invalidateAndCancel()
self.pendingBackgroundTask = nil
self.backgroundSession = nil
self.logManager.debugMessage("Pending background task cleared")
}
self.schedule()
}
func processFile(file: URL) {
guard let data = try? Data(contentsOf: file) else {
self.logManager.errorMessage("file could not be read as data")
return
}
guard let backgroundUpdateResponse = try? JSONDecoder().decode(BackgroundUpdateResponse.self, from: data) else {
self.logManager.errorMessage("Unable to decode response to Swift object")
return
}
let defaults = UserDefaults.standard
defaults.set(backgroundUpdateResponse.totalTimeCodedInSeconds,
forKey: DefaultsKeys.complicationCurrentTimeCoded)
self.complicationService.updateTimelines()
self.notificationService.isPermissionGranted(onGrantedHandler: {
self.notificationService.notifyGoalsAchieved(newGoals: backgroundUpdateResponse.goals)
})
self.logManager.debugMessage("Complication updated")
}
func schedule() {
let time = self.isStarted ? 15 * 60 : 60
let nextInterval = TimeInterval(time)
let preferredDate = Date.now.addingTimeInterval(nextInterval)
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: preferredDate,
userInfo: nil) { error in
if error != nil {
self.logManager.reportError(error!)
return
}
self.logManager.debugMessage("Scheduled for \(preferredDate)")
}
}
#objc func handleInitialSchedule(_ notification: NSNotification) {
if !self.isStarted {
self.schedule()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
The flow for the above file's usage is that it will be used by the ExtensionDelegate to schedule background refresh. The first time, it'll schedule a refresh for 1 minute out and then every 15 minutes after that.
Here is the ExtensionDelegate:
import Foundation
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
private var backgroundService: BackgroundService?
private var logManager: LogManager?
override init() {
super.init()
self.backgroundService = DependencyInjection.shared.container.resolve(BackgroundService.self)!
self.logManager = DependencyInjection.shared.container.resolve(LogManager.self)!
}
func isAuthorized() -> Bool {
let defaults = UserDefaults.standard
return defaults.bool(forKey: DefaultsKeys.authorized)
}
func applicationDidFinishLaunching() {
self.logManager?.debugMessage("In applicationDidFinishLaunching")
if isAuthorized() && !(self.backgroundService?.isStarted ?? false) {
self.backgroundService?.schedule()
}
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
self.logManager?.debugMessage("In handle backgroundTasks")
if !isAuthorized() {
return
}
for task in backgroundTasks {
self.logManager?.debugMessage("Processing task: \(task.debugDescription)")
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
self.backgroundService?.updateContent()
backgroundTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
self.backgroundService?.handleDownload(urlSessionTask)
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
When the app is launched from background, it'll try to schedule for the first time in applicationDidFinishLaunching.
From my understanding, WKExtension.shared().scheduleBackgroundRefresh will get called in schedule, then after the preferred time WatchOS will call handle with WKApplicationRefreshBackgroundTask task. Then I will use that to schedule a background URL session task and immediately start it as seen in the updateContent method of BackgroundService. After some time, WatchOS will then call ExtensionDelegate's handle method with WKURLSessionRefreshBackgroundTask and I'll handle that using the handleDownload task. In there, I process the response, update the complications, clear the task, and finally schedule a new background refresh.
I've found it works great if I'm actively working on the app or interacting with it in general. But let's say I go to sleep then the next day the complication will not have updated at all.
Ideally, I'd like for it to function as well as the Weather app complication WatchOS has. I don't interact with the complication, but it reliably updates.
Is the above process correct or are there any samples of correct implementations?
Some of the posts I've consulted:
https://wjwickham.com/posts/refreshing-data-in-the-background-on-watchOS/
https://developer.apple.com/documentation/watchkit/background_execution/using_background_tasks
https://spin.atomicobject.com/2021/01/26/complications-basic-functionality/
i have a list of music at my firebase real time database and i am retriving them but i have 1000 musics data and i want to show loader when i reading data and stop loader when if there is a error(internet connection, or something else) or reading completed.
when i turn off the internet i couldn't get the data and can't stop loader to show error alert like there is no internet connection.
please help me how to handle that problem.
here is my code
didload function called from viewdidload()
private var musicArray = [ItemModal]() {
didSet {
view?.updateTableView()
}
}
func didLoad() {
view?.showLoader()
getAllMusics { ItemModal in
self.musicArray = ItemModal
self.view?.hideLoader()
}
}
func getAllMusics(completion: #escaping ([ItemModal]) -> Void) {
var musicArray = [ItemModal]()
ref.child("music").observeSingleEvent(of: .value) { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot {
guard let data = try? JSONSerialization.data(withJSONObject: rest.value as Any, options: []) else { return }
if let itemModal = try? JSONDecoder().decode(ItemModal.self, from: data) {
musicArray.append(itemModal)
}
}
completion(musicArray)
}
}
You can use reachability function by using https://github.com/ashleymills/Reachability.swift. To get to notify when the internet is turned off, you can implement reachabilityChanged Notification. In the selector method of reachabilityChanged, you can hide the loader.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged), name: .reachabilityChanged)
}
#objc func changed() {
if reachability?.isReachable {
//Continue success implementation
} else {
view?.hideLoder
//Implement Error handling
}
}
Context:
App with all data in CloudKit
ViewController calls a query to load the data for a tableview
tableview crashes because the array of data for the tableview hasn't
come back from CK
I've researched semaphores and have it nearly
working But can't seem to figure out where to place the
semaphore.signal() to get the exact right behaviour
within viewDidLoad, I call the function:
Week.fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
and the function:
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.async {
completion(weeks)
//Option 1: putting semaphore.signal() here means it never completes
// beyond initialization of the week records
}
//Option 2: putting semaphore.signal() here means it completes after the
// initialization of the Week items, but before completion(weeks) is done
// so the array isn't initialized in the view controller in time. so the
// VC tries to use weeks and unwraps a nil.
semaphore.signal()
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
Note: I have tested that the weeks array within the view controller is properly set eventually - so it does seem to be purely a timing issue :)
I've tested placement of .signal() and if I put it within the 'DispatchQueue.main.async' block, it never gets triggered - probably because that block itself is waiting for the signal.
However if I put it anywhere else, then the viewcontroller picks up at that point and the completion(weeks) doesn't get called in time.
Maybe it is obvious - but as my first time working with semaphores - I'm struggling to figure it out!
Update 1: It works with DispatchQueue(label: "background")
I was able to get it working once I twigged that the semaphore.wait() was never going to get called with semaphore.signal() on the main thread.
So I changed it from:
DispatchQueue.main.async
to
DispatchQueue(label: "background").async and popped the semaphore.signal() inside and it did the trick
Comments/critiques welcome!
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
NSLog("inside fetchWeeks in Week ")
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue(label: "background").async {
completion(weeks)
semaphore.signal()
}
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
}
Update 2: Trying to avoid use of semaphores
Per comment thread - we shouldn't need to use semaphores with CloudKit - so it is likely that I'm doing something stupid :)
moving fetchWeeks() to the viewController to try to isolate the issue...but it still blows up as fetchWeeks() has't completed before the code tries to execute the line after and use the weeks array
my viewController:
class ChallengeDetailViewController: UIViewController {
#IBOutlet weak var rideTableView: UITableView!
//set by the inbound segue
var challenge: Challenge?
// set in fetchWeeks based on the challenge
var weeks: [Week]?
override func viewDidLoad() {
super.viewDidLoad()
rideTableView.dataSource = self
rideTableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
rideTableView.delegate = self
fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
//This is where it blows up as weeks is nil
weeks = weeks!.sorted(by: { $0.weekSequence < $1.weekSequence })
}
//moved this to the view controller
func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.sync {
completion(weeks)
}
}
Model.currentModel.publicDB.add(operation)
}
Once again: Never use semaphores with the CloudKit API.
First of all declare data source arrays always as non-optional empty arrays to get rid of unnecessary unwrapping the optional
var weeks = [Week]()
The mistake is that you don't use the fetched data at the right place.
As the closure is asynchronous you have to proceed inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks
self?.weeks = weeks.sorted(by: { $0.weekSequence < $1.weekSequence })
}
or simpler
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
}
And if you need to reload the table view do it also inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
self?.rideTableView.reloadData()
}
To do so you have to call completion on the main thread
DispatchQueue.main.async {
completion(weeks)
}
And finally delete the ugly semaphore!
let semaphore = DispatchSemaphore(value: 0)
...
semaphore.signal()
...
semaphore.wait()
I want to run a block of code in 10 seconds from an event, but I want to be able to cancel it so that if something happens before those 10 seconds, the code won't run after 10 seconds have gone by.
I've been using this, but it's not cancellable:
static func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure
)
}
How can I accomplish this?
Swift 3 has DispatchWorkItem:
let task = DispatchWorkItem { print("do something") }
// execute task in 2 seconds
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task)
// optional: cancel task
task.cancel()
Update for Swift 3.0
Set Perform Selector
perform(#selector(foo), with: nil, afterDelay: 2)
foo method will call after 2 seconds
func foo()
{
//do something
}
To cancel pending method call
NSObject.cancelPreviousPerformRequests(withTarget: self)
Try this (Swift 2.x, see David's answer below for Swift 3):
typealias dispatch_cancelable_closure = (cancel : Bool) -> ()
func delay(time:NSTimeInterval, closure:()->()) -> dispatch_cancelable_closure? {
func dispatch_later(clsr:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), clsr)
}
var closure:dispatch_block_t? = closure
var cancelableClosure:dispatch_cancelable_closure?
let delayedClosure:dispatch_cancelable_closure = { cancel in
if let clsr = closure {
if (cancel == false) {
dispatch_async(dispatch_get_main_queue(), clsr);
}
}
closure = nil
cancelableClosure = nil
}
cancelableClosure = delayedClosure
dispatch_later {
if let delayedClosure = cancelableClosure {
delayedClosure(cancel: false)
}
}
return cancelableClosure;
}
func cancel_delay(closure:dispatch_cancelable_closure?) {
if closure != nil {
closure!(cancel: true)
}
}
// usage
let retVal = delay(2.0) {
println("Later")
}
delay(1.0) {
cancel_delay(retVal)
}
From Waam's comment here: dispatch_after - GCD in swift?
You need to do this:
class WorkItem {
private var pendingRequestWorkItem: DispatchWorkItem?
func perform(after: TimeInterval, _ block: #escaping VoidBlock) {
// Cancel the current pending item
pendingRequestWorkItem?.cancel()
// Wrap the request in a work item
let requestWorkItem = DispatchWorkItem(block: block)
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + after, execute:
requestWorkItem)
}
}
// Use
lazy var workItem = WorkItem()
private func onMapIdle() {
workItem.perform(after: 1.0) {
self.handlePOIListingSearch()
}
}
References
Link swiftbysundell
Link git
This should work:
var doIt = true
var timer = NSTimer.scheduledTimerWithTimeInterval(10, target: self, selector: Selector("doSomething"), userInfo: nil, repeats: false)
//you have now 10 seconds to change the doIt variable to false, to not run THE CODE
func doSomething()
{
if(doIt)
{
//THE CODE
}
timer.invalidate()
}
I use #sas 's method in some projects, somehow this doesn't work anymore, maybe something changed after Swift 2.1.1. value copy instead of pointer?
the easiest work around method for me is:
var canceled = false
delay(0.25) {
if !canceled {
doSomething()
}
}
For some reason, NSObject.cancelPreviousPerformRequests(withTarget: self) was not working for me. A work around I thought of was coming up with the max amount of loops I'd allow and then using that Int to control if the function even got called.
I then am able to set the currentLoop value from anywhere else in my code and it stops the loop.
//loopMax = 200
var currentLoop = 0
func loop() {
if currentLoop == 200 {
//do nothing.
} else {
//perform loop.
//keep track of current loop count.
self.currentLoop = self.currentLoop + 1
let deadline = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: deadline) {
//enter custom loop parameters
print("i looped")
self.loop()
}
}
and then elsewhere in your code you can then
func stopLooping() {
currentLoop = 199
//setting it to 199 allows for one last loop to happen. You can adjust based on the amount of loops you want to be able to do before it just stops. For instance you can set currentLoop to 195 and then implement a fade animation while loop is still happening a bit.
}
It's really quite dynamic actually. For instance you can see if currentLoop == 123456789, and it will run infinitely (pretty much) until you set it to that value somewhere else in your code. Or you can set it to a String() or Bool() even, if your needs are not time based like mine were.