Notability and other note taking apps have this 'zoom box' feature where you can draw in the magnified box at the bottom. Users can also drag the box at the top to change what they want magnified at the bottom. I have tried literally everything I can think of to add this feature in my app. I have added the same document in two views but then I run into many memory issues, I've duplicated the file but again memory issues. Does anyone know a simple way to do this? Is there anyway I can just have a view that is a magnification of another view?
Create a new Cocoa Touch Class (optionally name it MagnifyView) and set it as a subclass of UIView
Add the following code in your class:
var viewToMagnify: UIView!
var touchPoint: CGPoint!
override init(frame: CGRect)
{
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit()
{
// Set border color, border width and corner radius of the magnify view
self.layer.borderColor = UIColor.lightGray.cgColor
self.layer.borderWidth = 3
self.layer.cornerRadius = 50
self.layer.masksToBounds = true
}
func setTouchPoint(pt: CGPoint)
{
touchPoint = pt
self.center = CGPoint(x: pt.x, y: pt.y - 100)
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context!.translateBy(x: 1 * (self.frame.size.width * 0.5), y: 1 * (self.frame.size.height * 0.5))
context!.scaleBy(x: 1.5, y: 1.5) // 1.5 is the zoom scale
context!.translateBy(x: -1 * (touchPoint.x), y: -1 * (touchPoint.y))
self.viewToMagnify.layer.render(in: context!)
}
To use it, implement touchesBegan, touchesMoved and touchesEnd functions in View Controller which you want to have the magnifying effect.
Here is how:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
let point = touches.first?.location(in: self.view)
if magnifyView == nil
{
magnifyView = MagnifyView.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
magnifyView.viewToMagnify = self.view
magnifyView.setTouchPoint(pt: point!)
self.view.addSubview(magnifyView)
}
}
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
if magnifyView != nil
{
magnifyView.removeFromSuperview()
magnifyView = nil
}
}
override func touchesMoved(_ touches: Set, with event: UIEvent?) {
let point = touches.first?.location(in: self.view)
magnifyView.setTouchPoint(pt: point!)
magnifyView.setNeedsDisplay()
}
original source here
Related
I have created Circle using "draw" and can place it anywhere on view. but when I want to move a specific circle by clicking it. it selects "LAST" created circle.
how can I select a specific circle? please guide me for the same.
if anything else you need let me know.
**CircleView**
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init(coder aDecoder : NSCoder) {
fatalError("init(coder : ) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext(){
context.setLineWidth(2)
UIColor.yellow.set()
let circleCenter = CGPoint(x: frame.size.width / 2, y: frame.self.height / 2)
let circleRadius = (frame.size.width - 10) / 2
context.addArc(center: circleCenter, radius: circleRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
context.strokePath()
}
}
}
**ViewController**
import UIKit
class ViewController: UIViewController {
var circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
let circleWidth = CGFloat(100)
var lastCircleCenter = CGPoint()
var currentCenter = CGPoint()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.lightGray
circleView.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
lastCircleCenter = touch.location(in: view)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let circleCenter = touch.location(in: view)
if circleCenter == lastCircleCenter{
let circleHeight = circleWidth
circleView = CircleView(frame: CGRect(x: circleCenter.x - circleWidth / 2, y: circleCenter.y - circleWidth / 2, width: 100, height: 100))
view.addSubview(circleView)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let location = touch.location(in: view)
circleView.center = location
}
}
how can I select a specific circle? please guide me for the same.
if anything else you need let me know.
how can I select a specific circle? please guide me for the same.
if anything else you need let me know.
If you are creating multiple circles and adding them to your view, I would suggest to keep track of the created circles in a collection. That way on each touch you can check if the coordinate matches any of the created circles. Based on that you can determine if to create a new circle or to move an existing one.
Example:
class ViewController: UIViewController {
var circleViews: [CircleView] = []
let circleWidth = CGFloat(100)
var draggedCircle: CircleView?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do nothing if a circle is being dragged
// or if we do not have a coordinate
guard draggedCircle == nil, let point = touches.first?.location(in: view) else {
return
}
// Do not create new circle if touch is in an existing circle
// Keep the reference of the (potentially) dragged circle
if let draggedCircle = circleViews.filter({ UIBezierPath(ovalIn: $0.frame).contains(point) }).first {
self.draggedCircle = draggedCircle
return
}
// Create new circle and store in an array
let offset = circleWidth / 2
let rect = CGRect(x: point.x - offset, y: point.y - offset, width: circleWidth, height: circleWidth)
let circleView = CircleView(frame: rect)
circleViews.append(circleView)
view.addSubview(circleView)
// The newly created view can be immediately dragged
draggedCircle = circleView
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// If touches end then a circle is never dragged
draggedCircle = nil
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let draggedCircle = draggedCircle, let point = touches.first?.location(in: view) else {
return
}
draggedCircle.center = point
}
}
i'm trying to draw over UIImageView with this custome class and i would like to be able to change the size and color of lines being drawn by user. But every time i change the size or color of CALayer Previously added layer's color and size also changes then.
class DrawingImageView: UIImageView {
private lazy var path = UIBezierPath()
private lazy var previousTouchPoint = CGPoint.zero
var strokeColor: CGColor!
var strokeWidth: CGFloat!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = strokeWidth ?? 4
shapeLayer.strokeColor = strokeColor ?? UIColor.black.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
layer.addSublayer(shapeLayer)
if let location = touches.first?.location(in: self) { previousTouchPoint = location }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let location = touches.first?.location(in: self) {
path.move(to: location)
path.addLine(to: previousTouchPoint)
previousTouchPoint = location
let layerA = layer.sublayers?.last as! CAShapeLayer
layerA.path = path.cgPath
}
}
}
this is how i assign that class to tempImageView
let mainImageView = UIImageView()
var tempImgView: DrawingImageView?
mainImageView.frame = CGRect(x: 0, y: self.view.bounds.height / 7, width: imageWidth, height: imageHieght)
mainImageView.contentMode = .scaleAspectFit
mainImageView.layer.zPosition = 2
mainImageView.isOpaque = false
mainImageView.backgroundColor = .clear
mainImageView.isUserInteractionEnabled = true
mainImageView.tintColor = .clear
self.view.addSubview(mainImageView)
func DrawOverImage() {
mainImageView.isHidden = true
let drawnImageView = DrawingImageView(frame: mainImageView.frame)
drawnImageView.image = mainImageView.image
drawnImageView.contentMode = .scaleAspectFit
drawnImageView.isUserInteractionEnabled = true
drawnImageView.layer.zPosition = 3
self.tempImgView?.isUserInteractionEnabled = true
self.tempImgView = drawnImageView
self.view.addSubview(tempImgView!)
}
i also tried creating array of CAShapeLayers and add layers to an array and then assign replace subLayer like View.layer.subLayers[0] = layers[0] but that also didn't work and i couldn't find any useful resource online.
any help or suggestion would be really appreciated, thanks.
That is because you are continuing the same path each time and so the latest stroke color and width gets applied to the whole path.
Without looking at much of your other logic, I believe you should initialize a new path each time in your touchesBegan and this new path should be used in touchesMoved
So in touchesBegan, try adding path = UIBezierPath() after layer.addSublayer(shapeLayer) and see if this gives you what you are looking for
i am trying to change the shape of UIBezierPath that i have created like in this tutorial:
[https://www.appcoda.com/bezier-paths-introduction/]
thats my code:
class Geometry: UIView {
//var path: UIBezierPath!
var path = UIBezierPath()
override func draw(_ rect: CGRect) {
createLine(x: 100, y: 100)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.white
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func transformShape(x:CGFloat, y:CGFloat)
{
path.removeAllPoints()
createLine(x: x, y: y)
}
func createLine(x: CGFloat, y: CGFloat)
{
var path = UIBezierPath()
path.move(to: CGPoint(x: 0.0, y: 0.0))
path.addLine(to: CGPoint(x: x, y: y))
path.close()
path.fill()
path.stroke()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
let touch = touches.first
let location = (touch?.location(in: self))!;
transformShape(x:location.x, y:location.y)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
let touch = touches.first
let location = (touch?.location(in: self))!;
transformShape(x:location.x, y:location.y)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
let touch = touches.first
let location = (touch?.location(in: self))!;
transformShape(x:location.x, y:location.y)
}
}
i have added the the view in the viewcontroller like this:
import UIKit
var geo = Geometry()
let screenWidth: Int = 1024
let screenHeight: Int = 768
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
geo = Geometry(frame: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight))
self.view.addSubview(geo)
}
}
to make it easier to understand what i want to do i made this. i want to be able to move the end of both lines to the left and right on x without changing the y-postion
i have not found anything besides animation. but thats not what i want to accomplish. the top of the triangle should follow the movements of the finger. thanks
It certainly isn't difficult to do this in a crude way:
Here's how I'm doing that:
class MyView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
backgroundColor = .yellow
let g = UIPanGestureRecognizer(target: self, action: #selector(pan))
self.addGestureRecognizer(g)
}
#objc
func pan(_ g:UIGestureRecognizer) {
switch g.state {
case .changed:
let p = g.location(in: self)
self.apex.x = p.x
self.setNeedsDisplay()
default: break
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var apex = CGPoint(x:self.bounds.midX, y:self.bounds.midY)
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.move(to: CGPoint(x: 0, y: self.bounds.maxY))
con.addLine(to: self.apex)
con.addLine(to: CGPoint(x: self.bounds.maxX, y: self.bounds.maxY))
con.strokePath()
}
}
In that code, I'm simply setting the apex point to where the finger is (keeping the y-component constant, as you specify), and redrawing the whole bezier path. So everything from there is just a question of refinement — reducing latency, for example (which you can do by anticipating touches).
when I start to draw on large UIView ( width: 3700 , height: 40000 ), it takes a lot of memory
when app starts, memory is 150 MB and when start drawing on it( calling setNeedsDisplay method) take around 1 GB and app is gonna crash
class DrawingVc: UIViewController {
let contentView = DrawableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = .clear
self.view.addSubview(contentView)
contentView.frame = CGRect(x: 0, y: 0, width:view.frame.width, height:
view.frame.height * 50)
}
here is the code of custom view, as you can see, setNeedsDisplay runs on touchMoves
class DrawableView: UIView {
var mLastPath: UIBezierPath?
weak var scdelegate: DrawableViewDelegate?
var isDrawEnable = true
private var drawingLines : [UIBezierPath] = []
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
debugPrint("request draw")
drawLine()
}
private func drawLine() {
UIColor.blue.setStroke()
for line in drawingList {
line.lineWidth = 4
line.stroke()
line.lineCapStyle = .round
}
}
var drawingList = [UIBezierPath]()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touches.count == 2 {
return
}
let location = (touches.first?.location(in: self))!
mLastPath = UIBezierPath()
mLastPath?.move(to: location)
prevPoint = location
drawingList.append(mLastPath!)
}
var prevPoint: CGPoint?
var isFirst = true
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
debugPrint("touchesMoved: " , (touches.first?.location(in: self).x)! , (touches.first?.location(in: self).y)! )
if let coalescedtouches = event?.coalescedTouches(for: touches.first!)
{
for coalescedTouch in coalescedtouches
{
let locationInView = coalescedTouch.location(in: self)
if let prevPoint = prevPoint {
let midPoint = CGPoint( x: (locationInView.x + prevPoint.x) / 2, y: (locationInView.y + prevPoint.y) / 2)
if isFirst {
mLastPath?.addLine(to: midPoint)
}else {
mLastPath?.addQuadCurve(to: midPoint, controlPoint: prevPoint)
}
isFirst = false
} else {
mLastPath?.move(to: locationInView)
}
prevPoint = locationInView
}
}
setNeedsDisplay()
}
}
What makes this problem and how that fix?
Your view is larger than the largest possible screen on an iOS device, so I suppose your view is embedded in a scrollview. You should only draw the visible parts of your view. Unfortunately, this is not supported by UIView directly. You may take a look on CATiledLayer, which supports drawing of only visible parts of a layer, and it supports different levels of details for zoomed layers, too.
Note: In the below question, I use the term 'lagging' when I probably mean 'latency' when drawing using the addCurveToPoint function.
Problem:
Both bezier curve functions, addQuadCurveToPoint and addCurveToPoint have one strength and one weakness each. The aim is to get the perfect combination of both, a perfect continuous smooth curved line that is lag-free when drawn. The images below show where the touch on the screen typically is in comparison to the updated drawing.
The below image uses the function addQuadCurveToPoint. It draws fast
with no lagging while drawing during touch events, but the end result is a
less perfect smooth curved line that appears more segmented.
The below image uses the function
addCurveToPoint. It draws near perfect continuous smooth curved
lines but is slower with some lag noticeable while drawing during
touch events.
Question:
Can anyone help explain or give a solution please:
how to get perfect addQuadCurveToPoint curved lines or lag-free addCurveToPoint curved lines?
Note: The focus of this question is immediate lagging from the initial touch event for addCurveToPoint, not lagging over time, and also the less perfect curve line for addQuadCurveToPoint.
This code example is just one type of many implementations of addCurveToPoint:
// Swift 2 code below tested using Xcode 7.0.1.
class drawView: UIView {
var path:UIBezierPath?
var incrementalImage:UIImage?
var points = [CGPoint?](count: 5, repeatedValue: nil)
var counter:Int?
var infoView:UIView = UIView()
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = UIBezierPath()
path?.lineWidth = 20.0
strokeColor = UIColor.darkGrayColor()
path?.lineCapStyle = CGLineCap.Round
}
override init(frame: CGRect) {
super.init(frame: frame)
self.multipleTouchEnabled = false
path = UIBezierPath()
path?.lineWidth = 20.0
}
override func drawRect(rect: CGRect) {
incrementalImage?.drawInRect(rect)
strokeColor?.setStroke()
path?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
counter = 0
let touch: AnyObject? = touches.first
points[0] = touch!.locationInView(self)
infoView.removeFromSuperview()
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
counter = counter! + 1
points[counter!] = point
if counter == 4{
points[3]! = CGPointMake((points[2]!.x + points[4]!.x)/2.0, (points[2]!.y + points[4]!.y)/2.0)
path?.moveToPoint(points[0]!)
path?.addCurveToPoint(points[3]!, controlPoint1: points[1]!, controlPoint2: points[2]!)
self.setNeedsDisplay()
points[0]! = points[3]!
points[1]! = points[4]!
counter = 1
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
path?.removeAllPoints()
counter = 0
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap(){
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if((incrementalImage) == nil){
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
path?.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
This code example is just one type of many implementations of addQuadCurveToPoint:
// Swift 2 code below tested using Xcode 7.0.1.
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=20.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: "pan:")
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.darkGrayColor().setStroke()
path.stroke()
path.lineWidth=lineWidth
path.lineCapStyle = .Round
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void
{
let currentPoint=panGestureRecognizer.locationInView(self)
let midPoint=self.midPoint(previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .Began
{
path.moveToPoint(currentPoint)
}
else if panGestureRecognizer.state == .Changed
{
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}