The Problem
This is going to sound crazy. I'm making a drawing app and I want users to be able to draw on images that are bigger or smaller than the screen. So when the user selects an image from his photo library it is put into an image view in a scroll view. The user draws on image views that are the same dimensions as the selected image and in another scroll view on top of the other one. The scrolling of the two scroll views is synchronized so when you draw then scroll the drawing appears to be above the image (in the right place). For some reason however, when the user selects a long image (let's say 400 x 2000), the drawing works at the top of the image, but when you scroll down to draw, the lines you draw go to the top. I can't figure out what's going wrong... My code is below.
About The Code
cameraStill is the image view containing the image
drawable is the height of the image
myScroll is the scroll view for the image
mainImageView, tempImageView, undo1, undo2, undo3 are the drawing layers
drawScroll is the scroll view for the drawing layers
Image Selection
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
self.dismissViewControllerAnimated(true, completion: { () -> Void in
})
if (image != nil) {
self.cameraStill.contentMode = UIViewContentMode.ScaleAspectFit
cameraStill.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
// change uiimageivews size
mainImageView.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
tempImageView.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
undo1.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
undo2.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
undo3.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
drawable = screenWidth*(image.size.height/image.size.width)
myScroll.contentSize = CGSize(width: screenWidth,height: screenWidth*(image.size.height/image.size.width))
drawScroll.contentSize = CGSize(width: screenWidth,height: screenWidth*(image.size.height/image.size.width))
if (screenWidth*(image.size.height/image.size.width) > (screenHeight-130)) {
myScroll.scrollEnabled = true
drawScroll.scrollEnabled = true
}
else {
myScroll.scrollEnabled = false
drawScroll.scrollEnabled = false
cameraStill.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
mainImageView.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
tempImageView.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
undo1.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
undo2.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
undo3.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
}
self.camera!.stopCamera()
}
//drawView.alpha = 1.0
}
Drawing
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
println("began")
if (drawingEnabled == true) {
c1 = 3
closeAllExtras()
swiped = false
if let touch = touches.first as? UITouch {
lastPoint = touch.locationInView(self.view)
}
}
}
func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint) {
//if (fromPoint.y > 50 && fromPoint.y < screenHeight-80 && toPoint.y > 50 && toPoint.y < screenHeight-80) {
// 1
UIGraphicsBeginImageContext(CGSize(width: view.frame.size.width,height: drawable))
let context = UIGraphicsGetCurrentContext()
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable))
// 2
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y)
CGContextAddLineToPoint(context, toPoint.x, toPoint.y)
// 3
CGContextSetLineCap(context, kCGLineCapRound)
CGContextSetLineWidth(context, brushWidth)
CGContextSetRGBStrokeColor(context, red, green, blue, 1.0)
CGContextSetBlendMode(context, kCGBlendModeNormal)
// 4
CGContextStrokePath(context)
// 5
tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.alpha = opacity
UIGraphicsEndImageContext()
//}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
// 6
if (drawingEnabled == true) {
swiped = true
if let touch = touches.first as? UITouch {
let currentPoint = touch.locationInView(view)
drawLineFrom(lastPoint, toPoint: currentPoint)
// 7
lastPoint = currentPoint
}
}
}
func mergeViewContext(v1 : UIImageView, v2: UIImageView) {
UIGraphicsBeginImageContext(v1.frame.size)
v1.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: 1.0)
v2.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: 1.0)
v1.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
v2.image = nil
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if (drawingEnabled == true) {
if !swiped {
// draw a single point
drawLineFrom(lastPoint, toPoint: lastPoint)
}
mergeViewContext(mainImageView, v2: undo1)
undo1.image = undo2.image
undo2.image = nil
undo2.image = undo3.image
undo3.image = nil
UIGraphicsBeginImageContext(undo3.frame.size)
undo3.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: 1.0)
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: opacity)
undo3.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}
Synching Both Scroll Views
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView == drawScroll) {
var offset = scrollView.contentOffset
myScroll.setContentOffset(offset, animated: false)
}
}
I got it to work by correcting the following values with the offset of the scroll view. However, I get some blurring for long images and a very strange bug with short ones. No idea what's wrong.
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y)
CGContextAddLineToPoint(context, toPoint.x, toPoint.y)
Related
I'm drawing lines with UIBezierPath in draw(_ rect: CGRect) method previously i did tried that with CAShapeLayer but i wasn't able to select particular path and move it so i'm trying this, but after drawing i'm not able to access that BezierPath from view's subviews or it's sublayers and not directly from UIView. How can i access that bezierpath drawn in drawRect method so i can change it's position according to touches.
private var bezierPaths: [MyBezierPath] = [MyBezierPath]()
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.orange.set()
for path in bezierPaths {
path.lineWidth = 4
path.lineCapStyle = .round
path.lineJoinStyle = .round
path.stroke()
}
}
func drawingPath(_ path: [MyBezierPath]) {
bezierPaths = path
}
like creating CAShapeLayer and setting layer.path = BezierPath like this :
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
shapeLayer.lineDashPattern = [10, 10]
shapeLayer.name = "ShapeLayer"
self.canvas.layer.addSublayer(shapeLayer)
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if canvas.layer.sublayers != nil && canvas.layer.sublayers?.last?.name == "ShapeLayer" {
guard let layer = canvas.layer.sublayers?.last as? CAShapeLayer else { return }
layer.path = path.cgPath
}
}
like this i can access layer.path but how can i do it for path drawn in draw(Rect:) method ?
Depending on what you're actually trying to do, overriding draw(_:) may not be the best approach.
For example, if you want to animate the drawn paths, it will be much easier if you are using CAShapeLayer as sublayers, or using subviews.
Also, shape layers are highly optimized ... if you write your own draw / animate functions you risk ending up with lesser-optimized code.
However, whichever approach you take, to "find the path" you want to use contains(_ point: CGPoint)
For example, if you have an array of UIBezierPath, with all paths relative to the top-left (0,0) of the view, on touch you could do:
// find a path that contains the touch point
if let touchedPath = bezierPaths.first(where: {$0.contains(point)}) {
// do something with that path
}
If the paths are not relative to the top-left of the view - for example, if the path is relative to the layer position, or is part of a subview - you'd need to convert the touch point.
Here's a quick example that looks like this - on load, and then after dragging a few shapes around:
We'll start with a simple "path" struct, which contains the bezier path and the color to use. We could add various other properties, such as line width, dash pattern, etc:
struct MyPath {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
}
then we'll use this UIView subclass that will handle the drawing, as well as touches began/moved/ended:
class BezDrawView: UIView {
var myPaths: [MyPath] = [] {
didSet {
setNeedsDisplay()
}
}
// used to track path to move
var activePath: MyPath?
var startPoint: CGPoint = .zero
override func draw(_ rect: CGRect) {
super.draw(rect)
if myPaths.count > 0 {
myPaths.forEach { p in
p.color.set()
p.path.stroke()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let point = t.location(in: self)
// find a path that contains the touch point
if let touchedPath = myPaths.first(where: {$0.path.contains(point)}) {
self.activePath = touchedPath
self.startPoint = point
return
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ap = activePath, let t = touches.first else { return }
let point = t.location(in: self)
// move the path by the distance the touch moved
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
ap.path.apply(tr)
startPoint = point
// this triggers draw(_:)
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// done dragging
activePath = nil
}
}
and an example controller which defines some sample shapes (paths):
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let testBezDrawView = BezDrawView()
testBezDrawView.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
testBezDrawView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testBezDrawView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain bez draw view to all 4 sides with 20-points "padding"
testBezDrawView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testBezDrawView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testBezDrawView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testBezDrawView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add some sample path shapes and colors
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow, .orange, .green,
]
let rects: [CGRect] = [
CGRect(x: 20, y: 20, width: 60, height: 60),
CGRect(x: 180, y: 20, width: 40, height: 60),
CGRect(x: 20, y: 120, width: 60, height: 100),
CGRect(x: 200, y: 140, width: 50, height: 90),
CGRect(x: 90, y: 220, width: 100, height: 60),
CGRect(x: 220, y: 260, width: 80, height: 160),
CGRect(x: 50, y: 360, width: 200, height: 100),
CGRect(x: 150, y: 480, width: 120, height: 80),
]
var somePaths: [MyPath] = []
var i: Int = 0
for (c, r) in zip(colors, rects) {
var b = UIBezierPath()
switch i % 4 {
case 1: // oval
b = UIBezierPath(ovalIn: r)
case 2: // triangle shape
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.maxY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.maxY))
b.close()
case 3: // diamond
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.maxY))
b.close()
default: // rect
b = UIBezierPath(rect: r)
}
b.lineWidth = 4
b.lineCapStyle = .round
b.lineJoinStyle = .round
b.setLineDash([5, 10], count: 2, phase: 0)
let p = MyPath(color: c, path: b)
somePaths.append(p)
i += 1
}
testBezDrawView.myPaths = somePaths
}
}
Here's a video of it in use (too big to convert to gif and embed here):
https://imgur.com/a/BmaHkKM
Edit in response to comment...
That needs very few changes to make it work with shape layers instead of draw(_:).
We can use the same Translation Transform to "move" the path, then update the .path property of that path's associated layer:
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
active.path.apply(tr)
activeLayer.path = activeParh.path.cgPath
I strongly, strongly recommend that you try to convert that sample code to shape layers on your own - it would be a good learning exercise.
But, if you run into trouble, here's a modified version...
First, we're going to use the. array index of the MyPath object to match with the sublayer index, so we need to make our struct Equatable:
struct MyPath: Equatable {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
}
Then some minor changes to BezDrawView -- which we'll name BezLayerView:
class BezLayerView: UIView {
var myPaths: [MyPath] = [] {
didSet {
// remove any existing layers
if let subs = layer.sublayers {
subs.forEach { lay in
lay.removeFromSuperlayer()
}
}
// create layers for paths
myPaths.forEach { p in
let lay = CAShapeLayer()
lay.lineWidth = 4
lay.lineCap = .round
lay.lineJoin = .round
lay.lineDashPattern = [5, 10]
lay.fillColor = UIColor.clear.cgColor
lay.strokeColor = p.color.cgColor
lay.path = p.path.cgPath
layer.addSublayer(lay)
}
}
}
// used to track path to move
var activeLayer: CAShapeLayer?
var activePath: MyPath?
var startPoint: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first, let subs = layer.sublayers else { return }
let point = t.location(in: self)
// find a path that contains the touch point
if let touchedPath = myPaths.first(where: {$0.path.contains(point)}) {
// find the layer associated with that path
if let idx = myPaths.firstIndex(of: touchedPath) {
if let lay = subs[idx] as? CAShapeLayer {
self.activePath = touchedPath
self.activeLayer = lay
self.startPoint = point
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let lay = activeLayer, let ap = activePath, let t = touches.first else { return }
let point = t.location(in: self)
// move the path by the distance the touch moved
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
ap.path.apply(tr)
lay.path = ap.path.cgPath
startPoint = point
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// done dragging
activeLayer = nil
activePath = nil
}
}
and an almost identical version of the controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let testBezLayerView = BezLayerView()
testBezLayerView.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
testBezLayerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testBezLayerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain bez draw view to all 4 sides with 20-points "padding"
testBezLayerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testBezLayerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testBezLayerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testBezLayerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add some sample path shapes and colors
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow, .orange, .green,
]
let rects: [CGRect] = [
CGRect(x: 20, y: 20, width: 60, height: 60),
CGRect(x: 180, y: 20, width: 40, height: 60),
CGRect(x: 20, y: 120, width: 60, height: 100),
CGRect(x: 200, y: 140, width: 50, height: 90),
CGRect(x: 90, y: 220, width: 100, height: 60),
CGRect(x: 220, y: 260, width: 80, height: 160),
CGRect(x: 50, y: 360, width: 200, height: 100),
CGRect(x: 150, y: 480, width: 120, height: 80),
]
var somePaths: [MyPath] = []
var i: Int = 0
for (c, r) in zip(colors, rects) {
var b = UIBezierPath()
switch i % 4 {
case 1: // oval
b = UIBezierPath(ovalIn: r)
case 2: // triangle shape
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.maxY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.maxY))
b.close()
case 3: // diamond
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.maxY))
b.close()
default: // rect
b = UIBezierPath(rect: r)
}
let p = MyPath(color: c, path: b)
somePaths.append(p)
i += 1
}
testBezLayerView.myPaths = somePaths
}
}
The output and functionality should be indistinguishable from the BezDrawView implementation.
Hello all, I tried to add arc for UIBezierPath I could not able to get the exact curve,
here is my code here I have added the bezier path for the added curve from the center position.
#IBDesignable
class MyTabBar: 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
}
}
this is tab bar controller added plus button in center view center, and the when tap the plus button to add the curve based popup should show, I don't know how to add curve based popup.
class TabbarViewController: UITabBarController,UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.navigationController?.navigationBar.isHidden = true
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))
middleBtn.setImage(UIImage(named: "PlusBtn"), for: .normal)
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) {
//show the popUp
}
}
Please share me the findings & share me your refreance
Thanks
New edit:
I created a general-purpose method that will generate polygons with a mixture of sharp and rounded corners of different radii. I used it to create a project with a look rather like what you are after. You can download it from Github:
https://github.com/DuncanMC/CustomTabBarController.git
Here's what it looks like:
Note that the area of the tab's view controller that extends into the tab bar controller does not get taps. If you try to tap there, it triggers the tab bar controller. You will have to do some more tinkering to get that part to work.
Ultimately you may have to create a custom UITabBar (or UITabBar-like component) and possibly a custom parent view controller that acts like a UITabBar, in order to get what you want.
The method that creates polygon paths is called buildPolygonPathFrom(points:defaultCornerRadius:)
It takes an array of PolygonPoint structs. Those are defined like this:
/// A struct describing a single vertex in a polygon. Used in building polygon paths with a mixture of rounded an sharp-edged vertexes.
public struct PolygonPoint {
let point: CGPoint
let isRounded: Bool
let customCornerRadius: CGFloat?
init(point: CGPoint, isRounded: Bool, customCornerRadius: CGFloat? = nil) {
self.point = point
self.isRounded = isRounded
self.customCornerRadius = customCornerRadius
}
init(previousPoint: PolygonPoint, isRounded: Bool) {
self.init(point: previousPoint.point, isRounded: isRounded, customCornerRadius: previousPoint.customCornerRadius)
}
}
The code to build the path for the custom tab bar looks like this:
func tabBarMaskPath() -> CGPath? {
let width = bounds.width
let height = bounds.height
guard width > 0 && height > 0 else { return nil }
let dentRadius: CGFloat = 35
let cornerRadius: CGFloat = 20
let topFlatPartWidth = (width - dentRadius * 2.0) / 2
let polygonPoints = [
PolygonPoint(point: CGPoint(x: 0, y: 0), // Point 0
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: 0, y: height), // Point 1
isRounded: false),
PolygonPoint(point: CGPoint(x: width, y: height), // Point 2
isRounded: false),
PolygonPoint(point: CGPoint(x: width, y: 0), // Point 3
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth + dentRadius * 2, y: 0), // Point 4
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth + dentRadius * 2, y: dentRadius + cornerRadius), // Point 5
isRounded: true,
customCornerRadius: dentRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth , y: dentRadius + cornerRadius), // Point 6
isRounded: true,
customCornerRadius: dentRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth , y: 0), // Point 7
isRounded: true,
customCornerRadius: cornerRadius),
]
return buildPolygonPathFrom(points: polygonPoints, defaultCornerRadius: 0)
}
Previous answer:
I just tried it, and it is possible to subclass UITabBar. I created a subclass of UITabBar where I use a mask layer to cut a circular "notch" out of the top of the tab bar. The code is below. It looks like the screenshot below. It isn't quite what you're after, but it's a start:
(The background color for the "Page 1" view controller is set to light gray, and you can see that color showing through in the "notch" I cut out of the tab bar.)
//
// CustomTabBar.swift
// TabBarController
//
// Created by Duncan Champney on 3/31/21.
//
import UIKit
class CustomTabBar: UITabBar {
var maskLayer = CAShapeLayer()
override var frame: CGRect {
didSet {
configureMaskLayer()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configureMaskLayer()
self.layer.mask = maskLayer
self.layer.borderWidth = 0
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configureMaskLayer()
self.layer.mask = maskLayer
}
func configureMaskLayer() {
let rect = layer.bounds
maskLayer.frame = rect
let circleBoxSize = rect.size.height * 1.25
maskLayer.fillRule = .evenOdd
let path = UIBezierPath(rect: rect)
let circleRect = CGRect(x: rect.size.width/2 - circleBoxSize / 2,
y: -circleBoxSize/2,
width: circleBoxSize,
height: circleBoxSize)
let circle = UIBezierPath.init(ovalIn: circleRect)
path.append(circle)
maskLayer.path = path.cgPath
maskLayer.fillColor = UIColor.white.cgColor // Any opaque color works and has no effect
}
}
Edit:
To draw your popover view you'll need to create a filled path that shape. You'll have to construct a custom shape like that with a combination of lines and arcs. I suggest using a CGMutablePath and the method addArc(tangent1End:tangent2End:radius:transform:) since that enables you to provide endpoints rather than angles.
Edit #2:
Another part of the puzzle:
Here is a custom UIView subclass that masks itself in the shape you're after
//
// ShapeWithTabView.swift
// ShapeWithTab
//
// Created by Duncan Champney on 4/1/21.
//
import UIKit
class ShapeWithTabView: UIView {
var cornerRadius: CGFloat = 20
var tabRadius: CGFloat = 60
var tabExtent: CGFloat = 0
var shapeLayer = CAShapeLayer()
var maskLayer = CAShapeLayer()
func buildShapeLayerPath() -> CGPath {
let boxWidth = min(bounds.size.width - 40, 686)
let boxHeight = min(bounds.size.height - 40 - tabRadius * 2 - tabExtent, 832)
// These are the corners of the view's primary rectangle
let point1 = CGPoint(x: 0, y: boxHeight)
let point2 = CGPoint(x: 0, y: 0)
let point3 = CGPoint(x: boxWidth, y: 0)
let point4 = CGPoint(x: boxWidth, y: boxHeight)
// These are the corners of the "tab" that extends outside the view's normal bounds.
let tabPoint1 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight)
let tabPoint2 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight + tabExtent + tabRadius * 2 )
let tabPoint3 = CGPoint(x: boxWidth / 2 - tabRadius, y: boxHeight + tabExtent + tabRadius * 2)
let tabPoint4 = CGPoint(x: boxWidth / 2 - tabRadius , y: boxHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: boxHeight - cornerRadius))
path.addArc(tangent1End: point2,
tangent2End: point3,
radius: cornerRadius)
path.addArc(tangent1End: point3,
tangent2End: point4,
radius: cornerRadius)
path.addArc(tangent1End: point4,
tangent2End: point1,
radius: cornerRadius)
//
path.addArc(tangent1End: tabPoint1,
tangent2End: tabPoint2,
radius: tabRadius)
path.addArc(tangent1End: tabPoint2,
tangent2End: tabPoint3,
radius: tabRadius)
path.addArc(tangent1End: tabPoint3,
tangent2End: tabPoint4,
radius: tabRadius)
path.addArc(tangent1End: tabPoint4,
tangent2End: point1,
radius: tabRadius)
path.addArc(tangent1End: point1,
tangent2End: point2,
radius: cornerRadius)
return path
}
func doInitSetup() {
self.layer.addSublayer(shapeLayer)
self.layer.mask = maskLayer
backgroundColor = .lightGray
//Configure a shape layer to draw an outline
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 2
//Configure a mask layer to mask the view to our custom shape
maskLayer.fillColor = UIColor.white.cgColor
maskLayer.strokeColor = UIColor.white.cgColor
maskLayer.lineWidth = 2
}
override init(frame: CGRect) {
super.init(frame: frame)
self.doInitSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.doInitSetup()
}
public func updateShapeLayerPath() {
let path = buildShapeLayerPath()
shapeLayer.path = path
maskLayer.path = path
}
override var frame: CGRect {
didSet {
print("New frame = \(frame)")
shapeLayer.frame = layer.bounds
}
}
}
Combined with the modified tab bar from above, it looks like the image below. The final task is to get the custom view sized and positioned correctly, and have it land on top of the tab bar.
I am attempting to make an image editing VC for my app and encountered the above issue. Whenever I start drawing on my image, the image would warp and then lose the aspect ratio.
Gif is:
My full code is:
class DrawImageController: UIViewController {
var canvasImageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.backgroundColor = .yellow
iv.contentMode = .scaleAspectFit
return iv
}()
var lastTouch = CGPoint.zero
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
view.backgroundColor = .black
view.addSubview(canvasImageView)
canvasImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
canvasImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
canvasImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
canvasImageView.heightAnchor.constraint(equalToConstant: 300).isActive = true
canvasImageView.image = UIImage(named: "testImage")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let firstTouch = touches.first {
lastTouch = firstTouch.location(in: canvasImageView)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let firstTouch = touches.first {
let touchLocation = firstTouch.location(in: canvasImageView)
drawLine(from: lastTouch, to: touchLocation)
lastTouch = touchLocation
}
}
func drawLine(from: CGPoint, to: CGPoint) {
UIGraphicsBeginImageContext(canvasImageView.frame.size)
if let context = UIGraphicsGetCurrentContext() {
canvasImageView.image?.draw(in: CGRect(x: 0, y: 0, width: canvasImageView.frame.size.width, height: canvasImageView.frame.size.height))
context.move(to: from)
context.addLine(to: to)
context.setLineCap(.round)
context.setLineWidth(5.0)
context.setStrokeColor(UIColor.blue.cgColor)
context.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()
canvasImageView.image = image
UIGraphicsEndImageContext()
}
}
}
I adapted my draw method from various tutorials on YouTube, GitHub and SO. Where have I gone wrong?
Solved
With the advice from #Sweeper, I have modified my code in setupViews() and drawLine to account for aspect ratio of the image and imageView.
func setupViews() {
view.backgroundColor = .black
view.addSubview(canvasImageView)
canvasImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
canvasImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
canvasImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
let aspectRatio = getImageAspectRatio(image: UIImage(named: "testImage")!)
let screenWidth = UIScreen.main.bounds.width
let height = CGFloat(1.0) / aspectRatio * screenWidth
canvasImageView.heightAnchor.constraint(equalToConstant: height).isActive = true
canvasImageView.image = UIImage(named: "testImage")
}
func drawLine(from: CGPoint, to: CGPoint) {
UIGraphicsBeginImageContext(canvasImageView.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {return}
if let canvasImage = canvasImageView.image {
let imageViewAspectRatio = getAspectRatio(frame: canvasImageView.frame)
let imageAspectRatio = getImageAspectRatio(image: canvasImage)
if imageViewAspectRatio > imageAspectRatio {
canvasImageView.image?.draw(in: CGRect(x: 0, y: 0, width: imageAspectRatio * canvasImageView.frame.size.height, height: canvasImageView.frame.size.height))
} else if imageViewAspectRatio < imageAspectRatio {
canvasImageView.image?.draw(in: CGRect(x: 0, y: 0, width: canvasImageView.frame.size.width, height: CGFloat(1.0) / imageAspectRatio * canvasImageView.frame.size.width))
} else {
canvasImageView.image?.draw(in: CGRect(x: 0, y: 0, width: canvasImageView.frame.size.width, height: canvasImageView.frame.size.height))
}
context.move(to: from)
context.addLine(to: to)
context.setLineCap(.round)
context.setLineWidth(5.0)
context.setStrokeColor(UIColor.blue.cgColor)
context.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()
canvasImageView.image = image
UIGraphicsEndImageContext()
}
}
The problem is here:
canvasImageView.image?.draw(in: CGRect(x: 0, y: 0, width: canvasImageView.frame.size.width, height: canvasImageView.frame.size.height))
You are drawing the image using the image view's frame. This stretches the image.
You need to draw the image as if contentMode is .scaleAspectFit.
To do this, first determine the image's aspect ratio (W:H). You can do this by access the size property of UIImage. Compare this ratio to the aspect ratio of the image view.
If the image's ratio is smaller than the view's, then that means the height at which you draw the image can be the same as the image view height, and the image width can be calculated using the aspect ratio of the image.
If the image's ratio is larger than the view's, then that means the width at which you draw the image can be the same as the image view width, and the image height can be calculated,
I am working on drawing app. I have three image views -
imageView - Contains base Image
tempImageView - for drawing annotations. drawLineFrom function takes a point and draw then lines on tempImageView
func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint)
{
//print("drawLineFrom")
let mid1 = CGPoint(x:(prevPoint1.x + prevPoint2.x)*0.5, y:(prevPoint1.y + prevPoint2.y)*0.5)
let mid2 = CGPoint(x:(toPoint.x + prevPoint1.x)*0.5, y:(toPoint.y + prevPoint1.y)*0.5)
UIGraphicsBeginImageContextWithOptions(self.tempImageView.boundsSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext()
{
tempImageView.image?.draw(in: CGRect(x: 0, y: 0, width: self.tempImageView.frame.size.width, height: self.tempImageView.frame.size.height))
let annotaionPath = UIBezierPath()
annotaionPath.move(to: CGPoint(x: mid1.x, y: mid1.y))
annotaionPath.addQuadCurve(to: CGPoint(x:mid2.x,y:mid2.y), controlPoint: CGPoint(x:prevPoint1.x,y:prevPoint1.y))
annotaionPath.lineCapStyle = CGLineCap.round
annotaionPath.lineJoinStyle = CGLineJoin.round
annotaionPath.lineWidth = editorPanelView.brushWidth
context.setStrokeColor(editorPanelView.drawingColor.cgColor)
annotaionPath.stroke()
tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.alpha = editorPanelView.opacity
UIGraphicsEndImageContext()
}
}
drawingImageView - after each touchesEnded method I am merging tempImageView with drawingImageView and setting tempImageView.image = nil .
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
isDrawing = false
if !swiped
{
drawLineFrom(fromPoint: lastPoint, toPoint: lastPoint)
}
annotationArray.append(annotationsPoints)
annotationsPoints.removeAll()
// Merge tempImageView into drawingImageView
UIGraphicsBeginImageContext(drawingImageView.frame.size)
drawingImageView.image?.draw(in: CGRect(x: 0, y: 0, width: drawingImageView.frame.size.width, height: drawingImageView.frame.size.height), blendMode: CGBlendMode.normal, alpha: 1.0)
tempImageView.image?.draw(in: CGRect(x: 0, y: 0, width: drawingImageView.frame.size.width, height: drawingImageView.frame.size.height), blendMode: CGBlendMode.normal, alpha: editorPanelView.opacity)
drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}
When save button clicked,
let drawingImage = self.drawingImageView.image
let combinedImage = self.imageView.combineWithOverlay(overlayImageView: self.drawingImageView)
and I am saving combinedImage.
Problem is, when I merge tempImage view with drawing image view, the annotations get blurred.I want to maintain same clarity. I am not able to find any solution for this. Any help (even if it's just a kick in the right direction) would be appreciated.
I think the issue is with using UIGraphicsBeginImageContext(drawingImageView.frame.size).
The default scale it uses is 1.0 so if you're using a retina screen, it will cause the content to be scaled up 2 or 3 times causing the blurry appearance.
You should use UIGraphicsBeginImageContextWithOptions like you have in drawLineFrom with a scale of 0.0 which will default to the screens scale.
So in my swift app I'm allowing the user to paint with there finger on touch. I'm using cgcontext to do this. After the user lifts the finger and the touch ends I am dynamically adding a visualeffect view on top of the shape with the same height and width of the drawn shape. What i want to do next is use the shape as a mask for visualeffect view. The problem right now is if i try to mask the visualeffect view with the shape. the masked view does not show unless the origin point of the shape is (0,0). Here is a link to how i'm currently attempting to implementing this (https://pastebin.com/JaM9kx4G)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
swiped = false
if let touch = touches.first as? UITouch {
if (touch.view == sideView){
return
}
tempPath = UIBezierPath()
lastPoint = touch.location(in: view)
tempPath.move(to:lastPoint)
}
}
func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint) {
tempPath.addLine(to: CGPoint(x: toPoint.x, y: toPoint.y))
UIGraphicsBeginImageContext(view.frame.size)
let context = UIGraphicsGetCurrentContext()
otherImageView.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
// 2
context!.move(to: CGPoint(x:fromPoint.x,y:fromPoint.y))
context!.addLine(to: CGPoint(x:toPoint.x, y:toPoint.y))
// 3
context!.setLineCap(CGLineCap.round)
context!.setLineWidth(brushWidth)
context!.setStrokeColor(red: red, green: green, blue: blue, alpha: 1.0)
context!.setBlendMode(CGBlendMode.normal)
// 4
context!.strokePath()
// 5
otherImageView.image = UIGraphicsGetImageFromCurrentImageContext()
otherImageView.alpha = opacity
UIGraphicsEndImageContext()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
swiped = true
if let touch = touches.first as? UITouch {
let currentPoint = touch.location(in: view)
drawLineFrom(fromPoint: lastPoint, toPoint: currentPoint)
// 7
lastPoint = currentPoint
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
tempPath.close()
let tempImage = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
UIGraphicsBeginImageContext(tempImage.frame.size)
tempImage.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendMode.normal, alpha: 1.0)
otherImageView.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendMode.normal, alpha: opacity)
let context = UIGraphicsGetCurrentContext()
let image = UIGraphicsGetImageFromCurrentImageContext()
tempImage.image = image
otherImageView.image = nil
imageView.addSubview(tempImage)
let blur = VisualEffectView()
blur.frame = CGRect(x:tempPath.bounds.origin.x,y:tempPath.bounds.origin.y, width:tempPath.bounds.width, height:tempPath.bounds.height)
blur.blurRadius = 5
blur.layer.mask = tempImage.layer
}