1. Hiển thị photo giống facebook

Nếu mọi ngươi dùng app facebook thì sẽ hay xem ảnh trên đó, khi ấn vào ảnh thì sẽ có 1 viewcontroller hiện lên kèm theo ảnh đó, chúng ta có thể zoom, double tap vào ảnh đó để nó có thể phóng to ra. Ngoài ra tùy vào kích thước của anh mà chúng ta sẽ thấy bức ảnh đc hiển thị theo chiều ngang hoặc dọc, việc này làm cho bức ảnh hiển thị lên đẹp nhất có thể, hôm nay chúng ta sẽ làm viewcontroller tương tự vậy

2. Các components dùng trong viewcontroller

2.1 UIImageView

Có thể nói đây sẽ là thành phần chính được sử dụng trong viewcontroller, nó cho phép chúng ta hiển thị bức ảnh trong viewcontroller. Có 1 điểm khác biệt là ở đây chúng ta có thể zoom được ảnh, để làm được điều này chúng ta sẽ phải đặt UIImageView vào trong 1 UIScrollView, chúng ta sẽ đặt tên nó là class ScalingImageView: UIScrollView

class ScalingImageView: UIScrollView {
}

class ScalingImageView: UIScrollView sẽ có 2 component chính là image chính là ảnh chúng ta cần hiển thị và 1 imageView để hiển thị ảnh đó

var image: UIImage? {
    didSet {
        if let image = image {
            self.updateWithImage(image)
        }
    }
}

lazy var imageView: UIImageView = {
    let view = UIImageView()
    view.contentMode = .scaleAspectFill
    view.clipsToBounds = true
    return view
}()

Vì trong ScalingImageView sẽ cho phép zoom ảnh nên chúng ta sẽ có 1 hàm tính toán khả năng zoom ảnh dựa vào kích thước của ảnh, nếu kích thước bức ảnh quá bé, bé hơn kích thước của ScalingImageView thì sẽ không cho zoom

fileprivate func updateZoomScaleWithImage(_ image: UIImage) {
    let scrollViewFrame = bounds
    let imageSize = image.size
    
    let widthScale = scrollViewFrame.width / imageSize.width
    let heightScale = scrollViewFrame.height / imageSize.height
    
    let minScale = min(widthScale, heightScale)
    
    self.minimumZoomScale = minScale
    self.maximumZoomScale = minScale * 4
    
    if (imageSize.height / imageSize.width) > (scrollViewFrame.height / scrollViewFrame.width) {
        self.maximumZoomScale = max(maximumZoomScale, widthScale)
    }
    
    self.zoomScale = minimumZoomScale
    self.panGestureRecognizer.isEnabled = false
}

2.2 Photo

Trong class này chúng ta có 2 component chính là image chúng ta cần hiển thị và 1 block updatedImage dùng để update image lên imageView sau khi nó đã download xong

protocol PhotoProtocol: class {
    var image: UIImage? { get set }
    var updatedImage: ((_ image: UIImage?) -> Void)? { get set }
}

class Photo: NSObject, PhotoProtocol {
    
}

Khi tạo 1 object photo chúng ta có thể gán trực tiếp image cho nó hoặc chỉ gán url, từ đó mỗi khi kích vào object đó PhotoViewController sẽ tiến hành download rồi cache lại nó để hiển thị.

2.3 Kingfisher

Vì ở đây chúng ta dùng cả url của anh để hiển thị cho nên cần 1 framework download -> cache lại ảnh rồi hiển thị khi cần thiết, Kingfisher sẽ làm việc đó cho chúng ta.
Khi init 1 object Photo chúng ta có thể gán trực tiếp image cần hiện thị vào nó thông qua property image hoặc gán url image để Kingfisher tự download và cache nó.

var image: UIImage? {
    didSet {
        self.updatedImage?(image)
    }
}

var updatedImage: ((UIImage?) -> Void)?

init(_ imageUrl: String) {
    super.init()
    
    if let url = URL(string: imageUrl) {
        let iamgeResouce = ImageResource(downloadURL: url)
        KingfisherManager.shared.retrieveImage(with: iamgeResouce, options: nil, progressBlock: nil, completionHandler: {[weak self](image, error, cacheType, imageURL) in
            guard let weakSelf = self else {
                return
            }
            
            if let image = image {
                weakSelf.image = image
            }
        })
    }
}

Mỗi khi chúng ta tạo 1 object Photo kèm theo url Kingfisher sẽ download image rồi cache lại nó rồi trả về thông qua property image

3. Tạo PhotoViewController

PhotoViewController sẽ có 5 component chính

@IBOutlet weak var topView: UIView!
@IBOutlet weak var bottomView: UIView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

lazy var scalingImageView: ScalingImageView = {
    let view = ScalingImageView(frame: self.view.bounds)
    view.delegate = self
    return view
}()

var photo: Photo?

Trong đó, topView sẽ chức các component nhỏ như button close, label information, avatar …, còn bottomView chức các thông tin như like, share, comment của image, scalingImageView chứa image chính của chúng ta. saclingImageView có thể init trực tiếp trên storyboard hoặc qua code, ở đây mình init nó qua code

fileprivate func initImageView() {
    self.scalingImageView.frame = self.view.bounds
    self.scalingImageView.image = self.photo?.image
    
    self.view.insertSubview(scalingImageView, belowSubview: self.topView)
    
    self.photo?.updatedImage = {[weak self] image in
        guard let weakSelf = self else { return }
        
        weakSelf.scalingImageView.image = image
        
        if image != nil {
            weakSelf.activityIndicator.stopAnimating()
        }
    }
    
    if self.photo?.image == nil {
        self.activityIndicator.startAnimating()
    }
}

Ở hàm initImageView chúng ta gọi thêm hàm updatedImage từ photo, để khi Kingfisher cache xong image nó sẽ gọi lại trong viewcontroller và gán image vào imageView, đồng thời stop animation của activity.
Ở PhotoViewController chúng ta init thêm 1 func doubleTap cho imageView

@objc fileprivate func  didDoubleTap(_ sender: UITapGestureRecognizer) {
    let scrollViewSize = self.scalingImageView.bounds.size
    var pointInView = sender.location(in: self.scalingImageView.imageView)
    var newZoomScale = min(scalingImageView.maximumZoomScale, self.scalingImageView.minimumZoomScale * 2)
    
    if let imageSize = scalingImageView.imageView.image?.size, (imageSize.height / imageSize.width) > (scrollViewSize.height / scrollViewSize.width) {
        pointInView.x = scalingImageView.imageView.bounds.width / 2
        let widthScale = scrollViewSize.width / imageSize.width
        newZoomScale = widthScale
    }
    
    let isZoomIn = (scalingImageView.zoomScale >= newZoomScale) || (abs(scalingImageView.zoomScale - newZoomScale) <= 0.01)
    
    if isZoomIn {
        newZoomScale = scalingImageView.minimumZoomScale
    }
    
    scalingImageView.isDirectionalLockEnabled = !isZoomIn
    
    let width = scrollViewSize.width / newZoomScale
    let height = scrollViewSize.height / newZoomScale
    let originX = pointInView.x - (width / 2)
    let originY = pointInView.y - (height / 2)
    
    let rectToZoomTo = CGRect(x: originX, y: originY, width: width, height: height)
    self.scalingImageView.zoom(to: rectToZoomTo, animated: true)
}

Hàm này sẽ có 2 chức năng, zoom in ảnh khi ở trạng thái chưa zoom và zoom out ảnh khi đang ở trạng thái zoom in.

4. Demo

https://github.com/pqhuy87it/MonthlyReport/tree/master/PhotoViewController