I need to be able to create and display a UIView at a a certain CGPoint.
So far I have a gesture recogniser that is added as a subview to the main view.
I then create a UIView programatically and set's it's x and y coordinates to the CGPoint that I got from the gesture recogniser.
I am able to create it and add it as a subview but the position of the created UIView is different from the location of the TAP.
AnimationView subclasses UIView
My code is below
tappedLocation = gesture.locationInView(self.view)
var animationImage: AnimationView = AnimationView()
animationImage.frame = CGRectMake(tappedLocation.x, tappedLocation.y, 64, 64)
animationImage.contentMode = UIViewContentMode.ScaleAspectFill
self.view.addSubview(animationImage)
animationImage.addFadeAnimation(removedOnCompletion: true)
Is there anything I am doing wrong?
Your problem is, that the you want that the center of the view is the point you clicked. At the moment the top left corner of your UIView will be the point you touched. So try that:
var frameSize:CGFloat = 64
animationImage.frame = CGRectMake(tappedLocation.x - frameSize/2, tappedLocation.y - frameSize/2, frameSize, frameSize)
As you see, Now you set the width and height before and adjust the x and y so that the center of your view is the point you touched.
But a better way is, like Rob mentioned in his answer to just set the center of the view to your location. That way you only have to set the size of your frame and use the CGSizeMake instead the CGRectMake method:
animationImage.frame.size = CGSizeMake(100, 100)
animationImage.center = tappedLocation
Just set its center:
animationImage.center = tappedLocation
Let's create a Tap Gesture and assign it to a View
let tapGesture = UITapGestureRecognizer()
tapGesture.addTarget(self, action: "tappedView:") // action is the call to the function that will be executed every time a Tap gesture gets recognised.
let myView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
myView.addGestureRecognizer(tapGesture)
Every time you tap a view with the assigned Tap Gesture, this function gets called.
func tappedView(sender: UITapGestureRecognizer) {
// Now you ca access all the UITapGestureRecognizer API and play with it however you want.
// You want to center your view to the location of the Tap.
myView.center = sender.view!.center
}
Related
I wanted to make an animation where a pulse is being created when you hit a button. But that doesn't really work on a scrollview because it turns out to be a specific point on the 'screen', not on the scrollview.
When you're scrolling down, the origin of the pulse stays the same.
#objc func addPulse() {
let pulse = Pulsing(numberOfPulses: 1, radius: 120, position: playButton.center)
pulse.animationDuration = 0.8
pulse.backgroundColor = UIColor.red.cgColor
self.view.layer.insertSublayer(pulse, below: playButton.layer)
The position has to be from the type CGPoint.
If you are using a ScrollView, better use the center of the scrollview instead of the center of the button as the button will disappear as you scroll. So when you create the Pulsing object , use the self.view.center as position.
#objc func addPulse() {
let pulse = Pulsing(numberOfPulses: 1, radius: 120, position: self.view.center)
pulse.animationDuration = 0.8
pulse.backgroundColor = UIColor.red.cgColor
self.view.layer.insertSublayer(pulse, below: playButton.layer)
}
By the little amount of context you gave, I am gonna go ahead and assume you have a button inside a UIScrollView and you need to add a pulse animation behind the button but the problem is that as the view scrolls and the button changes its position the animation keeps appearing at the same position. Also, I suppose you're inside an UIViewController class.
If I'm completely right with my guesses this is what I think it's happening:
The position of playButton will always be the same since it is its position against its parent, in this case, the UIScrollView. The absolute position would be the position of the item in the screen.
You are adding the animation inside of the UIView (of the UIViewController) instead of the UIScrollView itself. This will be a problem while scrolling.
My suggestion is for you to try:
#objc func addPulse() {
let pulse = Pulsing(numberOfPulses: 1, radius: 120, position: playButton.center)
pulse.animationDuration = 0.8
pulse.backgroundColor = UIColor.red.cgColor
scrollView.layer.insertSublayer(pulse, below: playButton.layer)
}
As I understand, UI buttons may only have rectangular or round-ish shapes. I have a set of images of different shapes (e.g. pants, t-shirt, beard etc.) and each of them should work as a button. How do I make UI Image to work as a button?
Second question: how do I make mentioned UI images to change color. I'm building an app where you can describe your look using paper doll kinda tool ( https://www.behance.net/gallery/49171187/ICEBREAKER-(work-in-progress) ). You tap an element > it calls a keyboard where each key represents a color > after you tap a key and choose a color the element you've tapped before changes it's color accordingly.
Thank you 🤓
In your .xcassets file, set your images to render as templates:
Use UIButtons and set the images as needed.
Set the tintColor property of the UIButton to the desired color.
UIImage doesn't inherit from UIControl like UIButton ( UIControl is a class that define the behavior of all visual elements that are used for handling action. )
A way for triggering an action when the user clicks on an UIImage, is to create a gesture ( like a pinch or a touch ) and adding it to the relative UIImageView.
func mySelector(gesture : UITapGestureRecognizer){
print(123)
}
override func viewDidLoad(){
let myImage = UIImage(named: "yourImage")
let myView = UIImageView(image: myImage)
myView.isUserInteractionEnabled = true
let touchGesture = UITapGestureRecognizer(target: self, action: #selector((mySelector)))
myView.addGestureRecognizer(touchGesture)
}
An important thing to remember is that by default, for UIImageView's instances, myView.isUserInteractionEnabled is set to false, so even if you are going to add a gesture recognizer, it will not be handled.
EDIT
I'm supposing that you already did the keyboard with every colors!
So when you click on the color, a selector is called.
If you change your myImage declaration of the answer above with
let myImage = UIImage(named:"yourImage")?.withRenderingMode(.alwaysTemplate), you can change your color easily, setting inside the called selector the tint color of the imageView :
myView.tintColor = selectedColor
That it is!
As I see the images are totally different and shapes are complex. If you want your custom shape (pants, beard etc.) to act like a button and to recognize touches not only in rectangular area there's a "hard way" but you can achieve the 100% result.
The "Hard Way" (Overkill)
You can subclass UIButton and create bezier paths for different shapes, in drawrect you will need to fill path with color and handle touches in hitTest method.
Here's an approximate algorithm of what you should do to achieve the result you want.
import UIKit
class CustomShapeButton: UIButton {
lazy var pantsShapeBezierPath: UIBezierPath = {
// Crate new path
let path = UIBezierPath()
// Set starting point for path
path.move(to: CGPoint)
// Add lines with:
path.addLine(to: CGPoint)
// Add curves with:
path.addCurve(to: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)
// Add arcs with:
path.addArc(withCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)
// Close path with (finalize path):
path.close()
return path
}()
override func draw(_ rect: CGRect) {
super.draw(rect)
// Set shape filling color
UIColor.red.setFill()
// Fill the shape
pantsShapeBezierPath.fill()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Handling touch events
if (pantsShapeBezierPath.contains(point)) {
return self
} else {
return nil
}
}
}
Please check out the documentation for UIButton:
https://developer.apple.com/reference/uikit/uibutton
and UIBezierPath:
https://developer.apple.com/reference/uikit/uibezierpath
The simple (best) way.
For simpler solution just use UIButton with its rectangular touch area. It's made for cases where user interaction takes place :]
drag an IBAction from UIButton in storyboard to your code
create a collectionView in in your viewController or in xib
set it's frame to bottom area when you want to see it:
var collectionHeight: CGFloat = 210.0
var frame = CGRect(x: CGFloat(0), y: CGFloat(self.view.bounds.size.height - collectionHeight), width: CGFloat(self.view.bounds.size.width), height: collectionHeight)
and set it hidden when you don't:
var frame = CGRect(x: CGFloat(0), y: CGFloat(self.view.bounds.size.height), width: CGFloat(self.view.bounds.size.width), height: collectionHeight)
animate frame change on button taps. Should be something like this:
func setCollectionViewHidden(hide: Bool) {
let collectionHeight = 210.0
let yPosition = hide ? self.view.bounds.size.height : self.view.bounds.size.height - 210
UIView.animate(withDuration: 0.33) {
var updatedFrame = CGRect(x: 0.0, y: yPosition, width: CGFloat(self.view.bounds.size.width), height: collectionHeight)
self.collectionView.frame = updatedFrame
}
}
set the UICollectionViewFlowLayout how you want, please refer to the documentation:
https://developer.apple.com/reference/uikit/uicollectionview
https://developer.apple.com/reference/uikit/uicollectionviewflowlayout
you can just set an array of color as the dataSource
implement the collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) delegate method you can make the button tintColor change.
That's an approximate algorithm of what you should do. Good luck, bro!
1. Making UI image work as button:
You can easily give UI Image or any other view the same capabilities as a button/control by adding a gesture recognizer to it
There is an example from Apple in their Start Developing iOS Apps (Swift) tutorial
Section Create a Gesture Recognizer
I am trying to get something like this to work. This is the Uber App. Where a user can swipe another view up, in front of a background view.
The background view is fairly simple, it has been done already. The view which will be swiped on top will be a UITableView. I want the user to be able to see just a little top part first when the app launches, then upon swiping a little it should stop in the middle and then after fully swiping up should take it all the way to the top, replacing the Background view.
Frameworks I have looked at are pullable view for iOS. But it is way too old and doesn't get any nice animations across. I have also looked at SWRevealViewController but I can't figure out how to swipe up from below.
I have also tried to use a button so when a user clicks on it, the table view controller appears modally, covering vertical, but that is not what I want. It needs to recognize a gesture.
Any help is greatly appreciated. Thanks.
I'm aware that the question is almost 2 and a half years old, but just in case someone finds this through a search engine:
I'd say that your best bet is to use UIViewPropertyAnimator. There's a great article about it here: http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
EDIT:
I managed to get a simple prototype working with UIViewPropertyAnimator, here's a GIF of it:
Here's the project on Github: https://github.com/Luigi123/UIViewPropertyAnimatorExample
Basically I have two UIViewControllers, the main one called ViewController and the secondary one called BottomSheetViewController. The secondary view has an UIPanGestureRecognizer to make it draggable, and inside the recognizer's callback function I do 3 things (after actually moving it):
① calculate how much percent of the screen have been dragged,
② trigger the animations in the secondary view itself,
③ notify the main view about the drag action so it can trigger it's animations. In this case I use a Notification, passing the percentage inside notification.userInfo.
I'm not sure how to convey ①, so as an example if the screen is 500 pixels tall and the user dragged the secondary view up to the 100th pixel, I calculate that the user dragged it 20% of the way up. This percentage is exactly what I need to pass into the fractionComplete property inside the UIViewPropertyAnimator instances.
⚠️ One thing to note is that I couldn't make it work with an actual navigation bar, so I used a "normal" view with a label in it's place.
I tried making the code smaller by removing some utility functions like checking if the user interaction is finished, but that means that the user can stop dragging in the middle of the screen and the app wouldn't react at all, so I really suggest you see the entire code in the github repo. But the good news is that the entire code that executes the animations fits in about 100 lines of code.
With that in mind, here's the code for the main screen, ViewController:
import UIKit
import MapKit
import NotificationCenter
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
#IBOutlet weak var blackView: UIView!
var animator: UIViewPropertyAnimator?
func createBottomView() {
guard let sub = storyboard!.instantiateViewController(withIdentifier: "BottomSheetViewController") as? BottomSheetViewController else { return }
self.addChild(sub)
self.view.addSubview(sub.view)
sub.didMove(toParent: self)
sub.view.frame = CGRect(x: 0, y: view.frame.maxY - 100, width: view.frame.width, height: view.frame.height)
}
func subViewGotPanned(_ percentage: Int) {
guard let propAnimator = animator else {
animator = UIViewPropertyAnimator(duration: 3, curve: .linear, animations: {
self.blackView.alpha = 1
self.someView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8).concatenating(CGAffineTransform(translationX: 0, y: -20))
})
animator?.startAnimation()
animator?.pauseAnimation()
return
}
propAnimator.fractionComplete = CGFloat(percentage) / 100
}
func receiveNotification(_ notification: Notification) {
guard let percentage = notification.userInfo?["percentage"] as? Int else { return }
subViewGotPanned(percentage)
}
override func viewDidLoad() {
super.viewDidLoad()
createBottomView()
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: receiveNotification(_:))
}
}
And the code for the secondary view (BottomSheetViewController):
import UIKit
import NotificationCenter
class BottomSheetViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var navBarView: UIView!
var panGestureRecognizer: UIPanGestureRecognizer?
var animator: UIViewPropertyAnimator?
override func viewDidLoad() {
gotPanned(0)
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(respondToPanGesture))
view.addGestureRecognizer(gestureRecognizer)
gestureRecognizer.delegate = self
panGestureRecognizer = gestureRecognizer
}
func gotPanned(_ percentage: Int) {
if animator == nil {
animator = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: {
let scaleTransform = CGAffineTransform(scaleX: 1, y: 5).concatenating(CGAffineTransform(translationX: 0, y: 240))
self.navBarView.transform = scaleTransform
self.navBarView.alpha = 0
})
animator?.isReversed = true
animator?.startAnimation()
animator?.pauseAnimation()
}
animator?.fractionComplete = CGFloat(percentage) / 100
}
// MARK: methods to make the view draggable
#objc func respondToPanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
moveToY(self.view.frame.minY + translation.y)
recognizer.setTranslation(.zero, in: self.view)
}
private func moveToY(_ position: CGFloat) {
view.frame = CGRect(x: 0, y: position, width: view.frame.width, height: view.frame.height)
let maxHeight = view.frame.height - 100
let percentage = Int(100 - ((position * 100) / maxHeight))
gotPanned(percentage)
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.post(name: name, object: nil, userInfo: ["percentage": percentage])
}
}
EDIT: So, some time has passed and now there is a really awesome library called Pulley. It does exactly what I wanted it to do, and its a breeze to setup!
Original answer:
Thanks to both Rikh and Tj3n for giving me hints. I managed to do something very basic, it doesn't have nice animations like Uber but it gets the job done.
With the following code, you can swipe any UIViewController. I use a UIPanGestureRecognizer on my image, which will stay on top of the dragged view at all times. Basically, you use that image and it recognizes where it gets dragged, and it sets the view's frame according to the user's input.
First go to your storyboard and add an identifier for the UIViewController that will be dragged.
Then in the MainViewController, use the following code:
class MainViewController: UIViewController {
// This image will be dragged up or down.
#IBOutlet var imageView: UIImageView!
// Gesture recognizer, will be added to image below.
var swipedOnImage = UIPanGestureRecognizer()
// This is the view controller that will be dragged with the image. In my case it's a UITableViewController.
var vc = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'm using a storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
// I have identified the view inside my storyboard.
vc = sb.instantiateViewController(withIdentifier: "TableVC")
// These values can be played around with, depending on how much you want the view to show up when it starts.
vc.view.frame = CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: -300)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
swipedOnImage = UIPanGestureRecognizer(target: self, action: #selector(self.swipedOnViewAction))
imageView.addGestureRecognizer(swipedOnImage)
imageView.isUserInteractionEnabled = true
}
// This function handles resizing of the tableview.
func swipedOnViewAction() {
let yLocationTouched = swipedOnImage.location(in: self.view).y
imageView.frame.origin.y = yLocationTouched
// These values can be played around with if required.
vc.view.frame = CGRect(x: 0, y: yLocationTouched, width: UIScreen.main.bounds.width, height: (UIScreen.main.bounds.height) - (yLocationTouched))
vc.view.frame.origin.y = yLocationTouched + 50
}
Final Product
Now, It is possible that my answer might not be the most efficient way of going at this, but I am new to iOS so this is the best I could come up with for the time being.
You can embed that table view inside a custom scroll view that will only handle touch when touch that table view part (override hittest), then drag it up (disable tableview scroll), till the upper part then disable scroll view and enable tableview scroll again
Or, you can just add the swipe gesture into your tableview and change it's frame along and disable swipe when it reach the top
Experiment with those and eventually you will achieve the effect you wanted
As Tj3n pointed out, you could use a UISwipeGesture to display the UITableView. So using constraints (instead of frames) heres how you could go about doing that:
Go to your UIViewController inside your Story board on which you wish to display the UITableView. Drag and drop the UITableView and add a leading, trailing and height to the UITableView. Now add a vertical constraint between the UIViewController and UITableView so that the UITableView appears below the UIViewController(Play around with this vertical value until you can display the top part of the UITableView to suit your need). Create outlets for the vertical spacing constraint and height constraint (in case you need to set a specific height that you can figure out at run time). On the swipe up just animatedly set the vertical constraint to be equal to the negative value of the height sort of like:
topSpaceToViewControllerConstraint.constant = -mainTableViewHeightConstraint.constant
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
};
Alternatively
If you want to be able to bring the UITableView up depending on the pan amount (i.e depending on how much the user has moved across the screen or how fast) you should use a UIPanGestureRecognizer instead and try and set frames instead of autoLayout for the UITableView (as I'm not a big fan of calling view.layoutIfNeeded repeatedly. I read somewhere that it is an expensive operation, would appreciate it if someone would confirm or correct this).
func handlePan(sender: UIPanGestureRecognizer) {
if sender.state == .Changed {
//update y origin value here based on the pan amount
}
}
Alternatively using UITableViewController
Doing what you wish to perform is also possible using a UITableViewController if you wish to but it involves a lot of faking and effort by creating a custom UINavigationControllerDelegate mainly to create a custom animation that will use UIPercentDrivenInteractiveTransition to pull the new UITableViewController up using a UIPanGestureRecognizer if you want it depending on the pan amount. Otherwise you can simply add a UISwipeGestureRecognizer to present the UITableViewController but you will still have to again create a custom animation to "fake" the effect you want.
I made more than one view without taking new Controller class , I used UIgestureRecognizer but it not look better.
I want to swipe same like ios device, when we start to swipe from left side corner.
You can implement this with scrollview.
Steps:
1)First place a scrollview that fills the full screen.
2)Next create a container view which is as big as to hold your two views.
let containerSize = CGSize(width: 640.0, height: 640.0)
containerView = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0),size:containerSize))
scrollView.addSubview(containerView)
3) Now you create your view 1 , view2 and add them to container view.
4) Set the scrollview content size to container size
scrollView.contentSize = containerSize;
5) Then set the scrollview delegate method
func viewForZoomingInScrollView(scrollView: UIScrollView!) -> UIView!{
return containerView
}
I need to be able to create and display a UIView at a a certain CGPoint.
So far I have a gesture recogniser that is added as a subview to the main view.
I then create a UIView programatically and set's it's x and y coordinates to the CGPoint that I got from the gesture recogniser.
I am able to create it and add it as a subview but the position of the created UIView is different from the location of the TAP.
AnimationView subclasses UIView
My code is below
tappedLocation = gesture.locationInView(self.view)
var animationImage: AnimationView = AnimationView()
animationImage.frame = CGRectMake(tappedLocation.x, tappedLocation.y, 64, 64)
animationImage.contentMode = UIViewContentMode.ScaleAspectFill
self.view.addSubview(animationImage)
animationImage.addFadeAnimation(removedOnCompletion: true)
Is there anything I am doing wrong?
Your problem is, that the you want that the center of the view is the point you clicked. At the moment the top left corner of your UIView will be the point you touched. So try that:
var frameSize:CGFloat = 64
animationImage.frame = CGRectMake(tappedLocation.x - frameSize/2, tappedLocation.y - frameSize/2, frameSize, frameSize)
As you see, Now you set the width and height before and adjust the x and y so that the center of your view is the point you touched.
But a better way is, like Rob mentioned in his answer to just set the center of the view to your location. That way you only have to set the size of your frame and use the CGSizeMake instead the CGRectMake method:
animationImage.frame.size = CGSizeMake(100, 100)
animationImage.center = tappedLocation
Just set its center:
animationImage.center = tappedLocation
Let's create a Tap Gesture and assign it to a View
let tapGesture = UITapGestureRecognizer()
tapGesture.addTarget(self, action: "tappedView:") // action is the call to the function that will be executed every time a Tap gesture gets recognised.
let myView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
myView.addGestureRecognizer(tapGesture)
Every time you tap a view with the assigned Tap Gesture, this function gets called.
func tappedView(sender: UITapGestureRecognizer) {
// Now you ca access all the UITapGestureRecognizer API and play with it however you want.
// You want to center your view to the location of the Tap.
myView.center = sender.view!.center
}