How to create a Collision Detection between a label and an Image - ios

INTRUDUCTION
I want to create a simple game where you should be able to drag a label and if you drag it in the correct place, you win. To be more specific: This is a game to help children with autism. In this game they have to create the correct sequence of numbers from one to ten dragging the label with the number in the correct place (which is an image actually). now you will understand better:
PROBLEM
I have already created the code to drag the labels (with a pan gesture recognizer) but I don't know how to create a Collision detection: When the card "1" is dragged and it collides with the blue image "1" something happens,
my mentally code is:
if LB_1 *collides with* IMG_1 {
self.IMG_1?.backgroundColor = UIColor.green
}
I hope the question it's clear.
Ah I don't use SpriteKit. I used "SingleViewApplication" as Template not "Game".

You want to use contains(). If you use intersects() then it will return true as soon as the two rectangles touch. With contains(), it won't return true until the dragged view is fully inside the target view, which seems much more intuitive.
I just wrote a sample app that implements this and it works perfectly.
I created a simple subclass of UIView I called BoxedView that just sets a border around it's layer so you can see it.
I set up a view controller with 2 boxed views, a larger "targetView", and a smaller view that the user could drag.
The target/action for my gesture recognizer moves the dragged view's frame as the user drags, and if target view contains the dragged view, it sets a Bool highlightTargetView, which causes the box around the target view to get thicker.
The entire view controller class' code looks like this:
class ViewController: UIViewController {
#IBOutlet weak var targetView: BoxedView!
var viewStartingFrame: CGRect = CGRect.zero
var highlightTargetView: Bool = false {
didSet {
targetView.layer.borderWidth = highlightTargetView ? 5 : 1
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func userDraggedView(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
viewStartingFrame = gesture.view?.frame ?? CGRect.zero
case .changed:
let offset = gesture.translation(in: view)
gesture.view?.frame = viewStartingFrame.offsetBy(dx: offset.x, dy: offset.y)
highlightTargetView = targetView.frame.contains(gesture.view?.frame ?? CGRect.zero)
case .ended:
gesture.view?.frame = viewStartingFrame
highlightTargetView = false
default:
break
}
}
}
In order for the math to work, I use the frame property of both views, which is in the coordinate system of the parent view (The parent view is the view controller's content view in this case, but the key thing is that we compare 2 rectangles using the same coordinate system for both.) If you used the bounds property of either view your math wouldn't work because bounds of a view is in the local coordinate system of that view.
Here's what that program looks like when running:
For comparison, I modified the program to also show what it looks like using the intersects() function, and created a video of what that looks like:

You can check if their frames intersect.
CGRect has method intersects.
So you're if statement should be the following:
if LB_1.frame.intersects(IMG_1.frame) {
self.IMG_1?.backgroundColor = UIColor.green
}
If that's not enough for you, you can calculate area of intersection rect.

Related

UIButton frame doesn't change if its panned

Here is an image of a simple APP I am trying to make
The App has a pan gesture recogniser for the centre purple button, the button when moved if it intersects with any of the 4 orange buttons, the purple button animates and moves inside the bounds of button it intersects otherwise it animates back to the initial starting position i.e centre.
For the first time when I pan the purple button the frame of the button updates, but If I try for the second time the frame of button remains the same (the value when it's at the centre of the view).
I am guessing this is something related to Auto Layout that I am missing, because if I remove the constrains on the centre purple button the frame updates every time correctly if I pan.
Can anybody explain what I have to keep in mind when animating with constraints
Here is my code for handling the Pan gesture:
#objc func handleCenterRectPan(_ pannedView: UIPanGestureRecognizer) {
let fallBackAnimation = UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.5) {
if self.button1.frame.intersects(self.centerRect.frame) {
self.centerRect.center = self.button1.center
} else if self.button2.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button2.center
} else if self.button3.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button3.center
} else if self.button4.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button4.center
} else {
// no intersection move to original position
self.centerRect.frame = OGValues.oGCenter
}
}
switch pannedView.state {
case .began:
self.centerRect.center = pannedView.location(in: self.view)
case .changed:
print("Changed")
self.centerRect.center = pannedView.location(in: self.view)
case .ended, .cancelled, .failed :
print("Ended")
fallBackAnimation.startAnimation();
//snapTheRectangle()
default:
break
}
}
First of all, you shouldn't be giving constraints if your aim is to move your views smoothly. And If you can't resist from constraints than you have to keep updating constraints and not the frames (centre),
In your code, I assume you have given your purple button centre constraints and then you are changing button's centre as user drag using the pan gesture. The problem is constraints you have given are still active and its try to set it back as soon as layout needs to update.
So what you can do is
Don't give constraints as its need to move freely when user drag
OR
Create IBoutlet for constraints and set .isActive to false when user dragging and make it true if it's not inside any of the other buttons and update UpdateLayoutIfNeeded, so it will come back to original position. (good approach)
#IBOutlet var horizontalConstraints: NSLayoutConstraint!
#IBOutlet var verticalConstraints: NSLayoutConstraint!
//make active false as the user is dragging the view
horizontalConstraints.isActive = false
verticalConstraints.isActive = false
self.view.layoutIfNeeded()
//make it true again if its not inside any of the other views
horizontalConstraints.isActive = true
verticalConstraints.isActive = true
self.view.layoutIfNeeded()
OR
Update constraints.constants for the horizontal and vertical constraints when user is moving the view (Not a good approach) and make it zero again if it's not inside of any other view
//make it true again if its not inside any of the other views
horizontalConstraints.constant = change in horizontal direction
verticalConstraints..constant = change in vertical direction
self.view.layoutIfNeeded()
Edit As you said constraints is nil after setting isActive = false because
what isActive actually do is add and remove those constraints as Apple doc says
Activating or deactivating the constraint calls addConstraint(:) and removeConstraint(:) on the view that is the closest common ancestor of the items managed by this constraint. Use this property instead of calling addConstraint(:) or removeConstraint(:) directly.
So making constraints strong reference does solve the issue but I think setting the strong reference to IBOutelet is not a good idea and adding and removing constraints programmatically is better. check out this question deallocating constraints

