티스토리 뷰

프로그래밍/iOS

Delegate를 확장해보자

hanweeee 2020. 11. 17. 14:05

Swift에서 각종 기본컨트롤러를 이용하다보면 Delegate를 확장하고싶을때가 있다.

예를들면..

UITableView의 Delegate는 UITableViewDelegate라는 Protocol이고, UITableView는 UIScrollView를 상속받는다.

 

UITableViewDelegate는 UIScrollViewDelegate라는 프로토콜을 따르며, 거기에 자신만의 요구하는 함수 프로토콜을 가지고있다.

 

그렇다면

저 UITableView처럼 UITableView를 상속받는 뷰컨트롤러를 만들고 UITableViewDelegate를 따르는 프로토콜을 만든 후 delegate를 그 프로토콜로 바꿔주면 되는거 아닌가?!?!

 

import Foundation
import UIKit

protocol MyTableViewDelegate: UITableViewDelegate {
    func myFunc()
}

class MyTableView: UITableView {
    weak var delegate: MyTableViewDelegate?
}

요렇게...

 

응 ~ 안돼 ~

... 오버라이드 할 수 없다.

 

 

결국 오픈소스를 뒤져봤음..

 

 

방법1.

1.UIView를 만들고 2.오버라이딩하길 원했던 뷰를 addSubView 해주고 3.오토레이아웃으로 UIView의 크기와 같게만들어준다. 4. append를 원하는 protocol을 따르는 프로토콜을 만들고 UIView의 delegate를 해당 프로토콜로 만들어준다.

 

실제로 위 로직대로 내가 구현해놓은 클래스

//
//  HWTableViewVer2.swift
//  MyTableViewTest
//
//  Created by hanwe lee on 2020/09/29.
//  Copyright © 2020 hanwe. All rights reserved.
//
import UIKit
import SkeletonView
import SnapKit

@objc protocol HWTableViewDelegate:class {
    @objc optional func hwTableView(_ hwTableVIew: HWTableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
    @objc optional func hwTableView(_ hwTableVIew: HWTableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
    @objc optional func hwTableView(_ hwTableVIew: HWTableView, didSelectRowAt indexPath: IndexPath)
    @objc optional func scrollViewDidScroll(_ scrollView: UIScrollView)
    @objc optional func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
    @objc optional func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
    @objc optional func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
    @objc optional func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
    @objc optional func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
    @objc optional func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView)
    @objc optional func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool
    @objc optional func scrollViewDidScrollToTop(_ scrollView: UIScrollView)
    
    @objc optional func callNextPage(_ scrollView:UIScrollView)
}

@objc protocol HWTableViewDatasource:class {
    func hwTableView(_ hwTableView: HWTableView, numberOfRowsInSection section: Int) -> Int
    func hwTableView(_ hwTableView: HWTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    func hwTableViewSekeletonViewCellIdentifier(_ hwTableView: HWTableView) -> String
    func hwTableViewSekeletonViewHeight(_ hwTableView: HWTableView) -> CGFloat
    @objc optional func hwTableViewSekeletonViewCount(_ hwTableView: HWTableView) -> Int
    @objc optional func hwTableView(_ hwtableView: HWTableView, heightForRowAt indexPath:IndexPath) -> CGFloat
    @objc optional func numberOfSections(in hwtableView: HWTableView) -> Int
    @objc optional func hwTableView(_ hwtableView: HWTableView, viewForHeaderInSection section: Int) -> UIView?
    @objc optional func hwTableView(_ hwtableView: HWTableView, heightForHeaderInSection section: Int) -> CGFloat
    @objc optional func hwTableView(_ hwtableView: HWTableView, viewForFooterInSection section: Int) -> UIView?
    @objc optional func hwTableView(_ hwtableView: HWTableView, heightForFooterInSection section: Int) -> CGFloat
    
}

class HWTableView: UIView {
    
    //MARK: public property
    
    public weak var delegate:HWTableViewDelegate?
    public weak var dataSource:HWTableViewDatasource?
    
    public lazy var tableView:UITableView = UITableView(frame: self.bounds)
    public lazy var separatorStyle:UITableViewCell.SeparatorStyle = self.tableView.separatorStyle {
        didSet {
            self.tableView.separatorStyle = self.separatorStyle
        }
    }
    
    public var callNextPageBeforeOffset:CGFloat = 150
    
    public var minimumSkeletonSecond:CGFloat = 0.5
    
