I'm a bit stumped on this :
Context
I have a 'workout session' which is in a tableView inside a UIViewController. Each cell has an exercise name, number of reps and a swap button. The swap button generates a new exercise from the exercise bank (i.e. if it says "bench press" but you don't have a bench press then you can swap it for something else).
Problem
My swap button code works and a new exercise populates the cell. However, when I press "Play" on my workout timer, it resets to what it was before!
Code - swap button
func swapButtonTapped(cell: WorkoutCell) {
let realmExercisePool = realm.objects(ExerciseGeneratorObject.self)
let index = Int(arc4random_uniform(UInt32(realmExercisePool.count)))
let newExercise = realmExercisePool[index].generateExercise()
cell.exerciseName.text = newExercise.name
cell.repsNumber.text = String(newExercise.reps)
}
Code - Timer
#IBAction func timerControlTapped(_ sender: UIButton) {
if isTimerRunning == false {
sender.setImage(#imageLiteral(resourceName: "timerPause"), for: UIControlState.normal)
runTimer()
isTimerRunning = true
swapButtonEnabled = false
workoutTableView.reloadData()
} else {
sender.setImage(#imageLiteral(resourceName: "timerPlay"), for: UIControlState.normal)
workoutTimerObject.invalidate()
isTimerRunning = false
}
}
Code - Run Timer
func runTimer() {
workoutTimerObject = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(WorkoutViewController.workoutTimer), userInfo: nil, repeats: true)
isTimerRunning = true
}
The original group of exercises are passed in an array of workoutExercises from the previous VC (a 'workout set up screen')
Thanks everyone!
For anyone else who has a similar problem, I managed to work this out.
This was happening because I was simply changing the 'content' of the cell in my swapButtonTapped and not the actual data source.
When I then called "reloadData()" in timerControlTapped, it was reloading the original data.
What I needed to do was replace the actual data by replacing the exercise at the selected index with a new one in the "selectedWorkoutExerciseArray".
I used this code :
func swapButtonTapped(cell: WorkoutCell) {
let myIndexPath = workoutTableView.indexPath(for: cell)
let realmExercisePool = realm.objects(ExerciseGeneratorObject.self)
let index = Int(arc4random_uniform(UInt32(realmExercisePool.count)))
let newExercise = realmExercisePool[index].generateExercise()
selectedWorkoutExerciseArray[(myIndexPath?.row)!] = newExercise
cell.exerciseName.text = newExercise.name
cell.repsNumber.text = String(newExercise.reps)
}
Related
I'm trying to create an app that is similar to SnapChat when it takes videos. You are supposed to hold down a button to start taking a video and then release it when you are done. I'm trying to implement a timeout feature after 7 seconds and also a progress bar by using a Timer object. However, when I try fire the timer, it only calls the given selector once and that's it even though I tell it to repeat itself.
Here's my code to initialize the button:
let photoButton:UIButton = {
let but = UIButton(type: .custom)
but.layer.cornerRadius = 40
but.layer.borderColor = UIColor.white.cgColor
but.layer.borderWidth = 4
but.clipsToBounds = true
but.addTarget(self, action: #selector(takeVideo), for: .touchDown)
but.addTarget(self, action: #selector(stopVideo), for: [.touchUpInside, .touchUpOutside])
but.translatesAutoresizingMaskIntoConstraints = false
return but
}()
Here's the function takeVideo that is called when the user starts to hold down the button. I initialize and fire the Timer object here:
#objc func takeVideo() {
progressBar.isHidden = false
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateProgressbar), userInfo: nil, repeats: true)
startTime = Date().timeIntervalSince1970()
timer?.fire()
}
This is the function stopVideo where I invalidate the Timer object:
#objc func stopVideo() {
timer?.invalidate()
// stop video
}
And this is the updateProgressBar function:
#objc private func updateProgressbar() {
let maxLength = 7.0
let difference = Date().timeIntervalSince1970 - startTime!
progressBar.progress = Float(difference / maxLength)
if difference >= maxLength {
stopVideo() // Invalidates the timer and will stop video.
}
}
It seems that this person has had a similar issue, but when I tried to incorporate his answer of presenting the current view controller using async, it still doesn't work. Here's how I present the video recording view controller:
let recorder = RecorderViewController()
recorder.db = db
DispatchQueue.main.async(execute: {
self.present(recorder, animated: true, completion: nil)
})
Edit: I've fixed the incorrectness concerning the Timer object's fireDate property (the fix is also fixed in the above code). It fixed my problem.
I've been trying to implement this toolbar, where only the 'Next' button is enabled when the top textField is the firstResponder and only the 'Previous' button is enabled when the bottom textField is the firstResponder.
It kind of works, but i need to execute my own code by accessing previous, next and done buttons action methods in other classes(like delegates)
Thanks in advance for your suggestions..
extension UIViewController {
func addInputAccessoryForTextFields(textFields: [UITextField], dismissable: Bool = true, previousNextable: Bool = false) {
for (index, textField) in textFields.enumerated() {
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
var items = [UIBarButtonItem]()
if previousNextable {
let previousButton = UIBarButtonItem(image: UIImage(named: "Backward Arrow"), style: .plain, target: nil, action: nil)
previousButton.width = 30
if textField == textFields.first {
previousButton.isEnabled = false
} else {
previousButton.target = textFields[index - 1]
previousButton.action = #selector(UITextField.becomeFirstResponder)
}
let nextButton = UIBarButtonItem(image: UIImage(named: "Forward Arrow"), style: .plain, target: nil, action: nil)
nextButton.width = 30
if textField == textFields.last {
nextButton.isEnabled = false
} else {
nextButton.target = textFields[index + 1]
nextButton.action = #selector(UITextField.becomeFirstResponder)
}
items.append(contentsOf: [previousButton, nextButton])
}
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: view, action: #selector(UIView.endEditing))
items.append(contentsOf: [spacer, doneButton])
toolbar.setItems(items, animated: false)
textField.inputAccessoryView = toolbar
}
}
}
I am calling this from other class as :
let field1 = UITextField()
let field2 = UITextField()
addInputAccessoryForTextFields([field1, field2], dismissable: true, previousNextable: true)
Although I'm not 100% convinced I understand your question, here goes:
From other classes, you want to call the actions of your buttons, but your actions are set to UITextField.becomeFirstResponder and UIView.endEditing.
Rather than call these methods directly, create your own methods the actions should call, and put these calls into those methods.
In addInputAccessoryForTextFields(...) change the previousButton's target and action to:
previousButton.target = self
previousButton.action = #selector(handlePreviousButton)
Now add the new method:
#objc func handlePreviousButton()
{
// you'll need to associate the previous button to a specific text field
// and hang onto that association in your class, such as in a property named textFieldRelatedToPreviousButton.
self.textFieldRelatedToPreviousButton.becomeFirstResponder()
}
Now you can call handlePreviousButton() directly from elsewhere in your class, if you wish, or even from other classes.
Update
I just noticed you're extending UIViewController. So you can't add storage by adding a property. You can add storage via objc_setAssociatedObject and then get it via objc_getAssociatedObject, however, to get around this. See this SO or this SO for details on that. So you can, for example, "attach" the textField to your previousButton so that you can access it via the handlePreviousButton() method you add to your extension. And you can pass in the previousButton as a parameter (the sender) to handlePreviousButton() too.
Update 2
Another approach to consider is to use the button's tag property to store the tag value of the related textField. (i.e. each button and its related textField would have the same tag value). So in handlePreviousButton(sender:UIBarButtonItem) you loop through all the UITextField children of your self.view and locate the one whose tag matches sender.tag . Then you can do what you need to that UITextField.
I have an array of image views.
var imageViewArray = [UIImageView(image: UIImage())]
I use a for loop to fill this array with images from urls. I want to make it so that when I touch one of these images it becomes hidden or alpha: 0. I tried this:
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(StoryVC.imageTapped))
newImage.userInteractionEnabled = true
newImage.addGestureRecognizer(tapGestureRecognizer)
And I tried adding a tag too but I can't figure out how to get the sender. I need to be able to run the function to hide the image and know which image to hide, that is the part i'm struggling with. Thanks in advance.
You get UITapGestureRecognizer object in your selector's parameter and it has a property view that gives you the view which has been tapped.
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTapped(_:)))
func imageTapped(_ sender: UITapGestureRecognizer) {
guard let tappedImage = sender.view else { return }
}
The selector should be a function within your class. Here's an example:
// Setting up the tapGestureRecognizers
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(pressed:))
newImage.userInteractionEnabled = true
newImage.addGestureRecognizer(tapGestureRecognizer)
// The function that handles the tap event
func pressed(sender: UIImageView!) {
if sender.alpha == 0{
sender.alpha = 1
}
else{
self.alpha = 0
}
}
Also make sure to double check my syntax, I don't write with Swift often, so it may have some small errors.
I am trying to have a button change title and color every second but there is lag. What happens is that when it changes from lets say YELLOW to RED the button will show R.. or simply ... for a split second. It does not happen every time the button changes, only a few times. This the the code I have.
#IBOutlet var tapButton: UIButton!
var color:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.yellowColor(), UIColor.greenColor()]
var colorName:[String] = ["RED", "BLUE", "YELLOW", "GREEN"]
func game(){
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(SecondViewController.subtractTime), userInfo: nil, repeats: true)
}
func subtractTime(){
seconds -= 1
let num1 = Int(arc4random_uniform(4))
let num2 = Int(arc4random_uniform(4))
tapButton.titleLabel?.textColor = color[num1]
tapButton.setTitle(colorName[num2], forState: UIControlState.Normal)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
game()
}
I have changed the segues to Show Details, this seems to have fixed my problem. I also have the timers stop when I change view controllers.
I no longer get the lag that I was experiencing before.
I created two UIBarButtonItem buttons inside viewDidLoad to be grouped on the right side of the navigation bar so that when certain condition is satisfied, one will be enabled and the other disabled and vice versa.
the buttons work fine except recognizing the condition and being enabled and disabled based on the condition.
override func viewDidLoad() {
super.viewDidLoad()
var btnDone: UIBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .Done,
target: self,
action: "btnDoneFunction")
btnDone.style = UIBarButtonItemStyle.Done
let btnAdd: UIBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .Add,
target: self,
action: "btnAddFunction")
var buttons = [btnAdd, btnDone]
self.navigationItem.rightBarButtonItems = buttons
if checked.count >= 1 {
btnDone.enabled = true
btnAdd.enabled = false
} else {
btnDone.enabled = false
btnAdd.enabled = true
}
}
checked is an array that holds row number of checked rows in the table inside didSelectRowAtIndexPath.
I tried moving the condition part:
if checked.count >= 1 {
btnDone.enabled = true
btnAdd.enabled = false
} else {
btnDone.enabled = false
btnAdd.enabled = true
}
inside of didSelectRowAtIndexPath, but i can not access the buttons there.
Is there any way I can implement this? I just want to check if there are any rows checked in the table and disable the 'Add' button and enable the 'Done' button. If no row is checked, the reverse.
This is inside a UITableViewController class.
You are creating the buttons in viewDidLoad so the scope of the variable is just for that function, you need to declare it inside the class.
class myClass{
lazy var btnDone:UIBarButtonItem = {
var done: UIBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .Done,
target: self,
action: "btnDoneFunction")
return done
}()
func anotherFunc(){
if checked.count >= 1 {
btnDone?.enabled = true
} else {
btnDone?.enabled = false
}
}
}
I create a generic example above that declares a btnDone as a class variable with lazy initialization, that means it will be create just as need