My application asks user question and get answer, each question has some constant time (for example 30 seconds) for answering. I want show user alert something like "Last (n) seconds..." and if user will not answer in that time - app should skips question.
Wrote some code, using DispatchQueue:
let timePerQuestion = 20
let timeStartAlert = 10
for i in (0..<timeStartAlert) {
DispatchQueue.main.asyncAfter(deadline: (.now() + .seconds(timePerQuestion-timeStartAlert+i))) {
self.failureLabel.text = "Left \(Int(timeStartAlert-i)) seconds..."
self.failureLabel.isHidden = false
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(timePerQuestion)) {
self.failureLabel.text = "Reseting question"
self.failureLabel.isHidden = false
self.quiz.skipQuestion()
self.playNewRound()
self.failureLabel.text = "Sorry, that's not it."
}
It works, but those code executes even if user answered question in time.
So how I can "reset" or "clear" DispatchQueue.main for prevent executing this code if user answered in time?
well you have to use scheduled Timer from class NStimer
the implementations are as follows:
you need to define a timer:
var timer = Timer()//NStimer()in older versions of swift
timer = scheduledTimer(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool)
in which the timeInterval is the period before the execution of the required function,invocationis the function you want to run, and repeats indicates if you want the function to repeat invocation until invalidated.
you can read more about NS timers in https://developer.apple.com/reference/foundation/timer
I hope it helps
It's better to use a array/dictionary of bool variables for this kind of problem.
You can have an array or dictionary as given in the following example.
var questionAnswered: [String: Bool] = ["1" : false, "2" : false, "3" : false, "4" : false, "5" : false]
Here, Key = question id and Value = a boolean indicating whether it is answered. You can update it depending on whether user answered the particular question or not. Then, you can use it in your code in the following way:
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(timePerQuestion)) {
if let answered: Bool = self.questionAnswered[questionID] {
if !answered {
DispatchQueue.main.async {
self.failureLabel.text = "Reseting question"
self.failureLabel.isHidden = false
self.quiz.skipQuestion()
self.playNewRound()
self.failureLabel.text = "Sorry, that's not it."
}
}
}
}
Feel free to suggest edits to make it better :)
Related
I'm trying to understand how to change a struct (or class?) in an array by using reduce. Creating 4 countdown timers, on tap pause the current timer and start the next. So I tried something like this:
var timer1 = CountdownTimer()
// var timer2 = CountdownTimer() etc.
.onTapGesture(perform: {
var timers: [CountdownTimer] = [timer1, timer2, timer3, timer4]
var last = timers.reduce (false) {
(setActive: Bool, nextValue: CountdownTimer) -> Bool in
if (nextValue.isActive) {
nextValue.isActive = false;
return true
} else {
return false
}
}
if (last) {
var timer = timers[0]
timer.isActive = true
}
})
############# CountdownTimer is a struct ######
struct CountdownTimer {
var timeRemaining: CGFloat = 1000
var isActive: Bool = false
}
This does not work, two errors I'm seeing
the timers in the array are copies of the timers, not the actual timer so changing them doesn't actually change the timers being displayed on screen.
nextValue (i.e. the next timer) can't be changed because it's a let variable in the reduce declaration. I don't know how to change this (or if it's even relevant because presumably it's a copy of the copy of the timer and not the one I actually want to change).
Am I approaching this in a way thats idiomatically wrong for Swift? How should I be changing the original timers?
I agree with Paul about the fact that this should likely all be pulled out into an observable model object. I'd make that model hold an arbitrary list of timers, and the index of the currently active timer. (I'll give an example of that at the end.)
But it's still worth exploring how you would make this work.
First, SwiftUI Views are not the actual view on the screen like in UIKit. They are descriptions of the view on the screen. They're data. They can be copied and destroyed at any time. So they're readonly objects. The way you keep track of their writable state is through #State properties (or similar things like #Binding, #StateObject, #ObservedObject and the like). So your properties need to be marked #State.
#State var timer1 = CountdownTimer()
#State var timer2 = CountdownTimer()
#State var timer3 = CountdownTimer()
#State var timer4 = CountdownTimer()
As you've discovered, this kind of code doesn't work:
var timer = timer1
timer.isActive = true
That makes a copy of timer1 and modifies the copy. Instead, you want WriteableKeyPath to access the property itself. For example:
let timer = \Self.timer1 // Note capital Self
self[keyPath: timer].isActive = true
Finally, reduce is the wrong tool for this. The point of reduce is to reduce a sequence to a single value. It should never have side-effects like modifying the values. Instead, you just want to find the right elements, and then change them.
To do that, it would be nice to be able to easily track "this element and the next one, and the last element is followed by the first." That seems very complicated, but it's surprisingly simple if you include Swift Algorithms. That gives cycled(), which returns a Sequence that repeats its input forever. Why is that useful? Because then you can do this:
zip(timers, timers.cycled().dropFirst())
This returns
(value1, value2)
(value2, value3)
(value3, value4)
(value4, value1)
Perfect. With that I can fetch the first active timer (keypath) and its successor, and update them:
let timers = [\Self.timer1, \.timer2, \.timer3, \.timer4]
if let (current, next) = zip(timers, timers.cycled().dropFirst())
.first(where: { self[keyPath: $0.0].isActive })
{
self[keyPath: current].isActive = false
self[keyPath: next].isActive = true
}
That said, I wouldn't do that. There are subtle requirements here that should be captured in a type. In particular, you have this assumption that there is only one active timer, but nothing enforces that. If that's what you mean, you should make a type that says so. For example:
class TimerBank: ObservableObject {
#Published private(set) var timers: [CGFloat] = []
#Published private(set) var active: Int?
var count: Int { timers.count }
init(timers: [CGFloat]) {
self.timers = timers
self.active = timers.startIndex
}
func addTimer(timeRemaining: CGFloat = 1000) {
timers.append(timeRemaining)
}
func start(index: Int? = nil) {
if let index = index {
active = index
} else {
active = timers.startIndex
}
}
func stop() {
active = nil
}
func cycle() {
if let currentActive = active {
active = (currentActive + 1) % timers.count
print("active = \(active)")
} else {
active = timers.startIndex
print("(init) active = \(active)")
}
}
}
With this, timerBank.cycle() replaces your reduce.
By using the modulus operator ( % ) on Index, we could cycle through last to first without zipping.
let timers = [\Self.timer1, \.timer2, \.timer3, \.timer4]
if let onIndex = timers.firstIndex(where: { self[keyPath: $0].isActive }) {
self[keyPath: timers[onIndex]].isActive = false
let nextIndex = (onIndex + 1) % 4 // instead of 4, could use timers.count
self[keyPath: timers[nextIndex]].isActive = true
}
I am currently working on a small Bingo app. I'm having a few issues with this, however.
First off, I have button that when clicked, change images showing that that piece is marked (intended to mark the numbers that have been called). I have an if statement as follows:
if BOneButton.isSelected && IOneButton.isSelected && NOneButton.isSelected && GOneButton.isSelected && OOneButton.isSelected {
winConditionLabel.text = "You win!"
func createAlert(_ sender: UIButton) {
let alert = UIAlertController(title: "Bingo!", message: "You win!", preferredStyle: .alert)
let action1 = UIAlertAction(title: "Play Again?", style: .default) { (action) in
self.NewBoardAction(sender)
self.dismiss(animated: true, completion: nil)
}
alert.addAction(action1)
present(alert, animated: true, completion: nil)
}
}
I am having a few issues with this. First off, the Alert window will not come up. Have I done something incorrectly? (This is my first time attempting to use Alerts, so that's very likely.) Secondly, the label, which I have been using to test to make sure that the program gets through the if statement in the first place, only updates after deselecting the final button in the row. This is the main issue I'm trying to fix right now. And lastly, I'm having trouble checking the calledNumbers array (which consists of exactly that - all the numbers that have been called) to make sure that the numbers that have been selected have been called.
I originally had something like this:
if BOneButton.isSelected && IOneButton.isSelected && NOneButton.isSelected && GOneButton.isSelected && OOneButton.isSelected && calledNumbers.contains(Int(randomBOne.text)) && calledNumbers.contains(Int(randomIOne.text)) && calledNumbers.contains(Int(randomNOne.text)) && calledNumbers.contains(Int(randomGOne.text)) && calledNumbers.contains(Int(randomOOne.text)) {
// do stuff here
}
But this wouldn't work at all. Is there a better way to do this? I'd really appreciate any help at all!
Pertaining to the issue with checking the array. Here is the code for the first letter. The others do essentially the same thing:
#IBAction func newNumberAction(_ sender: Any) {
// B NUMBERS
let randomLetter = ["B", "I","N","G","O"]
let randomIndex = Int(arc4random_uniform(UInt32(randomLetter.count))) // Gives random number from 0 - 4
if (randomLetter[randomIndex]) == (randomLetter[0]) // if statement for each index/letter possibility {
let randomBIndex = Int(arc4random_uniform(UInt32(listBNumbers.count))) // listBNumbers is the original array that randomizes a number from 1 - 15
if randomBIndex < 1 // makes sure there are still B numbers remaining and if not runs the newNumberAction again {
newNumberAction((Any).self)
} else {
let newBNumber = "\(listBNumbers[randomBIndex])" // creates unique variable for random BNumbers
let combinedNumber = (randomLetter[0]) + newBNumber
var calledBNumbers = [Int]() // creates array for called BNumbers
calledBNumbers.append(Int(newBNumber)!) // adds called B Number into new array
listBNumbers.remove(at: (Int(randomBIndex))) // removes B Number from bank of possible numbers that could be called, this should ensure that no number can be called twice
calledNumbers += calledBNumbers // adds called B Numbers to new array that will be used later to verify BINGO
newNumLabel.text = combinedNumber
// this randomizes the number and combines it with 'B' then displays it in the label's text. This is used to display the next called Number.
}
I know the append is working by checking a label that gives me the count of the variables in the calledNumbers array. Again, I apologize that I didn't provide enough information originally.
Your first problem the alert won't appear because you have the function declared inside of the if statement. Create it outside of the if statement then call it inside the if statement
Your second problem doesn't include enough context in the code for anyone to help you with. How would anyone other then yourself know what the "final button in the row" is?
Your last problem has the same issue as the second problem, "I'm having trouble checking the calledNumbers array". There isn't an array in your question so how can anyone help you with identifying the problem?
This should fix your First problem:
if BOneButton.isSelected && IOneButton.isSelected && NOneButton.isSelected && GOneButton.isSelected && OOneButton.isSelected {
winConditionLabel.text = "You win!"
createAlert() // now that this inside of the if statment it will run
}
// this should be declared outside of the if statement
func createAlert() {
let alert = UIAlertController(title: "Bingo!", message: "You win!", preferredStyle: .alert)
// your alert's code
}
You added array information but there still isn't enough context. This is the best I could do based on what I ASSUME you wanted. Your code still isn't clear enough. You should've added all the arrays with foo info. For now I added an array var listBNumbers = ["X", "Y", "Z"] with X,Y,Z as the listBNumbers values to use in your code since you didn't provide anything.
var listBNumbers = ["X", "Y", "Z"]
let randomLetter = ["B", "I","N","G","O"]
/*
// YOUR ORIGINAL CODE
let randomIndex = Int(arc4random_uniform(UInt32(randomLetter.count)))
*/
// 1. TEMPORARILY set this to 0 so that if statement below will run. Delete this and uncomment out your original code above when finished
let randomIndex = 0
if randomLetter[randomIndex] == randomLetter[0] {
/*
// YOUR ORIGINAL CODE
// randomIndex is of type INT there is no need to cast it as Int again in step 7B when your removing it from the listBNumbers array
let randomBIndex = Int(arc4random_uniform(UInt32(listBNumbers.count)))
*/
// 2. TEMPORARILY set this to 2 so that the else statement below will run. Delete this and uncomment out your original code above when finished
let randomBIndex = 2
if randomBIndex < 1 {
// I have no idea what this is
newNumberAction((Any).self)
} else {
// 3. newBNumber is now: Z
let newBNumber = "\(listBNumbers[randomBIndex])"
print("newBNumber: \(newBNumber)")
// 4. combinedNumber is now: BZ
let combinedNumber = randomLetter[0] + newBNumber
print("combinedNumber: \(combinedNumber)")
// 5. THIS IS AN ARRAY of type INT it will not accept Strings
var calledBNumbers = [Int]()
// 6A. newBNumber is of type STRING not a type INT. This line below CRASHES because what your saying is calledBNumbers.append(Int(Z)!). How can you cast a Z to an Int?
calledBNumbers.append(Int(newBNumber)!) // newBNumber contains the letter Z
// 6B. THIS IS MY ASSUMPTION seems like you wan to use
calledBNumbers.append(randomBIndex)
print("calledBNumbers: \(calledBNumbers.description)")
// 7A. check to see if the index your looking for is inside the listBNumbers array
if listBNumbers.indices.contains(randomBIndex){
// 7B. randomBIndex is ALREADY of type Int. Why are you casting it >>>Int(randomBIndex)
listBNumbers.remove(at: randomBIndex)
print("listBNumbers: \(listBNumbers.description)")
}
// 8. There is no context for me to even try and figure out what calledNumbers is so I can't give you any info about it
calledNumbers += calledBNumbers
// combinedNumber contains: BZ from the step 4 so that is what your newNumLabel.text will show
newNumLabel.text = combinedNumber
}
}
Here's a way to clean up this code so it's more readable. You need to break all of that down into smaller functions so its more readable:
if areButtonsSelected() == true && doesCalledNumbersContainBingo() == true{
// do stuff here
}
func areButtonsSelected() -> Bool {
if BOneButton.isSelected && IOneButton.isSelected && NOneButton.isSelected && GOneButton.isSelected && OOneButton.isSelected{
return true
}
return false
}
func doesCalledNumbersContainBingo() -> Bool {
let b = Int(randomBOne.text)
let i = Int(randomIOne.text)
let n = Int(randomNOne.text)
let g = Int(randomGOne.text)
let o = Int(randomOOne.text)
if calledNumbers.contains(b) && calledNumbers.contains(i) && calledNumbers.contains(n) && calledNumbers.contains(g) && calledNumbers.contains(o){
return true
}
return false
}
I'm trying to created an fun facts app. It displays a random string from an array every time, then that fact is deleted from the array. For me code, when the app is first launched it gets new array of facts and it saves the data when the app is closed and uses the array from the previous launch every time after the initial launch. My problem is I get an error "Thread 1: signal SIGABRT" when I try to remove a string from my array on my 4th last line. Please tell me what corrections I need to make. I am fairly new to programming. I appreciate all the help I get. Thanks for your time
import Foundation
let userDefaults = NSUserDefaults.standardUserDefaults()
func isAppAlreadyLaunchedOnce()->Bool{
let defaults = NSUserDefaults.standardUserDefaults()
if let isAppAlreadyLaunchedOnce = defaults.stringForKey("isAppAlreadyLaunchedOnce"){
println("App already launched")
return true
}
else{
defaults.setBool(true, forKey: "isAppAlreadyLaunchedOnce")
println("App launched first time")
return false
}
}
struct newFactBook {
let factsArray : NSMutableArray = [
"Ants stretch when they wake up in the morning.",
"Ostriches can run faster than horses.",
"Olympic gold medals are actually made mostly of silver.",
"You are born with 300 bones; by the time you are an adult you will have 206.",
"It takes about 8 minutes for light from the Sun to reach Earth.",
"Some bamboo plants can grow almost a meter in just one day.",
"The state of Florida is bigger than England.",
"Some penguins can leap 2-3 meters out of the water.",
"On average, it takes 66 days to form a new habit.",
"Mammoths still walked the earth when the Great Pyramid was being built."]
}
var checkLaunch = isAppAlreadyLaunchedOnce()
var oldFunFactsArray : NSMutableArray = []
if(checkLaunch == false){
oldFunFactsArray = newFactBook().factsArray
}
else if (checkLaunch == true){
oldFunFactsArray = userDefaults.objectForKey("key") as! NSMutableArray
}
func randomFacts1() -> (String, Int){
var unsignedArrayCount = UInt32(oldFunFactsArray.count)
var unsignedRandomNumber = arc4random_uniform(unsignedArrayCount)
var randomNumber = Int(unsignedRandomNumber)
return (oldFunFactsArray[randomNumber] as! String, randomNumber)
}
oldFunFactsArray.removeObjectAtIndex(randomFacts1().1) //this gives me the error
//oldFunFactsArray.removeValueAtIndex(randomFacts1().1, fromPropertyWithKey: "key") //this gives me the same error
//oldFunFactsArray.removeAtIndex(randomFacts1().1) //This gives me the error "NSMutableArray does not have a member named 'removeAtIndex'
userDefaults.setObject(oldFunFactsArray, forKey:"key")
userDefaults.synchronize()
println(oldFunFactsArray)
You will have to create a mutableCopy of the facts array to removeObjects from it.
if(checkLaunch == false){
oldFunFactsArray = newFactBook().factsArray.mutableCopy
}
Here's a struct I've written to convert an NSTimeInterval into a walltime-based dispatch_time_t:
public struct WallTimeKeeper {
public static func walltimeFrom(spec: timespec)->dispatch_time_t {
var mutableSpec = spec
let wallTime = dispatch_walltime(&mutableSpec, 0)
return wallTime
}
public static func timeStructFrom(interval: NSTimeInterval)->timespec {
let nowWholeSecsFloor = floor(interval)
let nowNanosOnly = interval - nowWholeSecsFloor
let nowNanosFloor = floor(nowNanosOnly * Double(NSEC_PER_SEC))
println("walltimekeeper: DEBUG: nowNanosFloor: \(nowNanosFloor)")
var thisStruct = timespec(tv_sec: Int(nowWholeSecsFloor),
tv_nsec: Int(nowNanosFloor))
return thisStruct
}
}
I've been trying to test the accuracy of it in a Playground, but my results are confusing me.
Here's the code in my Playground (with my WallTimeKeeper in the Sources folder):
var stop = false
var callbackInterval: NSTimeInterval?
var intendedTime: NSDate?
var intendedAction: ()->() = {}
func testDispatchingIn(thisManySeconds: NSTimeInterval){
intendedTime = NSDate(timeIntervalSinceNow: thisManySeconds)
intendedAction = stopAndGetDate
dispatchActionAtDate()
loopUntilAfterIntendedTime()
let success = trueIfActionFiredPunctually() //always returns false
}
func dispatchActionAtDate(){
let timeToAct = dateAsDispatch(intendedTime!)
let now = dateAsDispatch(NSDate())
/*****************
NOTE: if you run this code in a Playground, comparing the above two
values will show that WallTimeKeeper is returning times the
correct number of seconds apart.
******************/
dispatch_after(timeToAct, dispatch_get_main_queue(), intendedAction)
}
func loopUntilAfterIntendedTime() {
let afterIntendedTime = intendedTime!.dateByAddingTimeInterval(1)
while stop == false && intendedTime?.timeIntervalSinceNow > 0 {
NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode,
beforeDate: afterIntendedTime)
}
}
func trueIfActionFiredPunctually()->Bool{
let intendedInterval = intendedTime?.timeIntervalSinceReferenceDate
let difference = intendedInterval! - callbackInterval!
let trueIfHappenedWithinOneSecondOfIntendedTime = abs(difference) < 1
return trueIfHappenedWithinOneSecondOfIntendedTime
}
func dateAsDispatch(date: NSDate)->dispatch_time_t{
let intendedAsInterval = date.timeIntervalSinceReferenceDate
let intendedAsStruct = WallTimeKeeper.timeStructFrom(intendedAsInterval)
let intendedAsDispatch = WallTimeKeeper.walltimeFrom(intendedAsStruct)
return intendedAsDispatch
}
func stopAndGetDate() {
callbackInterval = NSDate().timeIntervalSinceReferenceDate
stop = true
}
testDispatchingIn(3)
...so not only doestrueIfActionFiredPunctually() always returns false, but the difference value--intended to measure the difference between the time the callback fired and the time it was supposed to fire--which in a successful result should be really close to 0, and certainly under 1--instead comes out to be almost exactly the same as the amount of time the callback was supposed to wait to fire.
In summary: an amount of time to wait is defined, and an action is set to fire after that amount of time. When the action fires, it creates a timestamp of the moment it fired. When the timestamp is compared to the value it should be, instead of getting close to zero, we get close to the amount of time we were supposed to wait.
In other words, it appears as if the action passed to dispatch_after is firing immediately, which it absolutely shouldn't!
Is this something wrong with Playgrounds or wrong with my code?
EDIT:
It's the code. Running the same code inside a live app gives the same result. What am I doing wrong?
I figured it out. It's a head-smacker. I'll leave it up in case anyone is having the same problem.
I was using NSDate().timeIntervalSinceReferenceDate to set my walltimes.
Walltimes require NSDate().timeIntervalSince1970!
The dispatch_after tasks all fired instantly because they thought they were scheduled for over forty years ago!
Changing everything to NSDate().timeIntervalSince1970 makes it work perfectly.
Moral: don't use walltimes unless you're sure your reference date is 1970!
I have a simple layout when the app is ran, just allowing the user to input some text, and if the text is right, the score counter will save the data received, and not run the for loop again
#IBAction func checkAnswer(sender: AnyObject) {
while canAdd {
if output.text == "hi" {
canAdd = false
rightOrWrong.text = "Right!"
scoreTotal += 1
count.text = String(scoreTotal)
} else {
canAdd = true
rightOrWrong.text = "Try again!"
}
}
}
What's the easiest way to do this?
Thanks
As your code stands now, if the user were to enter a wrong number, your loop would be performed over and over again an infinite number of times since canAdd would always equal true.
I think you're misunderstanding the purpose of a while loop... A while loop will continue to perform over and over and over again without pausing or waiting for user input as long a certain condition is met, so your current while loop can run forever thousands of times per second thus probably preventing user interaction and potentially crashing your program.
If you only want to score to increment/not increment once each time the button is pressed, you don't need to use the while loop. Just add your canAdd condition to your current if conditional. I think this is what you're trying to do:
#IBAction func checkAnswer(sender: AnyObject) {
if output.text == "hi" && canAdd == true {
canAdd = false
rightOrWrong.text = "Right!"
scoreTotal += 1
count.text = String(scoreTotal)
} else {
canAdd = true
rightOrWrong.text = "Try again!"
}
}