    //MARK: private property
    private var isShowDisplayAnimation:Bool = true
    private var isShowingSkeletonView:Bool = false
    private var isOverMinimumSkeletionTimer:Bool = true
    private let defaultCellHeight:CGFloat = 100
    private var numberOfRows:UInt = 0
    private var noResultView:UIView?
    private var minimumSkeletionTimer: Timer?
    
    //MARK: lifeCycle
    
    override func awakeFromNib() {
        super.awakeFromNib()
        initUI()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
    }
    
    //MARK: private func
    
    private func initUI() {
        self.tableView.backgroundColor = .clear
        self.addSubview(self.tableView)
        self.tableView.snp.makeConstraints{ (make) in
            make.leading.equalTo(self.snp.leading).offset(0)
            make.trailing.equalTo(self.snp.trailing).offset(0)
            make.top.equalTo(self.snp.top).offset(0)
            make.bottom.equalTo(self.snp.bottom).offset(0)
        }
        self.tableView.delegate = self
        self.tableView.dataSource = self
        self.isSkeletonable = true
    }
    
    private func getSkeletonCellBestCount(cellHeight:CGFloat) -> Int {
        var result:Int = 0
        result = Int(self.bounds.height/cellHeight)
        if self.bounds.height.truncatingRemainder(dividingBy: cellHeight) != 0 {
            result += 1
        }
        return result
    }
    
    @objc private func minimumSkeletionTimerCallback() {
        self.isOverMinimumSkeletionTimer = true
    }
    
    //MARK: public func
    public func showSkeletonHW() {
        self.isOverMinimumSkeletionTimer = false
        DispatchQueue.main.async { [weak self] in
            if self?.minimumSkeletionTimer?.isValid ?? false {
                self?.minimumSkeletionTimer?.invalidate()
            }
            self?.minimumSkeletionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(self!.minimumSkeletonSecond), target: self!, selector: #selector(self!.minimumSkeletionTimerCallback), userInfo: nil, repeats: false)
            self?.isShowDisplayAnimation = false
            self?.tableView.isSkeletonable = true
            self?.showAnimatedGradientSkeleton()
            self?.startSkeletonAnimation()
            self?.isShowingSkeletonView = true
        }
        
    }

    public func hideSkeletonHW() {
        if self.isOverMinimumSkeletionTimer {
            DispatchQueue.main.async { [weak self] in
                self?.stopSkeletonAnimation()
                self?.hideSkeleton()
                self?.tableView.reloadData() //리로드를 안해주면 데이터가 이상하게 set된다 ㅡㅡ; skeletonview 버그인듯
                self?.isShowingSkeletonView = false
                self?.isShowDisplayAnimation = true
            }
        }
        else {
            DispatchQueue.global(qos: .default).async { [weak self] in
                usleep(3 * 100 * 1000)
                DispatchQueue.main.async { [weak self] in
                    self?.hideSkeletonHW()
                }
            }
        }
    }
    
    public func addNoResultView(_ view:UIView) {
        self.noResultView = view
        guard let subView = self.noResultView else { return }
        self.addSubview(subView)
        subView.superview?.bringSubviewToFront(subView)
        subView.snp.makeConstraints{ (make) in
            make.leading.equalTo(self.snp.leading).offset(0)
            make.trailing.equalTo(self.snp.trailing).offset(0)
            make.top.equalTo(self.snp.top).offset(0)
            make.bottom.equalTo(self.snp.bottom).offset(0)
        }
        subView.isHidden = true
    }
    
    public func removeNoResultView() {
        self.noResultView = nil
    }
    
    public func showNoResultView(completion:(() -> ())?) {
        DispatchQueue.main.async { [weak self] in
            self?.noResultView?.isHidden = false
            self?.isShowDisplayAnimation = true
            completion?()
        }
    }
    
    public func hideNoResultView(completion:(() -> ())?) {
        DispatchQueue.main.async { [weak self] in
            self?.noResultView?.isHidden = true
            self?.isShowDisplayAnimation = true
            completion?()
        }
        
    }
    
    //MARK: public func for tableView
    
    public func register(_ nib: UINib?, forCellReuseIdentifier identifier: String) {
        self.tableView.register(nib, forCellReuseIdentifier: identifier)
    }
    
    public func reloadData() {
        self.tableView.reloadData()
    }
    
