How to animate cells and buttons throughout the app in RxSwift? - swift

I want to make click animations on every button and cell inside my app, basically all the views with actions. On every click I want a grayish blink.
The problem is the following: I made a custom class for button and the buttons across the app are working just fine. The problem is with cells as I try to make the animations rx-like. I have UITableView cells as well as UICollectionView cells and some views here and there, so I tried to do UIView extension which recognizes taps and if there is one - animation is played. I tried to do this with extension:
import UIKit
import RxSwift
extension UIView {
func makeSelectionIndicatable(forInitialBackgroundColor color: UIColor) {
let tapGesture = UILongPressGestureRecognizer()
tapGesture.minimumPressDuration = 0
tapGesture.cancelsTouchesInView = false
addGestureRecognizer(tapGesture)
tapGesture.rx.event.bind(onNext: { [weak self] recognizer in
if recognizer.state == .began {
self?.backgroundColor = .global(color: .athens_gray)
}
if recognizer.state == .ended {
self?.backgroundColor = color
}
if recognizer.state == .cancelled {
self?.backgroundColor = color
}
})
}
}
Obviously there is a dispose missing, but if I add one in extension - nothing will work. My question is: how to make animations rx-like, so that they'll be visible across app? Maybe I'm overthinking it and there are easier ways? Thx in advance

I can recommend RxDataSources
data.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, model, cell in
cell.textLabel?.text = model
}
RxDataSources provides two special data source types that automatically take care of animating changes in the bound data source: RxTableViewSectionedAnimatedDataSource and RxCollectionViewSectionedAnimatedDataSource

Related

animating a cell when pressed affects other cells

I want to animate a card when pressed.
I made a table view, and added a custom cell to it.
and in custom cells, I created a UIButton and applied .touchUpInside gesture on the button.
when the button is pressed, the background color of the card will turn to red.
but weird thing is, if I press the first button,
6th, 10th, 15th and more cards' background-color also changed.
I check the function called only once but it happens to several cards.
I assume this might be related to dequeueReusableCell cell but not sure...
can anybody please help me out?
#objc func cardPressed(){
changeCardBackground()
}
private func changeCardBackground(){
var color:UIColor = .red
if(isCardRed){
color = .white
}
isCardRed = !isCardRed
UIView.animate(withDuration: 0.3) {
self.cardButton.backgroundColor = color
}
}
TableviewCells are reused you need to add prepareForReuse Method like :
override func prepareForReuse() {
super.prepareForReuse()
self.cardButton.backgroundColor = defaultColor
}

#IBSegueAction with condition

In my app I want to perform a segue based on a UITapGestureRecognizer. In case the tap is in the top area of the screen, a segue to the SettingsView should be performed.
In UIKit this was quite simple. I've triggered the performSegue in the UITapGestureRecognizer by wrapping it in an if-statement.
Now I would like to write the SettingsView() in SwiftUI. The SettingsView() will be embedded in a UIHostingController.
My question is:
How can I perform the segue to this UIHostingController (while telling the UIHostingController which View to display)?
I tried to use the new #IBSegueAction. The reason to use this #IBSegueAction is that I can use it to tell the UIHostingController which View to display. The problem is that I can't insert a condition now. Wherever the tap is on the screen, the segue is performed. I haven't found a way to cancel the segue in #IBSegueAction.
My code currently looks like this:
#IBSegueAction func showSettingsHostingControler(_ coder: NSCoder, sender: UITapGestureRecognizer, segueIdentifier: String?) -> UIViewController? {
let location = sender.location(in: self.tableView)
if let _ = self.tableView.indexPathForRow(at: location) {
return nil
} else {
if location.y < 32 {
if location.x < view.bounds.midX {
direction = .left
} else {
direction = .right
}
return UIHostingController(coder: coder, rootView: SettingsView())
}
}
return nil
}
The result currently is that the app segues to a non-existing Nil-view when the tap is in the wrong area.
based on a UITapGestureRecognizer. In case the tap is in the top area of the screen
It sounds to me like your tap gesture recognizer is attached to the wrong view. There should not be any decision to make here. Position a view in the top area of the screen and attach the tap gesture recognizer to that. That way, if this view gets a tap, there is no decision to be made: the tap is in the right place.

view move up in particular textfield delegate

