Author: TracDV

  • Custom navigation trainsition iOS

    Custom navigation trainsition iOS

    Là một Developer iOS thì ai cũng quen với UINavigationController và chúng ta thường quen với các animation default. Nhưng với một số thiết kế đặc biệt chúng ta cần phải custom animation cho view controller transition.

    Để custom view controller transition bạn cần hiểu về UIViewControllerAnimatedTransitioning

    I. Overview UIViewControllerAnimatedTransitioning

    UIViewControllerAnimatedTransitioning là một protocol cho phép bạn implement các animation cho custom view controller transition

    Để implement protocol này thì có 2 việc ta cần phải làm

    • Xác định duration cho animation bằng func
    func transitionDuration( using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
    • Thực hiện animation traisition cho custom view controller
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
    • UIViewControllerContextTransitioning bao gồm tất cả thông tin về transitioning như: toView, fromView, containerView…

    Như vậy để custom view controller transition nó sẽ gói gọn trong func animateTransition

    II. Sử dụng UIViewControllerAnimatedTransitioning cho custom view controller transition

    Ở đây tôi sẽ hướng dẫn các bạn thực hiện custom animation push và pop thành animation của present(push -> bottom to top và pop -> top to bottom)

    1. Tạo enum cho NavigationTransitionStyle

    enum NavigationTransitionStyle {
        case nomal
        case bottom
    }
    • nomal: transition mặc định
    • bottom: custom animation transition từ bottom to top cho push và ngược lại cho pop

    2. Tạo class implement UIViewControllerAnimatedTransitioning

    Để hỗ trợ cho pop và push ta cần define các variable như sau:

    class CustomNavigationAnimationTransition: NSObject, UIViewControllerAnimatedTransitioning {
        var popStyle: Bool = false
           var navigationStyle: NavigationTransitionStyle = .nomal
    }
    • popStyle: flag để xác định là pop hay push
    • navigationStyle: xác định animation dạng nomal hay bottom

    2.1 Cài đặt duration cho animation

        func transitionDuration( using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.5
        }

    2.2 Cài đặt method animationTransition cho view controller

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    }
    • Điều hướng nếu là pop thì chúng ta sẽ xử lý animation pop ở method animatePop
    if popStyle {
        animatePop(using: transitionContext)
        return
    }
    • Thực hiện get các variable cần thiết để animation view controller: fromView, toView, frameTransition, frameOffset
    let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
    let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    let frameTransition = transitionContext.finalFrame(for: toView)
    var frameOffset = frameTransition.offsetBy(dx: 0, dy: frameTransition.height)
    if navigationStyle == .nomal {
         frameOffset = frameTransition.offsetBy(dx: frameTransition.width, dy: 0)
    }
    • Set frame cho toView và insert toView, fromView vào trong containerView
    toView.view.frame = frameOffset
    transitionContext.containerView.insertSubview(toView.view, aboveSubview: fromView.view)
    • Thực hiện animation bottom to top cho push
    UIView.animate( withDuration: transitionDuration(using: transitionContext),
                    animations: {
                        toView.view.frame = frameTransition
                    },
                    completion: {_ in
                        transitionContext.completeTransition(true)
                    })

    2.3 Cài đặt method animatePop

    Tương tự như ở method animationTransition ở đây ta sẽ bỏ bước set frame cho toView và thực hiện animation cho fromView

    func animatePop(using transitionContext: UIViewControllerContextTransitioning) {
    
            let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
            let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    
            let frameTransition = transitionContext.initialFrame(for: fromView)
            var frameOffset = frameTransition.offsetBy(dx: 0, dy: frameTransition.height)
            if navigationStyle == .nomal {
                frameOffset = frameTransition.offsetBy(dx: frameTransition.width, dy: 0)
            }
            transitionContext.containerView.insertSubview(toView.view, belowSubview: fromView.view)
    
            UIView.animate( withDuration: transitionDuration(using: transitionContext),
                            animations: {
                                fromView.view.frame = frameOffset
                            },
                            completion: {_ in
                                transitionContext.completeTransition(true)
                            })
    }

    3. Sử dụng CustomNavigationAnimationTransition trong view controller

    • Khỏi tạo variable custom animation bên trên
    private let navigationAnimationTransition = CustomNavigationAnimationTransition()
    • Sau đó config navigationStyle cho view controller và set delegate cho navigation controller
    navigationAnimationTransition.navigationStyle = .bottom
    navigationController?.delegate = self
    • Implement UIViewControllerTransitioningDelegate trong view controller
    extension ViewController: UIViewControllerTransitioningDelegate {
        func navigationController(
            _ navigationController: UINavigationController,
            animationControllerFor operation: UINavigationController.Operation,
            from fromVC: UIViewController,
            to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            navigationAnimationTransition.popStyle = (operation == .pop)
            return navigationAnimationTransition
    
        }
    }

    Như vậy chúng ta vừa mới hoàn thành custom view controller transition. Ngoài cách tạo animation như trên thì bạn có thể làm bất cứ dạng animation nào mà bạn muốn.

    Tài liệu tham khảo: https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

  • UICollectionView Custom Layout Tutorial

    UICollectionView Custom Layout Tutorial

    Xây dựng UICollection View Custom Layout và làm thế nào để lưu cache và dynamically size cell.

    UICollection view được giới thiệu từ bản iOS 6 và nó đã là UI Control mà các developer hay dùng nhất.

    UICollection view có thể hỗ trợ các loại hiển thị khác nhau và đặc biệt là tính năng dynamically size cell hỗ trợ người dùng hiển thị những phần mô tả ngắn mà đầy đủ nội dung cần truyền tải nhất mà không làm thay đổi thiết kế của mình. Như vậy thì UICollection view được sử dụng nhiều ở các app dạng social network, news,…,.

    Ở bài viết này tôi sẽ hướng dẫn các bạn

    • Custom layout
    • Xử lý dynamically cell
    • Những điểm cần lưu ý

    Tạo custom UICollection View Layout

    Bạn có thể tạo một chế độ hiển thị cho UICollection view theo cách riêng bằng cách tuỳ chỉnh layout của bạn.
    Collection view layout là lớp con của UICollectionViewLayout, nó xác đinh mọi thuộc tính trong chế độ hiển thị trên UICollectionView của bạn.
    Các thuộc tính của UICollectionViewLayout là intances của UICollectionViewLayoutAttributes. Chúng chứa các thuộc tính của từng mục trong chế độ hiển thị của bạn.

    Bắt đầu

    • Trước tiên ta cần chuẩn bị một project sample đã được implement UICollection View. UI cần custom là dạng cột và dynamic height.
    • Tạo custom collection view layout
    • Cách dùng Custom Collection View Layout

    Tạo Custom Collection View Layout

    Tạo file CustomCollectionViewLayout sub class là UICollectionViewLayout

    Tiếp theo config cho collection view sử dụng custom layout.
    Mở file view chứa collection view

    Mở thanh công cụ Attributes inspector. Chọn Custom trên thuộc tính Layout

    Như vậy chúng ta đã config thành công cho collection view sử dụng custom layout.

    Như vậy để hiển thị chúng cần thực hiện những gì

    Như vậy class CustomCollectionViewLayout vừa tạo cần thực hiện những function sau:

    • collectionViewContentSize: function này trả về chiều rộng chiều cao của toàn bộ collection view contents.
    • prepare: UIKit sẽ call function này trước khi có các thay đổi về UI. Đây là nơi mà bạn có thể chuẩn bị các tính toán liên quan đến chế độ xem, kích thước, vị trí ,…,.
    • layoutAttributesForElements(in:): Trong function này bạn trả về các thuộc tính bố cục cho tất cả item dưới dạng array UICollectionViewLayoutAttributes.
    • layoutAttributesForItem(at:): Function này cung cấp thông tin bố trí riêng cho từng item. Bạn cần override lại nó để có thể hiển thị cho từng item mà bạn mong muốn.

    Tính toán bố cục

    Với layout dạng cột và bạn cần height nó được dãn tự động thì bạn cần tính toán lại bố cục của layout

    Mở file CustomCollectionViewLayout và tạo protocol cho nó

    protocol CustomCollectionViewLayoutDelegate: AnyObject {
      func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat
    }

    Trong delegate bạn mới tạo 01 function yêu cầu trả về height, bạn sẽ implement function này trong Controller của chúng.

    Tiếp đến bạn cần thực hiện implement class CustomCollectionViewLayout

    // 1
        weak var delegate: CustomCollectionViewLayoutDelegate?
    
        // 2
        private let numberOfColumns = 2
        private let cellPadding: CGFloat = 6
    
        // 3
        private var cache: [UICollectionViewLayoutAttributes] = []
    
        // 4
        private var contentHeight: CGFloat = 0
    
        private var contentWidth: CGFloat {
          guard let collectionView = collectionView else {
            return 0
          }
          let insets = collectionView.contentInset
          return collectionView.bounds.width - (insets.left + insets.right)
        }
        // 5
        override var collectionViewContentSize: CGSize {
          return CGSize(width: contentWidth, height: contentHeight)
        }

    Bạn có thể thấy ở đây:

    1. delegate
    2. Số columns bạn cần hiển thị
    3. cache để lưu trữ các thuộc tính hiển thị.
    4. 02 Thuộc tính width và height của contents
    5. Trả về kích thước của collection view’s content. Bạn sử dụng cả 2 thuộc tính contentWidth và contentHeight để tính kích thước

    Để tính toán cách hiển thị, mời bạn xem sơ đồ sau:

    Bạn sẽ tính toán dựa trên số cột bạn muốn thiển thị và vị trí của mục trước đó trong cùng một cộ. Để tính toán thì bạn dùng xOffset cho cột và yOffset cho vị trí.

    Tiếp tục bạn cần override lại function prepare

    override func prepare() {
          // 1
          guard
            cache.isEmpty,
            let collectionView = collectionView
            else {
              return
          }
          // 2
          let columnWidth = contentWidth / CGFloat(numberOfColumns)
          var xOffset: [CGFloat] = []
          for column in 0..<numberOfColumns {
            xOffset.append(CGFloat(column) * columnWidth)
          }
          var column = 0
          var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns)
            
          // 3
          for item in 0..<collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: item, section: 0)
              
            // 4
            let photoHeight = delegate?.collectionView(
              collectionView,
              heightForPhotoAtIndexPath: indexPath) ?? 180
            let height = cellPadding * 2 + photoHeight
            let frame = CGRect(x: xOffset[column],
                               y: yOffset[column],
                               width: columnWidth,
                               height: height)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
              
            // 5
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
              
            // 6
            contentHeight = max(contentHeight, frame.maxY)
            yOffset[column] = yOffset[column] + height
              
            column = column < (numberOfColumns - 1) ? (column + 1) : 0
          }
        }

    Ở function này bạn có thể thấy tính width có thể cho số columns mà bạn muốn hiển thị, và tính height cho từng item và được lưu lại các thuộc tính ở cache.

    Tiếp theo bạn override lại function layoutAttributesForElements(in rect: CGRect) function này sẽ được gọi sau khi function prepare() kết thúc

        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
          var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
          
          // Loop through the cache and look for items in the rect
          for attributes in cache {
            if attributes.frame.intersects(rect) {
              visibleLayoutAttributes.append(attributes)
            }
          }
          return visibleLayoutAttributes
        }

    Ở function này, bạn duyệt lại bộ cache xem frame của từng item có giao nhau với collection view không.

    Cuối cùng bạn cần phải override lại function layoutAttributesForItem

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
          return cache[indexPath.item]
        }

    Ở đây sẽ return về các item theo các indexPath.

    Kết nối với ViewController

    Bạn mở View Controller chứa nó và thêm

    extension CustomCollectionViewController: CustomCollectionViewLayoutDelegate {
      func collectionView(
        _ collectionView: UICollectionView,
        heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
        return photos[indexPath.item].image.size.height
      }
    }

    Ở đây tôi đang dùng dynamic height. Height sẽ tự động theo height của image.

    Tiếp theo trong function viewDidLoad() bạn thêm

    if let layout = collectionView.collectionViewLayout as? CustomCollectionViewLayout {
                layout.delegate = self
            }

    Như vậy bạn đã custom thành công layout trên UICollectionView.

    Những điều cần lưu ý

    Tới đây bạn sẽ gặp bug là bạn reload data cho collection view khi tăng số lượng item hiển thị.
    Bạn debug bạn sẽ thấy mọi quá trình bạn làm đã hoàn tất, từ việc get data và add vào list của data source cũng đã đầy đủ, nhưng chỉ thiếu một điều là collection view chỉ chạy lại với số lượng item lần đầu tiên.

    Vậy vấn đề ở đây là gì?? tại sao reloadData lại không được và không có bất cứ một dòng log error nào ra ??

    Vấn đề ở đây là contents size của collection view không được update, nên collection view không thể hiển thị thêm bất cứ item nào.

    Vậy bug ở function nào?

    Function nào tính toán lại contents size?

    Là func prepare() nó là function được gọi để tính toán size. Ngay ở bước đầu tiên của func prepare()

    // 1
          guard
            cache.isEmpty,
            let collectionView = collectionView
            else {
              return
          }

    Đoạn code trên nó chỉ tính toán lại contents size lần đầu tiên.

    Vậy bước 01 của func prepare() bạn cần làm:

          // 1
          guard
    //        cache.isEmpty,
            let collectionView = collectionView
            else {
              return
          }

    Như vậy bạn đã custom được layout của UICollectionViewLayout và hoàn toàn có thể update data source của UICollection view mà không bị bug.

    Source code tham khảo: https://github.com/TracDV/CustomUICollectionViewLayout

    Tham khảo nguồn: https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest