UIButton is not clickable after first click - ios

I am trying to bring a subview from bottom when button is clicked. But only for the first time the button is clickable. For second click after animation button is not clickable.
Here is the code.
class AnimateView: UIView {
var button: UIButton!
var menuView: UIView!
var mainView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
mainView = UIView(frame: CGRect(x: 0, y: 0, width:
self.frame.size.width, height: self.frame.size.height))
mainView.clipsToBounds = true
mainView.backgroundColor = .clear
mainView.isUserInteractionEnabled = true
mainView.isExclusiveTouch = true
self.addSubview(mainView)
let theRect = CGRect(x: self.frame.size.width / 2 - 44 / 2, y: 0,
width: 44, height: 44)
button = UIButton(frame: theRect)
check.layer.cornerRadius = btnView.frame.size.height / 2
mainView.addSubview(self.check)
mainView.bringSubview(toFront: check)
check.addTarget(self, action: #selector(buttonClick(_:)),
for:.touchUpInside)
let newRect = CGRect(x: 0, y: check.frame.height, width:
self.frame.size.width, height: self.mainView.frame.size.height)
menuView = UIView(frame: newRect)
menuView.backgroundColor = .blue
mainView.addSubview(menuView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func buttonClick(_ sender: UIButton) {
toggleMenu()
}
func toggleMenu() {
if check.transform == CGAffineTransform.identity {
UIView.animate(withDuration: 1, animations: {
self.check.transform = CGAffineTransform(scaleX: 12, y: 12)
self.mainView.transform = CGAffineTransform(translationX: 0, y: -120)
self.check.transform = CGAffineTransform(rotationAngle: self.radians(180)) // 180 is 3.14 radian
self.setNeedsLayout()
}) { (true) in
UIView.animate(withDuration: 0.5, animations: {
})
}
} else {
UIView.animate(withDuration: 1, animations: {
// .identity sets to original position
//self.btnView.transform = .identity
self.mainView.transform = .identity
self.button.transform = .identity
})
}
}
}
This class is called from another UIView class like this
class MainViewClass: UIView {
var animateView: AnimateView!
override init(frame: CGRect) {
animateView = AnimateView(frame: CGRect(x: 0, y: self.frame.size.height - 44 - 44 - 20, width: self.frame.size.width, height: 122+44))
//animateView.clipsToBounds = true
self.addSubview(animateView)
self.bringSubview(toFront: animateView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When the screen first appears:
Initially the button is clickable and when it is clicked it moves up along with subview.
Now the button is not clickable. When I saw the frame of button it is same as when screen initially appears with button touching the screen bottom even though it moves up.
What I am intending is to show the screen with animation from bottom when button is clicked and move to default position for second time click.
But if I place the button as subview inside self, not as subview for mainView, then the button is clickable but it remains in the same position even after view is animated and moved up.
self.addSubview(button)

So, I finally got the solution of what I was intending to achieve before and after animation.
The reason for the UIButton not receiving any touch event after the animation is that after animation, the UIButton moves upward as it is subview of mainView. Due to this, it is out of bound of its superView and does not respond to any click event. Although I moved button along with its superView which is mainView, but button did not respond to touch event.
Then, I tested the whether the button bound is within mainView using hitTest:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let pointForTargetView: CGPoint = button.convert(point, from: mainView)
if button.bounds.contains(pointForTargetView) {
print("Button pressed")
return button.hitTest(pointForTargetView, with:event)
}
return super.hitTest(point, with: event)
}
This if condition:
button.bounds.contains(pointForTargetView)
Returns false when button is pressed after animation.
So, I need to capture touch event on subview which is out the its superview frame.
Using hitTest() solves my problem.
Here's the link which helped me. Capturing touches on a subview outside the frame of its superview using hitTest:withEvent:
Swift 3
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if clipsToBounds || isHidden || alpha == 0 {
return nil
}
for subview in subviews.reversed() {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
The screen shots:
When screen first appears, button is at bottom of screen, subview is hidden below the button.
When the button is clicked, mainView animates upward and button moves upward as it is subview of mainView.
When button is clicked again, mainView goes to previous position so does the button.

Related

Custom UIView class is not animating as expected

I have a custom UIView class that creates a square with a red background and I'm trying to get a green square slide on top of it from the left. I have a button from the main view that brings me to the screen above but the green screen doesn't animate over it. It just appears at the end result. I do know that I am creating these shapes when I change the initial width of the green square but no indication of an animation.
MainViewController.swift
private func configureAnimatedButton() {
//let sampleAnimatedButton
let sampleAnimatedButton = AnimatedButtonView()
sampleAnimatedButton.center = CGPoint(x: self.view.frame.size.width / 2,
y: self.view.frame.size.height / 2)
self.view.addSubview(sampleAnimatedButton)
sampleAnimatedButton.animateMiddleLayer()
}
AnimatedButton.swift
import Foundation
import UIKit
public class AnimatedButtonView: UIView {
//initWithFrame to init view from code
let movingLayer = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
//common func to init our view
private func setupView() {
//backgroundColor = .red
self.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
self.backgroundColor = .red
movingLayer.frame = CGRect(x: self.frame.maxX, y: self.frame.minY, width: 0, height: 300)
movingLayer.backgroundColor = .green
self.addSubview(movingLayer)
}
public func animateMiddleLayer() {
UIView.animate(withDuration: 10.0, delay: 1.2, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
var grayLayerTopFrame = self.movingLayer.frame
grayLayerTopFrame.origin.x = 0
grayLayerTopFrame.size.width += 300
self.movingLayer.frame = grayLayerTopFrame
self.layoutIfNeeded()
}, completion: { finished in
print("animated")
})
}
}
You should move grayLayerTopFrame outside the UIView.animate(), before it starts. Also, you don't need layoutIfNeeded if you're just animating the frame (you use it when you have constraints).
var grayLayerTopFrame = self.movingLayer.frame
grayLayerTopFrame.origin.x = 0
grayLayerTopFrame.size.width += 300
UIView.animate(withDuration: 10.0, delay: 1.2, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
self.movingLayer.frame = grayLayerTopFrame
self.layoutIfNeeded()
}, completion: { finished in
print("animated")
})
Also, make sure you don't call it in viewDidLoad() -- at that time, the views aren't laid out yet.

Swift: Changing (translate) a UIView position through a pan gesture in its superview window

Introduction
Context:
In my main ViewController I have a scrollView with a few objects inside (which are UIViews). When one of the UIViews are tapped/selected I animate forward a UITextView in a UIView to go with the selected object. (only one UIView can appear at a time)
This UIView that appears on object selection is separated into a separate class called AdjunctiveTextView.
Issue/goal:
(the example code provided below will clear make this clear, I've also commented where the issue lies in the code)
When an object has been tapped and has an adjacent UIView with a text I want to have that adjacent UIView to follow with the scrollView.
I'm using a UIPanGestureRecognizer to attempt to do this. But I can't figure out how to make it work when the user drags in the scrollview. It only work if the user drags on the actual adjunctiveTextView.
Everything works as expected except that the adjunctiveTextView does not change its position during the panGesture.
I would like (if possible) to have the AdjunctiveTextView as a separate class. My ViewController file is getting rather big.
Question:
Why doesn't the UIPanGestureRecognizer work as expected? What is needed in order for it to translate the backView correctly?
Code
My attempt: (as shown below)
My attempt simply makes the backView itself "dragable" around through the panGesture. Nothing happens to it when I scroll the scrollView.
(I have only included relevant portions of my code)
class ViewController: UIViewController {
let adjunctiveTextView = AdjunctiveTextView()
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
** adjunctiveTextView.scrollView = scrollView // **Edited! (scrollView is the name of the scrollView in this class too)
adjunctiveTextView.showView(passInObject: AvailableObject)
}
}
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
}
lazy var textView: UITextView = {
//textView setup
}
//additional init and setup
** weak var scrollView : UIScrollView! // **Edited!
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// window.addGestureRecognizer(panG)
** scrollView.addGestureRecognizer(panG) // **Edited!
window.addSubview(backView)
textView.text = passInObject.information
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window) //Have tried setting this to scrollView also
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window) //Have tried setting this to sccrollView also
break
case .ended:
break
default:
break
}
}
}
}
Thanks for reading my question.
I just add a weak reference to your scrollView and then add the pan gesture to scrollView. It works as you want. You may consider add another pan gesture to the back view if you want your original behavior.
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
return UIView.init()
}()
lazy var textView: UITextView = {
//textView setup
return UITextView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 100))
}()
weak var scrollView: UIScrollView!
//additional init and setup
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// passInObject.addGestureRecognizer(panG)
scrollView.addGestureRecognizer(panG)
window.addSubview(backView)
textView.text = passInObject.information
textView.backgroundColor = UIColor.blue
backView.addSubview(textView)
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.frame.minY, width: window.frame.width - passInObject.frame.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width , y: passInObject.frame.minY , width: window.frame.width - passInObject.frame.maxX - 6, height: self.textView.bounds.height + 5)
self.backView.backgroundColor = UIColor.red
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window)
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window)
break
case .ended:
break
default:
break
}
}
}
}
class ObjectScrollView: UIScrollView{
}
class AvailableObject: UIView{
var information: String!
}
class MySCNViewController: UIViewController {
#IBOutlet weak var oScrollView: ObjectScrollView!
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
adjunctiveTextView.showView(passInObject: object)
}
let adjunctiveTextView = AdjunctiveTextView()
let ao = AvailableObject.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
ao.information = "test"
adjunctiveTextView.scrollView = oScrollView
ao.backgroundColor = UIColor.yellow
}
#IBAction func tap(_ sender: Any?){
scrollViewObjectIsTapped(oScrollView, object: ao)}
}