I have to move the UIView in only last UITextField in Swift 3.0 on mentioned below delegate method using tag,
func textFieldDidBeginEditing(_ textField: UITextField) {
if (textField.tag == 4){
//UIView Up
}
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if (textField.tag == 4){
//UIView Down
}
return true
}
I tried many codes but none of them are working like notification,..etc.
You need to add Observers into the NotificationCenter for listening to both when Keyboard goes up and down (i'll assume your textfield outlet is lastTextField for this example to work but this obviously have to be adapted to whatever name you've had provide for it)
IBOutlet weak var passwordTextField: UITextField!
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
(Code above can be added in viewDidLoad())
Then you add methods to be executed when those notifications arrive, like this:
func keyboardWillShow(_ notification:Notification) {
if view.frame.origin.y >= 0 && lastTextField.isFirstResponder {
view.frame.origin.y -= getKeyboardHeight(notification)
}
}
func keyboardWillHide(_ notification:Notification) {
if view.frame.origin.y < 0 {
view.frame.origin.y += getKeyboardHeight(notification)
}
}
Validations within those methods prevent double execution like moving up/down twice when moving between textfields without resigning first responder which is common in cases like your (i assume your doing this for a form hence the clarification you only need it for the fourth textfield). Notice i'm only doing validation in for the specified textfield (with its outlet lastTextField) in the keyboardWillShow method, this in case you move thor another textfield while the keyboard is shown and resign responder from it in which case, even though it isn't the original place where you started, the view will return to its original place when the keyboard is hidden.
You'll also need a method for getting keyboard's height, this one can help with that:
func getKeyboardHeight(_ notification:Notification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue // of CGRect
return keyboardSize.cgRectValue.height
}
Let me know how it goes but i just tested this same code on my app and it works so you should be fine.
PS: pay close attention to the storyboard (if you're using it) and that delegate for textfields are set up properly.
The problem you are trying to remedy is rather complicated, because it requires you to:
Find the textField which is firstResponder
Calculate where that TextField is relative to it's superViews
Determine the distance for the animation, so that the containing
superview doesnt jump out of the window, or jumps too
much/repeatedly
Animate the proper superView.
As you can see.. it's quite the algorithm. But luckily, I can help. However, this only works for a hierarchy which has the following layout:
superView (view in the case of UIViewController) > (N)containerSubviews > textFields
where N is an integer
or the following:
superView (view in the case of UIViewController) > textFields
The idea is to animate superView, based on which textField is firstResponser, and to calculate if it's position inside of the SCREEN implies that it either partially/totally obstructed by the Keyboard or that it is not positioned the way you want for editing. The advantage to this, over simply moving up the superView when the keyboard is shown in an arbitrary manner, is that your textField might not be positioned properly (ie; obstructed by the statusbar), and in the case where your textfields are in a ScrollView/TableView or CollectionView, you can simply scroll the texfield into the place you want instead. This allows you to compute that desired location.
First you need a method which will parse through a given superView, and look for which of it's subViews isFirstResponder:
func findActiveTextField(subviews : [UIView], textField : inout UITextField?) {
for view in subviews {
if let tf = view as? UITextField {
guard !tf.isFirstResponder else {
textField = tf; break
return
}
} else if !subviews.isEmpty {
findActiveTextField(subviews: view.subviews, textField: &textField)
}
}
}
Second, to aleviate the notification method, also make a method to manage the actual animation:
func moveFromDisplace(view: UIView, keyboardheight: CGFloat, comp: #escaping (()->())) {
//You check to see if the view passed is a textField.
if let texty = view as? UITextField {
//Ideally, you set some variables to animate with.
//Next step, you determine which textField you're using.
if texty == YourTextFieldA {
UIView.animate(withDuration: 0.5, animations: {
self./*the proper superView*/.center.y = //The value needed
})
comp()
return
}
if texty == YourTextFieldB {
// Now, since you know which textField is FirstResponder, you can calculate for each textField, if they will be cropped out by the keyboard or not, and to animate the main view up accordingly, or not if the textField is visible at the time the keyboard is called.
UIView.animate(withDuration: 0.5, animations: {
self./*the proper superView*/.center.y = //The Value needed
})
comp()
return
}
}
}
Finally, the method which is tied to the notification for the keyboardWillShow key; in this case, i have a UIViewController, with an optional view called profileFlow containing a bunch of UITextFields
func searchDisplace(notification: NSNotification) {
guard let userInfo:NSDictionary = notification.userInfo as NSDictionary else { return }
guard let keyboardFrame:NSValue = userInfo.value(forKey: UIKeyboardFrameEndUserInfoKey) as? NSValue else { return }
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let keybheight = keyboardHeight
var texty : UITextField? //Here is the potential textfield
var search : UISearchBar? //In my case, i also look out for searchBars.. So ignore this.
guard let logProfile = profileFlow else { return }
findActiveTextField(subviews: [logProfile], textField: &texty)
//Check if the parsing method found anything...
guard let text = texty else {
//Found something.. so determine if it should be animated..
moveFromDisplace(view: searchy, keybheight: keybheight, comp: {
value in
search = nil
})
return
}
//Didn't find anything..
}
Finally, you tie in this whole logic to the notification:
NotificationCenter.default.addObserver(self, selector: #selector(searchDisplace(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
I can't provide more content to the code, since it all depends on your view hierarchy, and how you want things to animate. So it's up to you to figure that out.
On a side note, usually, if you have so many textfields that to lay them out properly means they overstep the length of the screen.. it's probable that you could simplify your layout. A way to make this algorithm better would be to make sure you have all your textfields in one containing view, which again can become heavy for when, say, you use AutoLayout constraints. Odds are if you're in this situation, you can probably afford to add a flow of several views etc.
There is also the fact that i've never really needed to use this for iPhone views, more for iPad views, and even then for large forms only (e-commerce). So perhaps if you're not in that category, it might be worth reviewing your layout.
Another approach to this, is to use my approach, but to instead check for specific textFields right in the findActiveTextField() method if you only have a handful of textfields, and to animate things within findActiveTextField() as well if you know all of the possible positions they can be in.
Either way, i use inout parameters in this case, something worth looking into if you ask me.

How to print "image x tapped" when user taps on one of many UIImages in the View Controller? (Swift 3)

I have a set of 12 UIImageViews in my storyboard, and, for argument's sake, I want to get each one to print to logs "You just tapped image x", when the user taps on it, where x is the number of image tapped, from 1-12). So i need to detect which image is tapped, and do something depending on that information. What would be the best way to do this, in Swift 3 ?
(I assume 12 IBActions -treat them as button with an image on background- is really bad code. Also they need to be placed on specific positions on top of a background image, so cannot use UICollectionView to do this.) Thanks
First of all, I think using a collectionView is a better approach to achieve what do you want. However, you'll need to:
Set userInteractionEnabled to true for all of your imageViews.
Set -sequential- tag for all of your imageViews, for example image1.tag = 1, image2.tag = 2 ... and so on.
Implement a method to be the target of all of your images tapping, it should be similar to this:
func imageViewTapped(imageView: UIImageView) {
print("You just tapped image (imageView.tag)")
}
Create -one single- tap gesture and assign the implemented method to its selector:
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageViewTapped))
Finally, add the tapGesture for all of your image, for example: image1.addGestureRecognizer(tapGesture), image2.addGestureRecognizer(tapGesture)... and so on.
Hope this helps.
It's not bad code per-se, but if you did it that way, or with tap gestures on each of the ImageViews you would likely want to separate out that part of the logic.
There are other approaches/views you could use to manage this kind of thing better, especially if this is supposed to scale, but with your constraints this is what I would suggest:
Either add TapGestureRecognizers to your imageViews or make them buttons, then connect all their actions to this:
#IBAction func phoneWasPressed(sender: AnyObject) {
guard let tappedImageView = sender as? UIImageView else {
return
}
switch tappedImageView {
case imageView1:
//do something
case imageView2:
// do something else
//etc.
default:
break
}
switch sender
}
I don't think it's a bad idea at all to have 12 buttons. Assign each of them a tag from 1-12 and connect them to the same IBAction.
#IBAction func didTapImageButton(button: UIButton) {
print("You just tapped image \(button.tag)")
}

