CGAffineTransform doesn't work with custom UIView - swift

I'm trying to create a custom view class that is a popup with a UICollectionView on it. I've created functions to animate the view in and out, but only the animate out function is actually working.
I'm setting constraints as follows...
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit(){
...
self.addSubview(collectionView)
collectionView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
collectionView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.7).isActive = true
...
self.animateIn()
}
Then to handle the animation I'm using the following...
func animateIn(){
self.collectionView.transform = CGAffineTransform(translationX: 0, y: self.collectionView.frame.height)
self.blurredBackgroundView.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.blurredBackgroundView.alpha = 1
self.collectionView.transform = .identity
})
}
And the animateOut function (which works) is...
func animateOut(){
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.collectionView.transform = CGAffineTransform(translationX: 0, y: self.collectionView.frame.height)
self.blurredBackgroundView.alpha = 0
}) { (complete) in
if complete {
self.removeFromSuperview()
}
}
}
But the initial CGAfineTransform isn't working so the view is appearing in its final position when it first appears. I'm confused as it's working perfectly for the animateOut function which is called when a button is pressed to close the popup.
Can anyone tell me what I'm doing wrong? I feel like there must be something basic I'm missing.
Thanks

You're trying to start your animation from an initializer, but by that time, the view isn't a part of view hierarchy. Since it's impossible to animate a view that isn't part of the view hierarchy, the transform is applied immediately. animateOut works fine, because when you call it the view is already displayed.
What you need to do is to call your animateIn function explicitly after the view has been added to view hierarchy
let yourView = YourView(frame: someFrame)
superview.addSubview(yourView)
yourView.animateIn()

Related

UIButton Animations not working on UIScrollView

I have button animations that are not working on my ScrollView. I have other VCs that are just UIViews and animations work just fine.
So I tried unchecking from the Attributes Inspector and adding...
self.scrollView.delaysContentTouches = false
...in my viewDidLoad, but it had effect.
I also found that tapping does not trigger the animation but holding on the button for a second or more does and haptic presses also trigger the animation.
This is my animation code:
extension UIButton {
func startAnimatingPressActions() {
addTarget(self, action: #selector(animateDown), for: [.touchDown, .touchDragEnter])
addTarget(self, action: #selector(animateUp), for: [.touchDragExit, .touchCancel, .touchUpInside, .touchUpOutside])
}
#objc private func animateDown(sender: UIButton) {
animate(sender, transform: CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95))
}
#objc private func animateUp(sender: UIButton) {
animate(sender, transform: .identity)
}
private func animate(_ button: UIButton, transform: CGAffineTransform) {
UIView.animate(withDuration: 0.4,
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 3,
options: [.curveEaseInOut],
animations: {
button.transform = transform
}, completion: nil)
}
}
I am able to get this animation working, but it doesn't feel quite right. So I'd rather not use the code below. But it does work. So I'm not sure what in the code above is causing the problem.
extension UIButton {
func pulsate() {
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.duration = 0.2
pulse.fromValue = 0.96
pulse.toValue = 1.0
pulse.repeatCount = 0
pulse.initialVelocity = 0.5
pulse.damping = 1.0
layer.add(pulse, forKey: nil)
}
}
I had the same issue and I solved unchecking the delay touch down button in the scrollview's attributes inspector :
ScrollView's attrobutes inspector

Adding a search bar to ui table view on overlay