Adding a UIPanGestureRecognizer to UITableViewCell causes odd behaviour in iPhone X Landscape

I have some code in my HW Cell as seen below. I am leaving out the extra code that appears to be largely unrelated.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.recognizer = UIPanGestureRecognizer(target: self, action: #selector(HWCell.handlePan(_:)))
self.recognizer.delegate = self
addGestureRecognizer(self.recognizer)
}
override func awakeFromNib() {
super.awakeFromNib()
self.cardView.layer.masksToBounds = false
self.layer.masksToBounds = false
self.contentView.layer.masksToBounds = false
self.layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
self.cardView.layoutIfNeeded() //Needed for iOS10 since layoutSubviews() reports inaccurate frame sizes.
self.cardView.layer.cornerRadius = self.cardView.frame.size.height / 3
self.cardView.layer.masksToBounds = false
}
//MARK: - horizontal pan gesture methods
func handlePan(_ recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .began {
// when the gesture begins, record the current center location
originalCenter = self.center
//originalCenter = self.cardView.center
self.bringSubview(toFront: self.contentView)
self.originalAlpha = self.cardView.alpha
}
// 2
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
self.center = CGPoint(x: originalCenter.x + translation.x, y: originalCenter.y)
// 3 //Remembered, state == .Cancelled when recognizer manually disabled.
if recognizer.state == .ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if (!deleteOnDragRelease && !completeOnDrag) {
// if the item is not being deleted, snap back to the original location
UIView.animate(withDuration: 0.2, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: { self.frame = originalFrame }, completion: nil)
if (self.task?.completed == false) {
UIView.transition(with: self.completionImageView,
duration: 0.2,
options: UIViewAnimationOptions.transitionCrossDissolve,
animations: { self.completionImageView.image = UIImage(named: "Grey Checkmark") },
completion: nil)
}
}
What happens is that, and keep in mind that this ONLY HAPPENS on iPhone X in Landscape mode, is the cell extends itself further by stretching instead of simply moving when I drag my finger across it.
You can see this behaviour in a video I uploaded: https://www.youtube.com/watch?v=qPXZWwnuWhU&feature=youtu.be
This odd behaviour never occurs in iOS10, only in iOS 11, on this particular device and orientation.
My constraints are blue and 0 warnings/errors, I played around with them to see if the safe area was related to this problem. The problem is gone only if I set the leading/trailing constraints of the UITableView to line up with the Safe Area. If they are lined up with the Superview, this error occurs.
Anyone have an idea what could be causing this?

How do i tell my UIView that the button in UIViewController has been pressed? (Swift)

Is it possible to pass data between the UIViewController and the UIView?
if yes, how?
I can not find anything on the internet which explains it.
For example when you press button1 the UIViewController says to the UIView that the person has pressed button1 and then the UIView draws a Circle. But if you then press button2 the UIViewController says to the UIView that he has now pressed button2 and then the UIView eliminates the Circle and draws a triangle.
The buttons are built programmatically.
The question is how I can invoke the shapes which I have drawn with drawRect within the UIView in the UIViewController.
class ViewController: UIViewController {
#IBOutlet var UnitViewTabeller: UnitView!
var btn: UIButton!
override viewDidLoad {
self.btn = UIButton(frame: CGRectMake(0.04 * view.bounds.width, 0.91 * view.bounds.height, 0.44 * view.bounds.width, 0.07 * view.bounds.height)) //set frame
self.btn.layer.cornerRadius = 10
self.btn.setTitle("0", forState: .Normal) //set button title
self.btn.setTitleColor(UIColor.whiteColor(), forState: .Normal) //set button title color
self.btn.backgroundColor = UIColor(red: 0.2745, green: 0.2784, blue: 0.2706, alpha: 1.0) //set button background color
self.btn.tag = 0 // set button tag
self.btn.addTarget(self, action: "btnclicked:", forControlEvents: .TouchUpInside) //add button action
self.view.addSubview(btn) //add button in view
self.btnArray.append(btw)
}
func btnclicked(sender: UIButton) {
//draw the Circle from the UIView
}
}
class CircleView: UIView {
override func drawRect(rect: CGRect) {
let Circle = UIGraphicsGetCurrentContext()
let rectangle = CGRect(x: 0.2 * bounds.width, y: 0.15 * bounds.height, width: 0.6 * bounds.width, height: 0.6 * bounds.width)
CGContextSetLineWidth(Circle, 4.0)
CGContextAddEllipseInRect(Circle, rectangle)
CGContextStrokePath(Circle)
}
}
Here is an example in Swift 3 of a ShapeView that redraws itself when its shape property is set:
class ShapeView: UIView {
var shape: Shape = .blank {
didSet {
// calling setNeedsDisplay() triggers a redraw of ShapeView
setNeedsDisplay()
}
}
enum Shape {
case circle
case triangle
case blank
}
override func draw(_ rect: CGRect) {
switch shape {
case .circle: drawCircle()
case .triangle: drawTriangle()
default: break
}
}
func drawCircle() {
let circle = UIGraphicsGetCurrentContext()
let rectangle = CGRect(x: 0.2 * bounds.width, y: 0.15 * bounds.height, width: 0.6 * bounds.width, height: 0.6 * bounds.width)
circle?.setLineWidth(4.0)
circle!.addEllipse(in: rectangle)
circle!.strokePath()
}
func drawTriangle() {
// code to draw a triangle
}
}
Assuming you have a ShapeView object called shapeView as a property of your ViewController, you'd call it like this:
func drawCircleButton(_ sender: UIButton) {
shapeView.shape = .circle
}
func drawTriangleButton(_ sender: UIButton) {
shapeView.shape = .triangle
}

Darkened overlay while user is typing

When the user taps on a text field, I wanted to darken the rest of the screen (everything below the text box, above the keyboard) to make it clear what they should be doing. I believe it involved putting a transparent UI view down and adding a gesture recognizer to it, but I'm not quite sure how to do that.
I've got the following code for when the user arrives on the screen. Is this where I would add the new UI View?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
}
Thanks!
I created a subclass of UIView for two reasons:
you can add a gesture recogniser to it
you can add a delegate to it
Since the last subview added to a view is on top I first add the dark layer and then call bringSubviewToFront(textview) this will put the dark layer between the textview in question and everything else.
I created a protocol with one function. This function get's called by the gesture recogniser and returns the instance of DarkView to the delegate. The delegate (your ViewController) can then remove it from it's superview.
This you can do without a delegate function, but you also have to call resignFirstResponder() on the textfield.
Don't forget to set up the delegate of the DarkView in your ViewController.
Just a VC with some textfields.
class FirstViewController: UIViewController, DarkViewDelegate, UITextFieldDelegate {
var masterView : UIView!
override func viewDidLoad() {
super.viewDidLoad()
masterView = UIView(frame: self.view.frame)
masterView.backgroundColor = UIColor.whiteColor()
let textField1 = UITextField(frame: CGRect(x: 50, y: 20, width: 300, height: 20))
let textField2 = UITextField(frame: CGRect(x: 50, y: 60, width: 300, height: 20))
let textField3 = UITextField(frame: CGRect(x: 50, y: 100, width: 300, height: 20))
let textField4 = UITextField(frame: CGRect(x: 50, y: 140, width: 300, height: 20))
func styleTextField(field : UITextField) {
field.layer.borderColor = UIColor.blackColor().CGColor
field.layer.borderWidth = 2
field.layer.cornerRadius = field.frame.size.height / 2
field.backgroundColor = UIColor.whiteColor()
field.delegate = self
masterView.addSubview(field)
}
styleTextField(textField1)
styleTextField(textField2)
styleTextField(textField3)
styleTextField(textField4)
self.view.addSubview(masterView)
// Do any additional setup after loading the view, typically from a nib.
}
// delegate function of a textfield
func textFieldDidBeginEditing(textField: UITextField) {
focusUserOn(textField) // darken everything else
}
// delegate function of DarkView undarken everything
func tappedDark(view: DarkView) {
guard let superV = view.superview else {
return
}
if let textField = superV.subviews.last as? UITextField {
textField.resignFirstResponder() // also stop editing
}
view.removeFromSuperview()
}
func focusUserOn(textfield: UITextField) {
guard let superV = textfield.superview else {
return
}
let darkArea = DarkView(frame: superV.bounds)
darkArea.delegate = self
superV.addSubview(darkArea)// add DarkView (everything is dark now)
superV.bringSubviewToFront(textfield) // bring the textview back to the front.
}
}
simple subclass of UIView with a gesture recogniser
class DarkView : UIView, UIGestureRecognizerDelegate {
weak var delegate : DarkViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.4)
let tap = UITapGestureRecognizer(target: self, action: Selector("tapped"))
tap.delegate = self
self.addGestureRecognizer(tap)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("deinit dark")
}
func tapped() {
guard let del = self.delegate else {
return
}
del.tappedDark(self)
}
}
Protocol to pass the DarkView when it is tapped to a VC
protocol DarkViewDelegate : class {
func tappedDark(view:DarkView)
}
After thought
func textFieldDidEndEditing(textField: UITextField) {
guard let superV = view.superview else {
return
}
for subview in superV.subviews {
if subview is DarkView {
subview.removeFromSuperview()
}
}
}
Yes, you have the right idea. Add your UIView (let's call it darkeningView) to your storyboard and set it's background color to 50% opaque black. Position it where you want it and add constraints that position it there.
You can also attach a tap gesture recognizer to the darkeningView in IB and set up it's delegate. (You will probably need to set userInteractionEnabled = true on the darkeningView so that it responds to taps.
Add an IBOutlet to your view. In IB, set it to hidden = true.
In your code, when you activate the text field for editing, also set your darkeningView.hidden = false.

Resources