    public func dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell {
        return self.tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
    }

}

extension HWTableView:UITableViewDelegate {
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if !self.isShowingSkeletonView && self.isShowDisplayAnimation {
            cell.transform = CGAffineTransform(translationX: 0, y: 100 * 1.0)
            cell.alpha = 0
            UIView.animate(
                withDuration: 0.5,
                delay: 0 * Double(indexPath.row),
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
                    cell.alpha = 1
                })
            
        }
        self.delegate?.hwTableView?(self, willDisplay: cell, forRowAt: indexPath)
    }
    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if !self.isShowingSkeletonView && self.isShowDisplayAnimation {
            self.isShowDisplayAnimation = false
        }
        self.delegate?.hwTableView?(self, didEndDisplaying: cell, forRowAt: indexPath)
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.delegate?.hwTableView?(self, didSelectRowAt: indexPath)
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        self.delegate?.scrollViewDidScroll?(scrollView)
        
        let offset = scrollView.contentOffset;
        let bounds = scrollView.bounds;
        let size = scrollView.contentSize;
        let inset = scrollView.contentInset;
        let y = offset.y + bounds.size.height - inset.bottom;
        let h = size.height;
        if y + self.callNextPageBeforeOffset >= h {
            self.delegate?.callNextPage?(scrollView)
        }
    }
    
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        self.delegate?.scrollViewDidEndScrollingAnimation?(scrollView)
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        self.delegate?.scrollViewWillBeginDragging?(scrollView)
    }
    
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        self.delegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        self.delegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        self.delegate?.scrollViewDidEndDecelerating?(scrollView)
    }
    
    func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
        self.delegate?.scrollViewWillBeginDecelerating?(scrollView)
    }
    
    func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
        return self.delegate?.scrollViewShouldScrollToTop?(scrollView) ?? false
    }
    
    func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
        self.delegate?.scrollViewDidScrollToTop?(scrollView)
    }
    
}

extension HWTableView:SkeletonTableViewDataSource {
    func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
        return self.dataSource?.hwTableViewSekeletonViewCellIdentifier(self) ?? ""
    }

    func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let cellHight:CGFloat = self.dataSource?.hwTableViewSekeletonViewHeight(self) ?? 0
        return self.dataSource?.hwTableViewSekeletonViewCount?(self) ?? self.getSkeletonCellBestCount(cellHeight: cellHight)
    }
}

extension HWTableView:UITableViewDataSource {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.dataSource?.numberOfSections?(in: self) ?? 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let ds = self.dataSource  else { return 0 }
        self.numberOfRows = UInt(ds.hwTableView(self, numberOfRowsInSection: section))
        return Int(self.numberOfRows)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = self.dataSource!.hwTableView(self, cellForRowAt: indexPath)
//        cell.hideSkeleton()
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return self.dataSource?.hwTableView?(self, heightForRowAt: indexPath) ?? self.defaultCellHeight
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return self.dataSource?.hwTableView?(self, viewForHeaderInSection: section) ?? nil
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return self.dataSource?.hwTableView?(self, heightForHeaderInSection: section) ?? 0
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return self.dataSource?.hwTableView?(self, viewForFooterInSection: section) ?? nil
    }

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return self.dataSource?.hwTableView?(self, heightForFooterInSection: section) ?? 0
    }
}

github.com/HanweeeeLee/commonLib/blob/master/HWTableView.swift

 

HanweeeeLee/commonLib

자주쓰는 모듈 모음. Contribute to HanweeeeLee/commonLib development by creating an account on GitHub.

github.com

사실 이렇게 만들어버리면 UITableView의 모든 delegate들과 datasource를 전부 넘겨줘야하기 때문에 
좋은방법은 아닌것같다....

 

 

방법2. 

//
//  ViewController.swift
//  AppendDelegateExample
//
//  Created by hanwe lee on 2020/11/17.
//

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var myTextView: MyTestView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myTextView.delegate = self
    }
}

extension ViewController: MyTestViewProtocol {
    func changeMyValue(value: UInt) {
        print("change value:\(value)")
    }
    
    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        print("begin editing")
        return true
    }
    
    func textViewDidChange(_ textView: UITextView) {
        print("textViewDidChange")
        self.myTextView.value += 1
    }
    
}

protocol MyTestViewProtocol:UITextViewDelegate {
    func changeMyValue(value: UInt)
}

class MyTestView: UITextView {
    
    var value:UInt = 0 {
        willSet {
            guard newValue != self.value else {
                return
            }
            if let delegate = delegate as? MyTestViewProtocol {
                delegate.changeMyValue(value: newValue)
            }
        }
    }
}

이런식으로 작성하면 의도한대로 작동하긴 한다..

 

 

UITableView처럼 UIScrollView를 상속받고 delegate또한 UIScrollViewDelegateProtocol를 따르는 UITableViewDelegate로 사용하는 방법은 모르겠다.............

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함