How to Disable Multi Touch on UIBarButtonItems & UI Buttons?

My app has quite a few buttons on each screen as well as a UIBarButtonItem back button and I have problems with people being able to multi click buttons. I need only 1 button to be clickable at a time.
Does anyone know how to make a UIBarButtonItem back button exclusive to touch?
I've managed to disable multi clicking the UIButtons by setting each one's view to isExclusiveTouch = true but this doesn't seem to count for the back button in the navigation bar.
The back button doesn't seem to adhere to isExclusiveTouch.
Does anyone have a simple work around that doesn't involve coding each and every buttons send events?
Many Thanks,
Krivvenz.
you can enable exclusive touch simply this will stop multiple touch until first touch is not done
buttton.exclusiveTouch = true
You could write an extension for UIBarButtonItem to add isExclusiveTouch?
you can simply disable the multi-touch property of the super view. You can also find this property in the storyboard.
you could try this for the scene where you want to disable multiple touch.
let skView = self.view as! SKView
skView.isMultipleTouchEnabled = false
I have found a solution to this. isExclusiveTouch is the solution in the end, but the reason why this property didn't do anything is because you need to set it also on all of the subviews of each button that you want to set as isExclusiveTouch = true. Then it works as expected :)
It's working fine in Swift
self.view.isMultipleTouchEnabled = false
buttonHistory.isExclusiveTouch = true
In addition of #Luky LĂ­zal answer, you need pay attention that all subviews of a view you want disable multi-touching (exactly all in hierarchy, actually subviews of subviews) must be set as isExclusiveTouch = true.
You can run through all of them recursively like that:
extension UIView
{
func allSubViews() -> [UIView] {
var all: [UIView] = []
func getSubview(view: UIView) {
all.append(view)
guard view.subviews.count > 0 else { return }
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
}
// Call this method when all views in your parent view were set
func disableMultipleTouching() {
self.isMultipleTouchEnabled = false
self.allSubViews().forEach { $0.isExclusiveTouch = true }
}