iOS Swift round views inside cell show incorrectly

Evening, I have a calendar collection.
The cells have some rounded views shown incorrectly at the first time, but when they are reloaded they are shown correctly.
I know that the issue is that at the first time the cell doesn't know the right size of the frame.
What I've tried:
1- call the round function inside layoutSubviews(): only the right side is rounded correctly
2 - call the round function inside the cellWillLayout: nothing changes
This is the rounding function:
func makeRound() {
print("rounding")
currentDayView.layer.cornerRadius = currentDayView.frame.height/2
currentDayView.layer.masksToBounds = true
currentDayView.clipsToBounds = true
selectedDayView.layer.cornerRadius = selectedDayView.frame.height/2
selectedDayView.layer.masksToBounds = true
selectedDayView.clipsToBounds = true
}
Any suggestion?
The best place to do corner rounding is either in each view's layoutSubviews or (for example, if you haven't sub-classed them) put it in your view controllers viewDidLayoutSubviews.
Each view in your case is the layoutSubviews of currentDayView and selectedDayView.
You need to override layoutSubviews method, then call your method inside it:
override func layoutSubviews() {
super.layoutSubviews()
self.makeRound()
}

IBInpectable properties for different states when customizing UIButton

I have customised UI button and create some properties with IBInspectable. However, I also need the same property for selected or highlighted state and can be inspected in Interface Builder. I want to know if it can be achieved?
Here is the customized button I created
#IBDesignable
class ImageLabelButton: UIButton{
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
let buttonImgView = UIImageView()
let buttonLabel = UILabel()
// let stackView = UIStackView()
// Override property observors
#IBInspectable
var textColor:UIColor? {
get {
return buttonLabel.textColor
}
set(newValue) {
self.buttonLabel.textColor = newValue
}
}
}
I want to create a IBInspectable property for other states as well. Can it be done? Thanks!
Short answer? No. Interface Builder cannot "process" code.
I have a need to know when my app is in portrait or landscape orientation (various slider controls are on the bottom or right depending on this).
Can I use IB for this? Not if I need to know on an iPad... it's size class is (wR hR) unless the slide out or split screen is there. I can "design" something for each orientation, but even Apple - WWDC'16, Making Apps Adaptive, Part 2 - ended up putting code into viewWillLayoutSubviews() for this.
Put a UIButton on your storyboard. Can you process a tap? Put a UISlider on the storyboard. Can you pan it left or right?
You are asking a design time tool to process run time actions.
So again, you can't make certain button states IBDesignable.

ScrollView - Gesture Recognizer - Swipe vertically

