I've got a ViewController with three subviews. I'm trying to get them to detect touches in their bounds from a starting point outside their bounds without the user lifting their finger (ie the user dragging into the view). I thought hitTest would do this but it only works for separate taps. I assume this is probably passing a gesture through instead but I've not found out how to implement this.
class SuperViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
self.view = TestView()
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
}
}
Which produces this
And then I've subclassed UIView for the SuperViewController's view.
class TestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.isUserInteractionEnabled, !isHidden, alpha > 0.01 else {return nil}
if self.point(inside: point, with: event) {
for subview in subviews.reversed() {
let hitView = subview.hitTest(point, with: event)
if hitView != nil {
hitView?.backgroundColor = .red
return hitView
}
}
return self
}
return nil
}
}
So each one turns red when the user taps. But ideally I want them to each respond with one drag from the top left corner of the screen to the other.
You can accomplish this with a UIPanGestureRecognizer.
Here's an example below:
class ViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
self.view.addGestureRecognizer(gestureRecognizer)
}
#objc
private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let view = gestureRecognizer.view else {
return
}
let translation = gestureRecognizer.translation(in: view)
for subview in view.subviews.reversed() {
if let hitView = subview.hitTest(translation, with: nil) {
hitView.backgroundColor = .red
return
}
}
}
}
Related
I'm using gestures to zoom and move a UIImageView (like Instagram zoom, for example). When the gesture ends, I want to restore UIImageView initial position, but I cannot get a copy of the initial center because it's a reference type. Using:
let prevCenter = myImage.center
is of course useless. How can I copy it?
On zoom and move apply transform to view. On the end just set .identity value.
Edit
Example:
#IBOutlet weak var butt: UIButton!
var offsetTransform: CGAffineTransform = .identity
var zoomTransform: CGAffineTransform = .identity
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(pan:)))
pan.minimumNumberOfTouches = 2
pan.delegate = self
view.addGestureRecognizer(pan)
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(pinch:)))
pinch.delegate = self
view.addGestureRecognizer(pinch)
}
#objc func onPinch(pinch: UIPinchGestureRecognizer) {
let s = pinch.scale
zoomTransform = CGAffineTransform(scaleX: s, y: s)
butt.transform = offsetTransform.concatenating(zoomTransform)
if (pinch.state == .ended) {
finish()
}
}
#objc func onPan(pan: UIPanGestureRecognizer) {
let t = pan.translation(in: view)
offsetTransform = CGAffineTransform(translationX: t.x, y: t.y)
}
func updatePos() {
butt.transform = offsetTransform.concatenating(zoomTransform)
}
func finish() {
butt.transform = .identity
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Are you sure that you are not updating the prevCenter somewhere else?
center is struct so you shouldn't have problems copying it.
Just make sure to take it (the center) when the view is in original state, before starting to zoom it and after it is drawn with correct frame
let imageView = UIImageView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))
let prevCenter = view.center // 20, 20
imageView.center = CGPoint(x: 50, y: 50)
// imageView.center - 50, 50
// prevCenter - 20, 20
imageView.frame = CGRect(x: 40, y: 40, width: 10, height: 10)
// imageView.center - 45, 45
// prevCenter - 20, 20
I've been trying to be able to tap a UIImage as it animates to the top of my screen and print("Image Tapped"), yet to no success.
override func viewDidLoad() {
super.viewDidLoad()
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
UIView.animate(withDuration: 5, delay: 0, options: UIImageView.AnimationOptions.allowUserInteraction, animations: {
self.redBalloon.frame = CGRect(x: Int(self.xEnding), y: -192, width: 166, height: 192)
}, completion: {(finished:Bool) in
self.endGame()
})
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
}
#objc func imageTapped(_ sender: UITapGestureRecognizer) {
// do something when image tapped
print("image tapped")
}
The problem is that the image view is not in the spot where you see it during animation (it's at the endpoint of the animation). So you are not tapping on the image view at the point where it is, and thus the tap is not detected.
Therefore, either you must hit-test the presentation layer or, if you don't want to do that, you must use a UIViewPropertyAnimator instead of calling UIView.animate.
As an example of the first approach, I'll subclass UIImageView. Make your UIImageView an instance of this subclass:
class TouchableImageView : UIImageView {
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
let pres = self.layer.presentation()!
let suppt = self.convert(point, to: self.superview!)
let prespt = self.superview!.layer.convert(suppt, to: pres)
return super.hitTest(prespt, with: e)
}
}
However, personally I think it's a lot simpler to use UIViewPropertyAnimator. In that case, do not make your UIImageView a TouchableImageView! You don't want to do extra hit-test munging. Just let the property animator do all the work:
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
let anim = UIViewPropertyAnimator(duration: 5, timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
anim.addAnimations {
self.redBalloon.frame = CGRect(x: Int(xEnding), y: -192, width: 166, height: 192)
}
anim.addCompletion { _ in
self.endGame()
}
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
anim.startAnimation()
I would like to get the rectangular position in the scene view of a tapped node so that I can use that position to animate another UIView from that position to a bigger size (exactly like in the Measure app):
I get the tapped node with this code, though how do I get the rect or the dimensions or whatever to make the smooth transition from the node to the view?
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
if (touch.view == self.sceneView) {
let viewTouchLocation:CGPoint = touch.location(in: sceneView)
guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
return
}
}
}
I've looked into using hitTest or smartHitTest on the scene view given the touch position but I can't really seem to be able to get the size/position of the smaller view.
Not exactly your answer but try it out: (edited to send it back)
var front = false
var boxView = UIView()
#objc func tap(_ sender: UITapGestureRecognizer){
if !front{
let position = sender.location(in: sceneView)
let hitTest = sceneView.hitTest(position, options: nil)
if let hitNode = hitTest.first?.node{
self.boxView = UIView(frame: CGRect(x: position.x, y: position.y, width: 100, height: 40))
self.boxView.backgroundColor = .red
sceneView.addSubview(self.boxView)
UIView.animate(withDuration: 0.2) {
self.boxView.frame = CGRect(x: self.sceneView.frame.width/2 - 150, y: self.sceneView.frame.height/2 - 100, width: 300, height: 200)
}
front = true
}
}else{
let p = plane.convertPosition(plane.position, to: sceneView.scene.rootNode)
print(p, plane.position) // see difference between p and plane.position
let projectedPoint = sceneView.projectPoint(p)
let point = CGPoint(x: CGFloat(projectedPoint.x), y: CGFloat(projectedPoint.y))
UIView.animate(withDuration: 0.2, animations: {
self.boxView.frame = CGRect(x: point.x, y: point.y, width: 100, height: 40)
}) { (completed) in
self.boxView.removeFromSuperview()
}
front = false
}
}
plane here is node in my test case. You might want to look about convertPosition and projectPoint
I am using this code in Xcode 9, iOS 11.
var boolName: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
if boolName == true {
self.designTextField()
self.view.layoutIfNeeded()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if boolName == false {
self.designTextField()
boolName = true
}
self.view.layoutIfNeeded()
}
func designTextField() {
//Set the horizontal line in bottom of text field
nameLayer.frame = CGRect(x: 0, y: self.tfName.bounds.size.height, width: self.tfName.bounds.size.width, height: 1)
nameLayer.backgroundColor = UIColor.lightGray.cgColor
tfName.layer.addSublayer(nameLayer)
//Set the horizontal line in bottom of text field
phoneLayer.frame = CGRect(x: 0, y: self.tfPhone.bounds.size.height, width: self.tfPhone.bounds.size.width, height: 1)
phoneLayer.backgroundColor = UIColor.lightGray.cgColor
tfPhone.layer.addSublayer(phoneLayer)
//Set the horizontal line in bottom of text field
emailLayer.frame = CGRect(x: 0, y: self.tfEmail.bounds.size.height, width: self.tfEmail.bounds.size.width, height: 1)
emailLayer.backgroundColor = UIColor.lightGray.cgColor
tfEmail.layer.addSublayer(emailLayer)
}
When using that code and testing in iPad. And the issue is when i rotate the iPad in Landscape Mode, then the horizontal line of text fields(in bottom) are conflicting.
Can somebody plzz help ?
Case if boolName == false stops the redrawing on landscape mode, remove it. And in method designTextField, set the line layers' names, then you are able to get and update them.
let kLineName = "nameLine"
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.designTextField()
}
func designTextField()
{
//Set the horizontal line in bottom of text field
if let index = tfName.layer.sublayers?.index(where: { (layer) -> Bool in
return layer.name == kLineName
}) {
//Update line's frame if it's existed.
let nameLayer = tfName.layer.sublayers![index]
nameLayer.frame = CGRect(x: 0, y: self.tfName.bounds.size.height, width: self.tfName.bounds.size.width, height: 1)
}
else{
//Add layer if it's not existed.
nameLayer.frame = CGRect(x: 0, y: self.tfName.bounds.size.height, width: self.tfName.bounds.size.width, height: 1)
nameLayer.backgroundColor = UIColor.lightGray.cgColor
nameLayer.name = kLineName
tfName.layer.addSublayer(nameLayer)
}
//Same to the phoneLayer and emailLayer ......
}
I am trying to create a menu that uses scrollviews to swipe between 5 view controllers that are shaped like a T, my problem is that currently my viewcontrollers are shaped like a + sign. I was wondering based on the code below how I could set the left and right view controllers to be aligned to form a T shape instead of a + shape. Just to clarify, the code works as such: The snapcontainerviewcontroller sets up a vertical scrollvew view with three view controllers, top bottom and middle, and then this scrollview view is sandwiched in a horizontal scroll view that contains a left and a right viewcontroller.
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
// Override point for customization after application launch.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let left = storyboard.instantiateViewController(withIdentifier: "left")
let middle = storyboard.instantiateViewController(withIdentifier: "middle")
let right = storyboard.instantiateViewController(withIdentifier: "right")
let top = storyboard.instantiateViewController(withIdentifier: "top")
let bottom = storyboard.instantiateViewController(withIdentifier: "bottom")
let snapContainer = SnapContainerViewController.containerViewWith(left,
middleVC: middle,
rightVC: right,
topVC: top,
bottomVC: bottom)
self.window?.rootViewController = snapContainer
self.window?.makeKeyAndVisible()
return true
}`
SnapContainerViewController:
class SnapContainerViewController: UIViewController, UIScrollViewDelegate,UIGestureRecognizerDelegate {
var topVc: UIViewController?
var leftVc: UIViewController!
var middleVc: UIViewController!
var rightVc: UIViewController!
var bottomVc: UIViewController?
var directionLockDisabled: Bool!
var horizontalViews = [UIViewController]()
var veritcalViews = [UIViewController]()
var initialContentOffset = CGPoint() // scrollView initial offset
var middleVertScrollVc: VerticalScrollViewController!
var scrollView: UIScrollView!
var delegate: SnapContainerViewControllerDelegate?
let player = MPMusicPlayerController.applicationMusicPlayer()
class func containerViewWith(_ leftVC: UIViewController,
middleVC: UIViewController,
rightVC: UIViewController,
topVC: UIViewController?=nil,
bottomVC: UIViewController?=nil,
directionLockDisabled: Bool?=false) -> SnapContainerViewController {
let container = SnapContainerViewController()
container.directionLockDisabled = directionLockDisabled
container.topVc = topVC
container.leftVc = leftVC
container.middleVc = middleVC
container.rightVc = rightVC
container.bottomVc = bottomVC
return container
}
override func viewDidLoad() {
super.viewDidLoad()
setupVerticalScrollView()
setupHorizontalScrollView()
scrollView.delaysContentTouches = false
scrollView.bounces = false
//scrollView.canCancelContentTouches = false
scrollView.isPagingEnabled = true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func setupVerticalScrollView() {
middleVertScrollVc = VerticalScrollViewController.verticalScrollVcWith(topVc: topVc,
middleVc: middleVc,
bottomVc: bottomVc)
delegate = middleVertScrollVc
}
func setupHorizontalScrollView() {
scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.bounces = false
//self.view.bounds.origin.x
let view = (
x: CGFloat(0) ,
y: CGFloat(0),
width: self.view.bounds.width,
height: self.view.bounds.height
)
scrollView.frame = CGRect(x: view.x,
y: view.y,
width: view.width,
height: view.height
)
self.view.addSubview(scrollView)
let scrollWidth = 3 * view.width
let scrollHeight = view.height
scrollView.contentSize = CGSize(width: scrollWidth, height: scrollHeight)
leftVc.view.frame = CGRect(x: 0,
y: 0,
width: view.width,
height: view.height
)
middleVertScrollVc.view.frame = CGRect(x: view.width,
y: 0,
width: view.width,
height: view.height
)
rightVc.view.frame = CGRect(x: 2 * view.width,
y: 0,
width: view.width,
height: view.height
)
addChildViewController(leftVc)
addChildViewController(middleVertScrollVc)
addChildViewController(rightVc)
scrollView.addSubview(leftVc.view)
scrollView.addSubview(middleVertScrollVc.view)
scrollView.addSubview(rightVc.view)
leftVc.didMove(toParentViewController: self)
middleVertScrollVc.didMove(toParentViewController: self)
rightVc.didMove(toParentViewController: self)
scrollView.contentOffset.x = middleVertScrollVc.view.frame.origin.x
//scrollView.contentOffset.y = (topVc?.view.frame.origin.y)!
scrollView.delegate = self
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.initialContentOffset = scrollView.contentOffset
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if delegate != nil && !delegate!.outerScrollViewShouldScroll() && !directionLockDisabled {
let newOffset = CGPoint(x: self.initialContentOffset.x, y: self.initialContentOffset.y)
// Setting the new offset to the scrollView makes it behave like a proper
// directional lock, that allows you to scroll in only one direction at any given time
self.scrollView!.setContentOffset(newOffset, animated: false)
}
}
}
VerticalScrollViewController:
class VerticalScrollViewController: UIViewController, SnapContainerViewControllerDelegate {
var topVc: UIViewController!
var middleVc: UIViewController!
var bottomVc: UIViewController!
var scrollView: UIScrollView!
class func verticalScrollVcWith(topVc: UIViewController?=nil, middleVc: UIViewController,bottomVc: UIViewController?=nil) -> VerticalScrollViewController {
let middleScrollVc = VerticalScrollViewController()
middleScrollVc.topVc = topVc
middleScrollVc.middleVc = middleVc
middleScrollVc.bottomVc = bottomVc
return middleScrollVc
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view:
setupScrollView()
scrollView.isPagingEnabled = true
}
func setupScrollView() {
scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.showsVerticalScrollIndicator = false
scrollView.bounces = false
//scrollView.isScrollEnabled = false
let view = (
x: CGFloat(0),
y: CGFloat(0),
width: self.view.bounds.width,
height: self.view.bounds.height
)
scrollView.frame = CGRect(x: view.x, y: view.y, width: view.width, height: view.height)
self.view.addSubview(scrollView)
let scrollWidth: CGFloat = view.width
var scrollHeight: CGFloat
scrollHeight = 3 * view.height
topVc.view.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height)
middleVc.view.frame = CGRect(x: 0, y: view.height, width: view.width, height: view.height)
bottomVc.view.frame = CGRect(x: 0, y: 2 * view.height, width: view.width, height: view.height)
addChildViewController(topVc)
addChildViewController(middleVc)
addChildViewController(bottomVc)
scrollView.addSubview(topVc.view)
scrollView.addSubview(middleVc.view)
scrollView.addSubview(bottomVc.view)
topVc.didMove(toParentViewController: self)
middleVc.didMove(toParentViewController: self)
bottomVc.didMove(toParentViewController: self)
print("1st case!")
//scrollView.contentOffset.y = middleVc.view.frame.origin.y
scrollView.contentOffset.y = topVc.view.frame.origin.y
scrollView.contentSize = CGSize(width: scrollWidth, height: scrollHeight)
scrollView.delaysContentTouches = false
//scrollView.canCancelContentTouches = false
}
// MARK: - SnapContainerViewControllerDelegate Methods
/**
*/
func outerScrollViewShouldScroll() -> Bool {
if scrollView.contentOffset.y < middleVc.view.frame.origin.y || scrollView.contentOffset.y > middleVc.view.frame.origin.y {
return false
} else {
return true
}
}
}
You're outerScrollViewShouldScroll method is not correct. You're only letting it scroll when the contentOffset is equal to the middleVc. You should change to allow scrolling only when contentOffset.y is equal to topVc's origin.y
Replace
if scrollView.contentOffset.y < middleVc.view.frame.origin.y || scrollView.contentOffset.y > middleVc.view.frame.origin.y {
return false
} else {
return true
}
With this:
return scrollView.contentOffset.y == topVc.view.frame.origin.y