I created a drawing canvas using 2 UIImageViews. The reason there's 2 is to do with an eraser feature (not present in this minimum reproducible example), which applies a white colour to one of them, when user lifts finger off screen it merges the white strokes into the other UIImageView.
When I added a UIScrollView to allow zooming in and out with pinch and double tap gestures, it prevented me from drawing. The print statement in the touchesBegan method never executes. Something about the UIScrollView is interfering with the drawing methods.
import UIKit
extension UIView {
var safeAreaBottom: CGFloat {
if #available(iOS 11, *) {
if let window = UIApplication.shared.keyWindowInConnectedScenes {
return window.safeAreaInsets.bottom
}
}
return 0
}
var safeAreaTop: CGFloat {
if #available(iOS 11, *) {
if let window = UIApplication.shared.keyWindowInConnectedScenes {
return window.safeAreaInsets.top
}
}
return 0
}
}
/* Identifies the key window in the app so the current window can be accessed in above 2 methods to get it's bottom and top
safe area insets (navigation toolbar / tabBar).
*/
extension UIApplication {
var keyWindowInConnectedScenes: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
}
extension ViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return finalImage
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerImage()
}
}
class ViewController: UIViewController {
@IBOutlet weak var imageScrollView: UIScrollView!
@IBOutlet weak var finalImage: UIImageView!
@IBOutlet weak var drawImage: UIImageView!
var lastPoint = CGPoint.zero
var color: UIColor = .black
var lineWidth: CGFloat = 10.0
var opacity: CGFloat = 1.0
var swiped = Bool()
var screenBounds = CGRect()
var screenWidth = CGFloat()
var screenHeight = CGFloat()
let drawView = UIView()
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
screenWidth = view.bounds.width
screenHeight = view.bounds.height
screenBounds = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
drawView.alignmentRect(forFrame: screenBounds)
image = UIImage(named: "landscape")
finalImage.image = image
finalImage.image = imageResize(image: finalImage.image!, sizeChange: finalImage.intrinsicContentSize)
finalImage.contentMode = .scaleAspectFit
finalImage.alignmentRect(forFrame: CGRect(origin: .zero, size: image?.size ?? CGSize(width: 0, height: 0)))
drawImage.alignmentRect(forFrame: CGRect(origin: .zero, size: image?.size ?? CGSize(width: 0, height: 0)))
imageScrollView.delegate = self
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let image = image {
setMinZoomScaleForImageSize(image.size)
}
}
func imageResize(image: UIImage, sizeChange: CGSize) -> UIImage {
let hasAlpha = true
let scale: CGFloat = 0.0 // Use scale factor of main screen
// Create a Drawing Environment (which will render to a bitmap image, later)
UIGraphicsBeginImageContextWithOptions(sizeChange, !hasAlpha, scale)
image.draw(in: CGRect(origin: .zero, size: sizeChange))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
// Clean up the Drawing Environment (created above)
UIGraphicsEndImageContext()
return scaledImage!
}
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
UIGraphicsBeginImageContext(CGSize(width: screenWidth, height: screenHeight))
guard let context = UIGraphicsGetCurrentContext() else { return }
finalImage.image?.draw(in: screenBounds)
context.move(to: fromPoint)
context.addLine(to: toPoint)
context.setLineCap(.round)
context.setBlendMode(.normal)
context.setLineWidth(lineWidth)
context.setStrokeColor(color.cgColor)
context.strokePath()
finalImage.image = UIGraphicsGetImageFromCurrentImageContext()
finalImage.alpha = opacity
UIGraphicsEndImageContext()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("in touches began!")
guard let touch = touches.first else { return }
swiped = false
lastPoint = touch.location(in: drawView)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
swiped = true
var currentPoint = touch.location(in: drawView)
drawLine(from: lastPoint, to: currentPoint)
lastPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// draw a single point
if swiped { drawLine(from: lastPoint, to: lastPoint) } else { return }
// Merge drawImageView into finalImageView
UIGraphicsBeginImageContext(finalImage.frame.size)
finalImage.image?.draw(in: screenBounds, blendMode: .normal, alpha: 1.0)
drawImage?.image?.draw(in: screenBounds, blendMode: .normal, alpha: opacity)
finalImage.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
drawImage.image = nil
}
@IBAction func doubleTapImage(_ sender: UITapGestureRecognizer) {
if imageScrollView.zoomScale == imageScrollView.minimumZoomScale {
imageScrollView.zoom(to: zoomRectangle(scale: imageScrollView.maximumZoomScale, center: sender.location(in: sender.view)), animated: true)
} else {
imageScrollView.setZoomScale(imageScrollView.minimumZoomScale, animated: true)
}
}
// MARK: - Zoom IN / OUT Helper Methods Begin
private func setMinZoomScaleForImageSize(_ imageSize: CGSize) {
let widthScale = view.frame.width / imageSize.width
let heightScale = view.frame.height / imageSize.height
let minScale = min(widthScale, heightScale)
// Scale the image down to fit in the view
imageScrollView.minimumZoomScale = minScale
imageScrollView.zoomScale = minScale
// Set the image frame size after scaling down
let imageWidth = imageSize.width * minScale
let imageHeight = imageSize.height * minScale
let newImageFrame = CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight)
finalImage.frame = newImageFrame
centerImage()
}
private func centerImage() {
let imageViewSize = finalImage.frame.size
let scrollViewSize = view.frame.size
let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
imageScrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
}
private func zoomRectangle(scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
zoomRect.size.height = finalImage.frame.size.height / scale
zoomRect.size.width = finalImage.frame.size.width / scale
zoomRect.origin.x = center.x - (center.x * imageScrollView.zoomScale)
zoomRect.origin.y = center.y - (center.y * imageScrollView.zoomScale)
return zoomRect
}
}