Is it using UICollectionView or UIScrollView?
If using UICollectionView then do we have any sample code to have that zoom in and zoom out.
My requirement is to create a freeform drawing board on which sticky notes with text and GIF is added randomly with pan & zoom gestures.
I have tried with UICollectionView and created a custom UICollectionViewLayout but I am not able to get the desired effect.
Expected result with zoom in and zoom out gestures.
I have used the below custom collection view layout.
final public class CustomCollectionViewLayout: UICollectionViewLayout, UIGestureRecognizerDelegate {
private var currentScale: CGFloat = 1.0
private var zoomEnabled: Bool = true
private var pinchGestureRecognizer: UIPinchGestureRecognizer?
@objc public var itemSize: CGFloat = 100 {
didSet {
invalidateLayout()
}
}
@objc public var spacing: CGFloat = 0 {
didSet {
invalidateLayout()
}
}
private var _minScale: CGFloat = 0.2 {
didSet {
invalidateLayout()
}
}
@objc public var minScale: CGFloat {
get {
return _minScale
}
set {
_minScale = min(max(newValue, 0), 1)
}
}
private var _nextItemScale: CGFloat = 0.4 {
didSet {
invalidateLayout()
}
}
@objc public var nextItemScale: CGFloat {
get {
return _nextItemScale
}
set {
_nextItemScale = min(max(newValue, 0), 1)
}
}
@objc public func centeredOffsetForItem(indexPath: IndexPath) -> CGPoint {
guard let collectionView = self.collectionView else {
return .zero
}
guard attributes.indices.contains(indexPath.item) else {
return .zero
}
let attr = attributes[indexPath.item]
return CGPoint(
x: attr.center.x - collectionView.bounds.width * 0.5,
y: attr.center.y - collectionView.bounds.height * 0.5
)
}
@objc public private(set) var centeredIndexPath: IndexPath?
private var attributes = [UICollectionViewLayoutAttributes]()
private var layers = 1
public override var collectionViewContentSize: CGSize {
guard let collectionView = self.collectionView else {
return .zero
}
let size = CGFloat(layers) * (itemSize + spacing) * 2 - (itemSize + spacing)
let inset = collectionView.contentInset
return CGSize(width: size + collectionView.bounds.width + inset.left + inset.right,
height: size + collectionView.bounds.height + inset.top + inset.bottom)
}
public override class var layoutAttributesClass: AnyClass {
return UICollectionViewLayoutAttributes.self
}
@objc private func handlePinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .changed {
if gestureRecognizer.scale > 1.0 {
zoomIn()
} else {
zoomOut()
}
gestureRecognizer.scale = 1.0
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
public override func prepare() {
super.prepare()
if pinchGestureRecognizer == nil {
pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
pinchGestureRecognizer?.delegate = self
collectionView?.addGestureRecognizer(pinchGestureRecognizer!)
}
guard let collectionView = self.collectionView else {
return
}
let N = collectionView.numberOfItems(inSection: 0)
if attributes.count == N {
return
}
if N == 0 {
attributes.removeAll()
centeredIndexPath = nil
return
}
let center = CGPoint.zero
var i = 0
var layer = 0
attributes.removeAll()
centeredIndexPath = nil//IndexPath(item: 0, section: 0)
while i < N {
if layer == 0 {
let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
attr.size = CGSize(width: itemSize, height: itemSize)
attr.center = center
attributes.append(attr)
i += 1
} else {
let radius = CGFloat(layer) * (itemSize + spacing)
let hexagon = Multagon(6, center: center, radius: radius)
for j in 0 ..< layer {
let vertexes = hexagon.midVertex(slice: layer, slideIndex: j)
for vertex in vertexes {
let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
attr.size = CGSize(width: itemSize, height: itemSize)
attr.center = vertex
attributes.append(attr)
i += 1
if i >= N {
break
}
}
if i >= N {
break
}
}
}
layer += 1
}
layers = max(layer, 1)
// move all to center
let size = CGFloat(layers) * (itemSize + spacing) * 2 - (itemSize + spacing)
let inset = collectionView.contentInset
attributes.forEach { attr in
attr.center = CGPoint(
x: attr.center.x + size * 0.5 + inset.left + collectionView.bounds.width * 0.5,
y: attr.center.y + size * 0.5 + inset.top + collectionView.bounds.height * 0.5
)
}
}
@objc public func setZoomEnabled(_ enabled: Bool) {
zoomEnabled = enabled
invalidateLayout()
}
@objc public func zoomIn() {
currentScale += 0.1
currentScale = max(currentScale, 1.0)
invalidateLayout()
}
@objc public func zoomOut() {
currentScale -= 0.1
currentScale = max(currentScale, 0.1)
invalidateLayout()
}
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView else {
return nil
}
let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
let result = attributes.filter { rect.intersects($0.frame) }
result.forEach { attr in
let distance = CGPoint.distance(center, attr.center)
var scale = 1 - (1 - nextItemScale) * distance / itemSize // 0.8 is scale at 1 itemsize distance
scale = min(max(scale, minScale), 1)
if zoomEnabled {
scale *= currentScale
}
attr.transform = CGAffineTransform(scaleX: scale, y: scale)
}
return result
}
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
//auto snap to center of item
public override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
return proposedContentOffset
}
let proposedCenter = CGPoint(x: proposedContentOffset.x + collectionView.bounds.width * 0.5,
y: proposedContentOffset.y + collectionView.bounds.height * 0.5)
let closestAttr = attributes.min { (attr1, attr2) -> Bool in
return CGPoint.distance(attr1.center, proposedCenter) < CGPoint.distance(attr2.center, proposedCenter)
}
if let attr = closestAttr {
centeredIndexPath = attr.indexPath
let expectedOffset = CGPoint(x: attr.center.x - proposedCenter.x + proposedContentOffset.x,
y: attr.center.y - proposedCenter.y + proposedContentOffset.y)
return expectedOffset
} else {
return proposedContentOffset
}
}
}
I believe both are not necessary. You can just use UIView, add your sticky notes as subviews and change the scale of your UIView to zoom and change the position of your UIView to pan.