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
    }
}

}

1

There are 1 best solutions below

0
Pavel Gubarev On

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.