I have seen several answers about adding a GestureRecognzier to subViews but my problem is that I don't have the subView's frame available beforehand. I am drawing a CGPath and at the Touches Ended method, I want to create a new subView with frame equal to CGPath bounding box. After that, I want to drag that subView with PanGestureRecognizer. I am trying to implement Evernote crop functionality where the user selects a certain area of view and move it to other position. Is this the right approach to that solution?
Not quite understand the relationship between UIGestureRecognizer and .frame. you can simply add UIGestureRecognizer to object, once its init work finished.
Try to add gesture in TouchEnd method directly after drawing subview.
import UIKit
class GestureResearchVC: UIViewController{
var subViewByCGPath: UIView?
override func viewDidLoad() {
super.viewDidLoad()
}
func createSubView(){
//creat subview
subViewByCGPath = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
subViewByCGPath?.backgroundColor = UIColor.yellow
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 50,y: 50), radius: CGFloat(20), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
subViewByCGPath?.layer.addSublayer(shapeLayer)
self.view.addSubview(subViewByCGPath!)
//add pan gesture to subViewByCGPath
let gesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(rec:)))
subViewByCGPath?.addGestureRecognizer(gesture)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if subViewByCGPath == nil {
print("touch end once")
createSubView()
}else{
print("touch end repeatedly")
}
}
func panGestureAction(rec: UIPanGestureRecognizer){
print("pannnnnnn~")
let transPoint = rec.translation(in: self.view)
let x = rec.view!.center.x + transPoint.x
let y = rec.view!.center.y + transPoint.y
rec.view!.center = CGPoint(x: x, y: y)
rec.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
//distinguish state
switch rec.state {
case .began:
print("began")
case .changed:
print("changed")
case .ended:
print("ended")
default:
print("???")
}
}
}
Related
I'm looking to implement a custom UITabBarController in Swift. I have searched if there is a library or something that can be used to implement exactly same design, but couldn't find any. Most of the answers or libraries are outdated (after some changes that Apple made on NavigationBar and TabBar on iOS 15), or are not doing the exact same functionality as needed.
So, the TabBar should be with a rounded button in the center, between the button and TabBar should be some transparent space (padding). When that button is tapped it will popup a view.
The design can be seen on the image below.
Similar logic and UX has Binance app. So, when the button in the center is tapped, it popups a view to navigate to another View Controllers.
My Implementation
One of my implementations that worked best is the following one.
I have two classes CustomTabBarController and CustomTabBar.
CustomTabBarController
import UIKit
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
setupMiddleButton()
}
// TabBarButton – Setup Middle Button
func setupMiddleButton() {
let middleBtn = UIButton(frame: CGRect(x: (self.view.bounds.width / 2)-25, y: -20, width: 50, height: 50))
//STYLE THE BUTTON YOUR OWN WAY
middleBtn.backgroundColor = .blue
middleBtn.layer.cornerRadius = (middleBtn.layer.frame.width / 2)
//add to the tabbar and add click event
self.tabBar.addSubview(middleBtn)
middleBtn.addTarget(self, action: #selector(self.menuButtonAction), for: .touchUpInside)
self.view.layoutIfNeeded()
}
// Menu Button Touch Action
#objc func menuButtonAction(sender: UIButton) {
self.selectedIndex = 2 //to select the middle tab. use "1" if you have only 3 tabs.
print("MenuButton")
}
}
CustomTabBar
import UIKit
#IBDesignable
class CustomTabBar: UITabBar {
private var shapeLayer: CALayer?
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1.0
//The below 4 lines are for shadow above the bar. you can skip them if you do not want a shadow
shapeLayer.shadowOffset = CGSize(width:0, height:0)
shapeLayer.shadowRadius = 10
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.3
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
override func draw(_ rect: CGRect) {
self.addShape()
}
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return nil
}
}
Results
The results are not quite as expected.
It's not creating a transparency on the curve in center. It's taking background color or sometimes adding just white background. This can be noticed mostly on TableView or any other scrolling UI
As for popup view when Floating Button is pressed I would like to hear any suggestion from you. What would be the best option for that?
Thank you in advance for your contribution.
What you're seeing is the visual effect backdrop.
If you're still experiencing the issue, this should fix the problem...
In your CustomTabBarController:
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
setupMiddleButton()
// add these two lines
self.tabBar.backgroundImage = UIImage()
self.tabBar.shadowImage = UIImage()
}
Your are just missing couple of properties
1. set extendedLayoutIncludesOpaqueBars to true to each of your vc
class FirVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// set extendedLayoutIncludesOpaqueBars to true to each of your vc
self.extendedLayoutIncludesOpaqueBars = true
}
}
2. set BG color of your custom tab bar to clear
override init(frame: CGRect) {
super.init(frame: frame)
// set BG color to clear
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
super.init(coder: coder)
// set BG color to clear
self.backgroundColor = .clear
}
I would like to know how to draw a line from one point to another where the user touch the first point and the next point he touches, it create a line in between. I know that using UIBezierPath will be useful in here but I am still new to Swift and iOS platform so any guidance will helpful.
I have created a function called drawLineFromPoint but does not work for me.
import UIKit
import GLKit
class ViewController: GLKViewController {
private var context: EAGLContext?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
EAGLcontext()
// Gesture Code
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: view)
var dot = UIView(frame: CGRect(x: position.x, y: position.y, width: 10, height: 10))
dot.backgroundColor = .red
view.addSubview(dot)
// View the x and y coordinates
print(position)
}
}
//Not sure how to do this part ???
func drawLineFromPoint(start : CGPoint, toPoint end:CGPoint, ofColor lineColor: UIColor, inView view:UIView) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 1.0
view.layer.addSublayer(shapeLayer)
}
//Create EAGL Context for the GLKView
private func EAGLcontext() {
context = EAGLContext(api: .openGLES2)
EAGLContext.setCurrent(context)
if let view = self.view as? GLKView, let context = context {
view.context = context
delegate = self
}
}
override func glkView(_ view: GLKView, drawIn rect: CGRect) {
//Set the color
glClearColor(0.85, 0.85, 0.85, 1.0)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
}
}
extension ViewController: GLKViewControllerDelegate {
func glkViewControllerUpdate(_ controller: GLKViewController) {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Based On your code I changed some things and this code Works please check.
class ViewController: UIViewController {
var lastPosition: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Gesture Code
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: view)
if let lastPosition = self.lastPosition {
self.drawLineFromPoint(start: lastPosition, toPoint: position, ofColor: UIColor.red, inView: self.view)
}
self.lastPosition = position
// View the x and y coordinates
let dot = UIView(frame: CGRect(x: position.x, y: position.y, width: 10, height: 10))
dot.backgroundColor = .red
view.addSubview(dot)
print(position)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let lastPosition = self.lastPosition{
let position = touch.location(in: view)
self.drawLineFromPoint(start: lastPosition, toPoint: position, ofColor: UIColor.red, inView: self.view)
self.lastPosition = position
let dot = UIView(frame: CGRect(x: position.x, y: position.y, width: 10, height: 10))
dot.backgroundColor = .red
view.addSubview(dot)
}
}
//Not sure how to do this part ???
func drawLineFromPoint(start : CGPoint, toPoint end:CGPoint, ofColor lineColor: UIColor, inView view:UIView) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 1.0
view.layer.addSublayer(shapeLayer)
}
}
Currently in my app when user tap on the screen a red dot will appear at wherever the user touch if a user tap on the screen multiple times there will be multiple dot. I want to add a feature to allow user to change the colour.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let location = (touch as! UITouch).location(in: self.imageView)
let layer = CAShapeLayer()
layer.path = UIBezierPath(roundedRect: CGRect(x: location.x, y: location.y, width: 2, height: 2), cornerRadius: 50).cgPath
layer.fillColor = UIColor.red.cgColor
imageView.layer.addSublayer(layer)
}
}
When I tried the following method I am able to change the colour but for some reason no matter how many times I've tap on the screen there will only be one dot. My guess is that every time I tap on my screen a new layer will be created and it will replace the old layer. is there anyway I can allow user to change the colour of the layer while keeping all of dots that is already on the layer?
class ViewController: UIViewController,UIScrollViewDelegate {
let layer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let location = (touch as! UITouch).location(in: self.imageView)
layer.path = UIBezierPath(roundedRect: CGRect(x: location.x, y: location.y, width: 2, height: 2), cornerRadius: 50).cgPath
layer.fillColor = UIColor.red.cgColor
imageView.layer.addSublayer(layer)
}
}
#IBAction func changecolorBtn(_ sender: Any) {
layer.fillColor = UIColor.green.cgColor
}
}
Because of you used only one layer
let layer = CAShapeLayer()
and whenever you set
layer.path = UIBezierPath(roundedRect: CGRect(x: location.x, y: location.y, width: 2, height: 2), cornerRadius: 50).cgPath
It will remove the old path, and your new path has only one point.
That's why
When I tried the following method I am able to change the colour but for some reason no matter how many times I've tap on the screen there will only be one dot.
You can simple fix you issue by combine all your dot path. Like this
// add the combine path as your global variable
let layer = CAShapeLayer()
let combinePath = CGMutablePath()
and change your add path code
layer.path = UIBezierPath(roundedRect: CGRect(x: location.x, y: location.y, width: 2, height: 2), cornerRadius: 50).cgPath
to add the combine path
let path = UIBezierPath(roundedRect: CGRect(x: location.x, y: location.y, width: 2, height: 2), cornerRadius: 50).cgPath
combinePath.addPath(path)
layer.path = combinePath
I need to have a view with borders, and only borders should be clickable.
Here is my code which creates custom view, but when I added gesture all view becomes clickable.
class RectangleView: UIView {
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x:0, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y:rect.height))
aPath.addLine(to: CGPoint(x:0, y:rect.height))
aPath.close()
UIColor.green.setStroke()
aPath.stroke()
UIColor.clear.setFill()
aPath.fill()
}
}
Edited, here is the screenshot from other app
so I will click on view under my top view, this one will be in front and will be clickable, so I cant add smaller subView
The most correct way here is to override func point(inside point: CGPoint, with event: UIEvent?) -> Bool for UIView subclass.
Here is example:
class CustomView: UIView {
private let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupShapeLayer()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupShapeLayer() {
//Draw your own shape here
shapeLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let innerPath = UIBezierPath(arcCenter: center, radius: frame.width / 3, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
let outerPath = UIBezierPath(arcCenter: center, radius: frame.width / 4, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
//Subtract inner path
innerPath.append(outerPath.reversing())
shapeLayer.path = innerPath.cgPath
shapeLayer.lineWidth = 10
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.yellow.cgColor
layer.addSublayer(shapeLayer)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return shapeLayer.path?.contains(point) ?? false
}
}
Now you can add UITapGestureRecognizer to check how it works:
class ViewController: UIViewController {
#IBOutlet weak var resultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let customView = CustomView.init(frame: view.frame)
view.addSubview(customView)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction))
view.addGestureRecognizer(recognizer)
}
#objc func tapAction(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: sender.view)
let subview = view?.hitTest(location, with: nil)
if subview is CustomView {
resultLabel.text = "CustomView"
}
else {
resultLabel.text = "Other"
}
}
}
Result of tapAction:
If you need several views, there are two possible options:
Store CAShapeLayer inside one CustomView and iterate through it inside point function
Add CustomView for each instance and iterate it through inside UITapGestureRecognizer tap action
I'm trying to create a drawing app that sits on top of a UIImagePickerController after the picture is taken. The touch delegate functions are called correctly but do not leave any traces on the view as they are supposed to. What I have so far:
func createImageOptionsView() { //creates the view above the picker controller overlay view
let imgOptsView = UIView()
let scale = CGAffineTransform(scaleX: 4.0 / 3.0, y: 4.0 / 3.0)
imgOptsView.tag = 1
imgOptsView.frame = view.frame
let imageView = UIImageView()
imageView.frame = imgOptsView.frame
imageView.translatesAutoresizingMaskIntoConstraints = true
imageView.transform = scale //to account for the removal of the black bar in the camera
let useButton = UIButton()
imageView.tag = 2
imageView.contentMode = .scaleAspectFit
imageView.image = currentImage
useButton.setImage(#imageLiteral(resourceName: "check circle"), for: .normal)
useButton.translatesAutoresizingMaskIntoConstraints = true
useButton.frame = CGRect(x: view.frame.width / 2, y: view.frame.height / 2, width: 100, height: 100)
let cancelButton = UIButton()
colorSlider.previewEnabled = true
colorSlider.translatesAutoresizingMaskIntoConstraints = true
imgOptsView.addSubview(imageView)
imgOptsView.addSubview(useButton)
imgOptsView.addSubview(colorSlider)
imgOptsView.isUserInteractionEnabled = true
useButton.addTarget(self, action: #selector(ViewController.usePicture(sender:)), for: .touchUpInside)
picker.cameraOverlayView!.addSubview(imgOptsView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
mouseSwiped = false //to check if the touch moved or was simply dotted
let touch: UITouch? = touches.first
lastPoint = touch?.location(in: view) //where to start image context from
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
mouseSwiped = true
let touch: UITouch? = touches.first
let currentPoint: CGPoint? = touch?.location(in: view)
UIGraphicsBeginImageContext(view.frame.size) //begin drawing
tempDrawImage.image?.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(view.frame.size.width), height: CGFloat(view.frame.size.height)))
UIGraphicsGetCurrentContext()?.move(to: CGPoint(x: CGFloat(lastPoint.x), y: CGFloat(lastPoint.y)))
UIGraphicsGetCurrentContext()?.addLine(to: CGPoint(x: CGFloat((currentPoint?.x)!), y: CGFloat((currentPoint?.y)!)))
UIGraphicsGetCurrentContext()!.setLineCap(.round)
UIGraphicsGetCurrentContext()?.setLineWidth(1.0)
UIGraphicsGetCurrentContext()?.setStrokeColor(c)
UIGraphicsGetCurrentContext()!.setBlendMode(.normal)
UIGraphicsGetCurrentContext()?.strokePath() //move from lastPoint to currentPoint with these options
tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
lastPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !mouseSwiped {
UIGraphicsBeginImageContext(view.frame.size)
tempDrawImage.image?.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(view.frame.size.width), height: CGFloat(view.frame.size.height)))
UIGraphicsGetCurrentContext()!.setLineCap(.round)
UIGraphicsGetCurrentContext()?.setLineWidth(1.0)
UIGraphicsGetCurrentContext()?.setStrokeColor(c)
UIGraphicsGetCurrentContext()?.move(to: CGPoint(x: CGFloat(lastPoint.x), y: CGFloat(lastPoint.y)))
UIGraphicsGetCurrentContext()?.addLine(to: CGPoint(x: CGFloat(lastPoint.x), y: CGFloat(lastPoint.y)))
UIGraphicsGetCurrentContext()?.strokePath()
UIGraphicsGetCurrentContext()!.flush()
tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContext(mainImage.frame.size)
mainImage.image?.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(view.frame.size.width), height: CGFloat(view.frame.size.height)), blendMode: .normal, alpha: 1.0)
tempDrawImage.setNeedsDisplay()
mainImage.image = UIGraphicsGetImageFromCurrentImageContext() //paste tempImage onto the main image and then start over
tempDrawImage.image = nil
UIGraphicsEndImageContext()
}
func changedColor(_ slider: ColorSlider) {
c = slider.color.cgColor
}
This is just a guess, but worth a shot. I had a similar problem but in a different context... I was working with AVPlayer when it happened.
In my case, it's because the zPosition wasn't set correctly, so the overlays I was trying to draw appeared behind the video. Assuming that's the case, the fix would be to set the zPosition = -1 which moves it further back like so:
AVPlayerView.layer?.zPosition = -1
This moved the base layer behind the overlay layer (negative numbers move further away from the user in 3D space.)
I would see if the view controlled by your UIImagePickerController allows you to set it's zPosition similar to what AVPlayer allows. If I recall correctly, any view should be able to set it's zPosition. Alternatively, you could try moving your overlay layer to a positive value like zPosition = 1. (I'm not certain, but I would imagine that the default is position is 0.)