I have a UIScrollView that works when swiping left or right, however I've reduced the size of the scrollView so, now display area doesn't fully occupy the superview's frame, and swiping works only within the frame of the scroll view.
I would like to be able to scroll vertically even when swiping up and down outside the horizontal bounds of the narrowed scroll view.
It was recommended that I use a gesture recognizer, but that's beyond my current familiarity with iOS and could use more specific advice or a bit more guidance to get started with that.
There is a simpler approach then use a Gesture Recognizer =]
You can setup the superview of the scroll view (which is BIGGER...) to pass the touches to the scroll view. It's working M-A-G-I-C-A-L-Y =]
First, select the view that will pass all it's touches to the scroll view. if your parent view is already ok with that you may use it. otherwise you should consider add a new view in the size that you want that will catch touches.
Now create a new class (I'll use swift for the example)
class TestView: UIView {
#IBOutlet weak var Scroller: UIScrollView!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if (view == self) {
return Scroller
}
return view
}
}
Nice! now as you can see we added an outlet of the scroller. so use interface builder, select the new view and set it's class to "TestView" in the identity inspector (Or to the name that you'll use for your custom class).
After you set the class and your view is still selected go to connections inspector and connect "Scroller" to your scroll view on the storyboard. All connected properly =]
That's it!! no gesture recognizer needed!!
The new view will pass all it's touches to the scroll view and it'll behave just like you pan in it =]
In my answer I used that answer
EDIT: I improved the code now, it wasn't working as expected before, now it catches only when in needs and not every touch in the app as before
Search for a component called SwipeGestureRecognizer :
Grab it and drop it on top of the View (use the hierarchy to make sure
you drop it on it, if you drop it on another element this code will not work):
Select one of the SwipeGestureRecognizer in the hierarchy and go to its attribute page. Change Swipe to Right.
Make sure the other recogniser has the Swipe attribute to Left
Select UIScrollView and uncheck Scrolling enabled
Connect detectSwipe() (see source code below) to both recognizers.
--
#IBAction func detectSwipe (_ sender: UISwipeGestureRecognizer) {
if (currentPage < MAX_PAGE && sender.direction == UISwipeGestureRecognizerDirection.left) {
moveScrollView(direction: 1)
}
if (currentPage > MIN_PAGE && sender.direction == UISwipeGestureRecognizerDirection.right) {
moveScrollView(direction: -1)
}
}
func moveScrollView(direction: Int) {
currentPage = currentPage + direction
let point: CGPoint = CGPoint(x: scrollView.frame.size.width * CGFloat(currentPage), y: 0.0)
scrollView.setContentOffset(point, animated: true)
// Create a animation to increase the actual icon on screen
UIView.animate(withDuration: 0.4) {
self.images[self.currentPage].transform = CGAffineTransform.init(scaleX: 1.4, y: 1.4)
for x in 0 ..< self.images.count {
if (x != self.currentPage) {
self.images[x].transform = CGAffineTransform.identity
}
}
}
}
Refer to https://github.com/alxsnchez/scrollViewSwipeGestureRecognizer for more
I don't have time for detailed answer but:
In storyboard drag a pan gesture recognizer on the scroll view's superview... Connect it's action with your view controller and in this action change the scroll view position by using the properties from the gesture recognizer that you got as parameter
Tip: when connecting the action change parameter type from Any to UIPanGestureRecognizer in the combo box
please don't see this answer as recommendation to use this approach in your problem, I don't know if that's the best way, I'm just helping you to try it

Insert sublayer behind UIImageView

i'm trying to add a sublayer behind the imageView however the issue is that since it is using constraints it can't seem to figure out the position and just places sublayer in left corner? i've tried to add the LFTPulseAnimation to viewDidLayoutSubViews but then everytime i reopen the app it will add one on top.
viewDidLoad
//GroupProfile ImageView
imageGroupProfile = UIImageView(frame: CGRect.zero)
imageGroupProfile.backgroundColor = UIColor.white
imageGroupProfile.clipsToBounds = true
imageGroupProfile.layer.cornerRadius = 50
self.view.addSubview(imageGroupProfile)
imageGroupProfile.snp.makeConstraints { (make) -> Void in
make.height.equalTo(100)
make.width.equalTo(100)
make.centerX.equalTo(self.view.snp.centerX)
make.centerY.equalTo(self.view.snp.centerY).offset(-40)
}
let pulseEffect = LFTPulseAnimation(repeatCount: Float.infinity, radius:160, position:imageGroupProfile.center)
self.view.layer.insertSublayer(pulseEffect, below: imageGroupProfile.layer)
i've tried to add the LFTPulseAnimation to viewDidLayoutSubViews but then everytime i reopen the app it will add one on top.
Nevertheless that is the way to do it. Just add a Bool property so that your implementation of viewDidLayoutSubViews inserts the layer only once:
var didLayout = false
override func viewDidLayoutSubviews() {
if !didLayout {
didLayout = true
// lay out that layer here
}
}
The reason is that you don't have the needed dimensions until after viewDidLayoutSubviews tells you that (wait for it) your view has been laid out! But, as you rightly say, it can be called many times subsequently, so you also add the condition so that your code runs just once, namely the first time viewDidLayoutSubviews is called.

Resources