I have managed to create, a dark overlay once clicked a search icon and have managed to add a table uiview on the dark overlay but now want to add a search bar within that table view. I cant seem to figure it out as my code seems different to everyone else's examples. I am new to swift so my code probably isn't the cleanest. can i ask if someone can show me how to do this as i am out of ideas. I have posted my code below can someone please show me where i'm going wrong.
Many thanks
class SearchLauncher: NSObject {
let blackView = UIView()
let tableView = UITableView()
#objc func showSearch() {
if let window = UIApplication.shared.keyWindow {
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
window.addSubview(blackView)
window.addSubview(tableView)
let height: CGFloat = 600
let y = window.frame.height - height
tableView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
blackView.frame = window.frame
blackView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackView.alpha = 1
self.tableView.frame = CGRect(x: 0, y: y, width: self.tableView.frame.width, height: self.tableView.frame.height)
}, completion: nil)
}
}
#objc func handleDismiss() {
UIView.animate(withDuration: 0.5) {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.tableView.frame = CGRect(x: 0, y: window.frame.height, width: self.tableView.frame.width, height: self.tableView.frame.height)
}
}
}
override init() {
super.init()
//start doing something here maybe
}
}```
How did you try to add a search controller?
I would do it by adding an UISearchController as a tableHeaderView of your table view. First make sure to add UISearchResultsUpdating protocol to your class.
class SearchLauncher: NSObject, UISearchResultsUpdating
Then add the search bar to your table view
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.sizeToFit()
searchController.searchResultsUpdater = self
//you probably don't need this
//searchController.dimsBackgroundDuringPresentation = false
tableView.tableHeaderView = searchController.searchBar
And then, implement the updateSearchResults(for:_) delegate method
func updateSearchResults(for searchController: UISearchController) {
filteredTableData.removeAll(keepingCapacity: false)
//filter the table data by using searchController.searchBar.text!
//filteredTableData = ...
self.tableView.reloadData()
}

Strange animation 'lag' after launch screen disappears and before viewDidAppear() method runs

After launch screen dismisses itself logo and title of my app (they are in container) should go closer to the top of the screen. Between dismissing launch screen and viewDidAppear method there is a strange 'blink' of my container in the background. As you can see I am using snapkit but it should have nothing to do with the problem. Here is my code:
class WelcomeScreenViewController: UIViewController {
var welcomeScreenView: WelcomeScreenView {
return view as! WelcomeScreenView
}
override func loadView() {
let contentView = WelcomeScreenView(frame: .zero)
view = contentView
}
override func viewDidLoad() {
super.viewDidLoad()
self.welcomeScreenView.checkWeatherButton.transform = CGAffineTransform(translationX: 0, y: 200)
self.welcomeScreenView.checkWeatherButton.addTarget(self, action: #selector(showCityChoiceVC), for: .touchUpInside)
navigationController?.isNavigationBarHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.welcomeScreenView.appNameLogoContainerVerticalConstraint?.isActive = false
self.welcomeScreenView.appNameLogoContainer.snp.makeConstraints({ (make) in
make.top.equalTo(self.welcomeScreenView).offset(100)
})
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: {
self.welcomeScreenView.layoutIfNeeded()
self.welcomeScreenView.checkWeatherButton.transform = CGAffineTransform(translationX: 0, y: 0)
}, completion: nil)
}
#objc private func showCityChoiceVC() {
self.navigationController?.pushViewController(RegisterViewController(), animated: true)
}
Blinking comes from setting constraints in viewDidAppear. Use viewWillAppear or viewDidLoad instead. viewDidAppear is invoked when your view actually appears on screen. So any changes that happen will be visible to the user.

Switching between UITextViews resets view Layout

I implemented some functions to move the UIView, whenever a TextField or a TextView is tapped. The height for the UIView to move upwards is calculated depending how much the keyboard would overlap the active TextField or TextView. When I tap outside of the TextField, or -View the keyboard will be dismissed and the View will be resetted. Now everything is working fine, but when I switch from one TextField directly to another above (without dismissing the keyboard) it seems like the UIView will return to the initial position, instead of just keeping the shifted view (because the now active TextView would not be overlapped by the keyboard since it is above the former). It looks like some method is called to reset the view, resulting in the keyboard overlapping the upper TextView. Is there a way to suppress this behavior?
private func observeKeyboardNotification(){
NotificationCenter.default.addObserver(self, selector: #selector(keyboardShow), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardHide), name: .UIKeyboardWillHide, object: nil)
}
var distanceOfKeyboardToTextView: CGFloat = 0
var activeTextElement: UIView?
var viewIsShifted = false
func keyboardShow(notification: NSNotification){
findActiveTextField(subviews: self.view.subviews, textField : &activeTextElement)
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue, let activeTextElement = activeTextElement, !viewIsShifted {
let viewYPosition = (activeTextElement.superview?.convert(activeTextElement.frame.origin, to: nil).y)! + activeTextElement.frame.height
let keyboardYPosition = view.frame.height - keyboardSize.height
distanceOfKeyboardToTextView = viewYPosition - keyboardYPosition
if distanceOfKeyboardToTextView > 0 {
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1, options: .curveEaseOut,
animations: {
self.customView.frame = CGRect(x: 0, y: self.customView.frame.origin.y - self.distanceOfKeyboardToTextView, width: self.customView.frame.width, height: self.customView.frame.height)
}, completion: nil)
viewIsShifted = true
}
}
activeTextElement = nil
}
func keyboardHide(){
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1, options: .curveEaseOut,
animations: {
self.customView.frame = CGRect(x: 0, y: self.customView.frame.origin.y + self.distanceOfKeyboardToTextView, width: self.customView.frame.width, height: self.customView.frame.height)
}, completion: nil)
viewIsShifted = false
distanceOfKeyboardToTextView = 0
}
func findActiveTextField (subviews : [UIView], textField : inout UIView?) {
guard textField == nil else { return }
for view in subviews {
if view.isFirstResponder {
textField = view
break
}
else if !view.subviews.isEmpty {
findActiveTextField (subviews: view.subviews, textField: &textField)
}
}
}
Update:
After tapping from an active textfield directly onto another textfield, keyboardShow is call, but since the view is already shifed, UIView.animate will not be performed. However, the view is resetted like no keyboard would be displayed, but since the other textfield is active, the keyboard is visible.
I dismiss the keyboard using this extension:
extension UIViewController {
//functions to hide the keyboard
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
}
}
and in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
observeKeyboardNotification()
self.hideKeyboardWhenTappedAround()
...
}

Sliding a UIView "Out of nowhere"

i am trying to create a custom dropdown menu. I will have several cards on my view which should have a dropdown menu when tapped.
Right now i am having the dropDownView having cardView.frame.maxY as its frame.origin.y value with a height of 0 and when I tap the card view I set the height of the dropDownView to its real height-value within an animation.
But that looks kind of ugly since it looks like it stretches out of nowhere. I want it to slide out of nowhere.
By that i mean it having its original size right away and sitting below the card view (cardView.frame.maxY = dropDownView.frame.maxY) When the cardView is tapped the dropDownView slides down (dropDownView.frame.origin.y = cardView.frame.maxY) within an animation.
The Problem is, that the dropDownView is bigger than the cardView. So when it sits behind the cardView it is visible above the cardView. I tried to illustrate the Problem :)
This is state A (Before View A[cardView] is tapped) (View C is just some Background View which should be visible above and below View A)
This is state B (after cardView is tapped)
Any Ideas how to solve this problem? Thank you!
In addition heres a little sample code:
class cardViewComplete: UIView {
var card: CardView!
var dropDownMenu: DropDownView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func initSubViews() {
self.clipsToBounds = true
card = CardView()
card.frame = self.bounds
card.addTarget(self, action: #selector(YellowTackleTicketComplete.ticketTapped), forControlEvents: .TouchDown)
dropDownMenu = DropDownView()
dropDownMenu.frame = CGRect(x: 0, y: self.bounds.maxY, width: self.bounds.width, height: 350)
dropDownMenu.hidden = true
dropDownMenu.backgroundColor = UIColor.clearColor()
self.addSubview(card)
self.insertSubview(dropDownMenu, belowSubview: card)
dropDownMenu)
}
func showDropdown() {
dropDownMenu.hidden = false
originalHeight = self.frame.size.height
print("showing")
if !animating {
animating = true
UIView.animateWithDuration(
0.7,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: [],
animations: {
self.frame.size.height = self.frame.size.height + 350
}, completion: { _ in
self.animating = false
}
)
}
self.setNeedsDisplay()
self.dropDownMenu!.setNeedsDisplay()
dropped = true
}
func ticketTapped() {
showDropdown()
}
}
What I would do is place both View A and View B inside a new view, we can call this containerView.
containerView should be big enough to hold both A and B (when B is moved down). Then set containerView to clip at bounds. So when View B is in the "up" position, it is sitting both behind View A, and clipped at the top of containerView. Therefore it is not seen at all.
Once you're ready for View B to drop to it's "down" position, just animate it going down, where it will appear to come out from the bottom of View A. Since the containerView's frame will extend far enough down to accommodate A and B (in it's down position), nothing will be clipped and both views will be visible.
card = CardView()
card.frame = self.bounds
card.addTarget(self, action: #selector(YellowTackleTicketComplete.ticketTapped), forControlEvents: .TouchDown)
dropDownMenu = DropDownView()
// I changed the frame to place it right underneath the card view
dropDownMenu.frame = CGRect(x: 0, y: card.frame.size.height - 350, width: self.bounds.width, height: 350)
dropDownMenu.hidden = true
dropDownMenu.backgroundColor = UIColor.clearColor()
let containerView = UIView()
containerView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: card.frame.size.height + dropDownMenu.frame.size.height)
containerView.backgroundColor = nil
containerView.clipsAtBounds = true
containerView.addSubview(dropDownMenu)
containerView.addSubview(card)