探索之旅:代理原理

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看388 回复回复8 收藏收藏1 分享淘帖 转播转播 分享分享 微信
查看: 388|回复: 8
收起左侧

探索之旅:代理原理

[复制链接]
Ding 发表于 2016-10-24 09:18:53 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑
本帖最后由 Ding 于 2016-11-28 10:36 编辑

那时候因为项目需要,自学了iOS,从此爱上cocoa的封装到位。尤其初遇UITableView时,心中惊叹:这个容器视图,简直神器。

[Swift] 纯文本查看 复制代码
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)


这些神奇的函数,到底是怎么工作起来的?

想想UITableView,就是个scrollView,上边有一个个垂直排列的子视图,为使用方便,给它们定义一些共通的属性和方法,划归为UITableViewCell这个类。
怎么计算scroll的contentOffset,怎么重用并展示那些cell?……嗯,这些工作封装起来,使用的人只需要写UITableViewDataSource和UITableViewDelegate里的那些方法就好了。那些方法是具体使用场景决定的,怎么定义,交给使用者即可。我封装整个流程、计算的细节,和你在代理方法里定义的内容合起来,就是完整的列表了。什么时候调用代理方法?既然我知道整个流程,调用它们的时机当然是明确的。

想归想,还是迷迷糊糊。直到有一天,自己封装了一个自定义视图容器,一切都了然了。

忘了从哪个版本开始,我司来了这么个需求,要把商家信息用卡片的形式展示,就像下边这样:

探索之旅:代理原理

探索之旅:代理原理 - 敏捷大拇指 - 探索之旅:代理原理


GitHub上找了找,有类似的工程,但是代码耦合在UIViewController里,为什么不像UITableView那样封装一下呢?

先做一下整体规划:假设总共卡片有100张,但是可见的卡片就3张,当划走最上面的卡片,下边的卡片移动上来,最上边的卡片作为最下边的卡片复用,也就是说只需要在内存中生成3张卡片。灵活起见,可见卡片有几张,可以让用户定义,这个可以用代理,但是用属性更简洁直观。总共有多少张卡片、每张卡片长什么样,这就可以借鉴UITableView,用代理模式了。对了,我们这个容器视图的名字就叫SwipeableCards吧。

我们来定义数据源方法:

[Swift] 纯文本查看 复制代码
public protocol SwipeableCardsDataSource {
    func numberOfTotalCards(in cards: SwipeableCards) -> Int
    func view(for cards:SwipeableCards, index:Int, reusingView: UIView?) -> UIView
}


哎呀,太像tableView的作风了。要解释一下的是第二个方法里的reusingView:每张卡片其实都是同一类型的View,重用的时候,只是重新给View填充数据,所以SwipeableCards内部只需要一个UIView指针,在恰当的时机保存一张卡片,在另外的时机置为nil即可。而UITableView是怎么做的?它有个dequeReusableCellWithIdentifier的方法,想想就明白了:因为考虑到一个tableView里可能有好几种cell,显然一个指针是不够的,应该根据identifier来找到对应的cell,那些可回收利用的cell可以放在字典里(健就是identifier,值就是cell的指针),也可以放在NSChache里(类似字典,专做缓存,比字典好用),然后就可以在合适的时机queue(放进队列)那些cell,用户也可dequeue(从队列里取出来)那些cell来用。SwipeableCards也可以考虑这样扩展,一个容器里多种卡片,但是实际使用不多,先不了,就一个reusingView指针这样的方法,足以,使用的时候大概就是这样:
[Swift] 纯文本查看 复制代码
func view(for cards: SwipeableCards, index: Int, reusingView: UIView?) -> UIView {
        var label: UILabel? = reusingView as? UILabel
        if label == nil {
            let labelFrame = CGRect(x: 0, y: 0, width: cardsWidth.constant - 30, height: cardsHeight.constant - 20)
            label = UILabel(frame: labelFrame)
            label!.textAlignment = .center
            label!.layer.cornerRadius = 5
        }
        label!.text = String(cardsData[index])
        label!.layer.backgroundColor = Color.random.cgColor
        return label!
    }

再定义几个可选的代理方法,方便使用:
[Swift] 纯文本查看 复制代码
public protocol SwipeableCardsDelegate {
    func cards(_ cards: SwipeableCards, beforeSwipingItemAt index: Int)
    func cards(_ cards: SwipeableCards, didRemovedItemAt index: Int)
    func cards(_ cards: SwipeableCards, didLeftRemovedItemAt index: Int)
    func cards(_ cards: SwipeableCards, didRightRemovedItemAt index: Int)
}
extension SwipeableCardsDelegate {// This extesion makes the methods optionnal for use~
    func cards(_ cards: SwipeableCards, beforeSwipingItemAt index: Int) {}
    func cards(_ cards: SwipeableCards, didRemovedItemAt index: Int) {}
    func cards(_ cards: SwipeableCards, didLeftRemovedItemAt index: Int) {}
    func cards(_ cards: SwipeableCards, didRightRemovedItemAt index: Int) {}
}

extension SwipeableCardsDelegate是干嘛的?注释里也说了,是为了让SwipeableCardsDelegate那几个方法变成可选的~当然,定义代理的可选方法也有其他几种,让代理继承NSObject然后在方法前写上optional,或者在代理前写上@objc 再在方法前写上optional。我们这里使用代理扩展的方式。
现在来定义对外可见的属性和方法:
[Swift] 纯文本查看 复制代码
public class SwipeableCards: UIView {
    /// DataSource
    public var dataSource: SwipeableCardsDataSource? {
        didSet {
            reloadData()
        }
    }
    /// Delegate
    public var delegate: SwipeableCardsDelegate?
    /// Default is true
    public var showedCyclically = true {
        didSet {
            reloadData()
        }
    }
    /// We will creat this number of views, so not too many; default is 3
    public var numberOfVisibleItems = 3 {
        didSet {
            reloadData()
        }
    }
    /// Offset for the next card to the current card, (it will decide the cards appearance, the top card is on top-left, top, or bottom-right and so on; default is (5, 5)
    public var offset: (horizon: CGFloat, vertical: CGFloat) = (5, 5) {
        didSet {
            reloadData()
        }
    }
    /// If there is only one card, maybe you don't want to swipe it
    public var swipEnabled = true {
        didSet {
            panGestureRecognizer.isEnabled = swipEnabled
        }
    }
    /// The first visible card on top
    public var topCard: UIView? {
        get {
            return visibleCards.first
        }
    }
    
    // Mark - init
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }
    public override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }
    /**
     Refresh to show data source
     */
    public func reloadData() {
        currentIndex = 0
        reusingView = nil
        visibleCards.removeAll()
        if let totalNumber = dataSource?.numberOfTotalCards(in: self) {
            let visibleNumber = numberOfVisibleItems > totalNumber ? totalNumber : numberOfVisibleItems
            for i in 0..<visibleNumber {
                if let card = dataSource?.view(for: self, index: i, reusingView: reusingView) {
                    visibleCards.append(card)
                }
            }
        }
        layoutCards()
    }
}

注释明确,就不细解释了。说一下关乎代理的私有方法。

初始化的时候有个setUp,只干了一件事:就是给容器添加了一个滑动手势。

下边是私有方法,主要的实现在这里,是手势事件处理和reloadData后的操作:

[Swift] 纯文本查看 复制代码
// MARK: - Private
private extension SwipeableCards {
    func setUp() {
        self.addGestureRecognizer(panGestureRecognizer)
    }
    func layoutCards() {
        let count = visibleCards.count
        guard count > 0 else {
            return
        }
        subviews.forEach { view in
            view.removeFromSuperview()
        }
        layoutIfNeeded()
        let width = frame.size.width
        let height = frame.size.height
        if let lastCard = visibleCards.last {
            let cardWidth = lastCard.frame.size.width
            let cardHeight = lastCard.frame.size.height
            if let totalNumber = dataSource?.numberOfTotalCards(in: self) {
                let visibleNumber = numberOfVisibleItems > totalNumber ? totalNumber : numberOfVisibleItems
                var firstCardX = (width - cardWidth - CGFloat(visibleNumber - 1) * fabs(offset.horizon)) * 0.5
                if offset.horizon < 0 {
                    firstCardX += CGFloat(visibleNumber - 1) * fabs(offset.horizon)
                }
                var firstCardY = (height - cardHeight - CGFloat(visibleNumber - 1) * fabs(offset.vertical)) * 0.5
                if offset.vertical < 0 {
                    firstCardY += CGFloat(visibleNumber - 1) * fabs(offset.vertical)
                }
                
                UIView.animate(withDuration: 0.08) {
                    for i in 0..<count {
                        let index = count - 1 - i   //add cards form back to front
                        let card = self.visibleCards[index]
                        let size = card.frame.size
                        card.frame = CGRect(x: firstCardX + CGFloat(index) * self.offset.horizon, y: firstCardY + CGFloat(index) * self.offset.vertical, width: size.width, height: size.height)
                        self.addSubview(card)
                    }
                }
            }
        }
    }
    @objc func dragAction(_ gestureRecognizer: UIPanGestureRecognizer) {
        guard visibleCards.count > 0 else {
            return
        }
        if let totalNumber = dataSource?.numberOfTotalCards(in: self) {
            if currentIndex > totalNumber - 1 {
                currentIndex = 0
            }
        }
        if swipeEnded {
            swipeEnded = false
            delegate?.cards(self, beforeSwipingItemAt: currentIndex)
        }
        if let firstCard = visibleCards.first {
            xFromCenter = gestureRecognizer.translation(in: firstCard).x  // positive for right swipe, negative for left
            yFromCenter = gestureRecognizer.translation(in: firstCard).y  // positive for up, negative for down
            switch gestureRecognizer.state {
            case .began:
                originalPoint = firstCard.center
            case .changed:
                let rotationStrength: CGFloat = min(xFromCenter / Const.rotationStrength, Const.rotationMax)
                let rotationAngel = Const.rotationAngle * rotationStrength
                let scale = max(1.0 - fabs(rotationStrength) / Const.scaleStrength, Const.scaleMax)
                firstCard.center = CGPoint(x: originalPoint.x + xFromCenter, y: originalPoint.y + yFromCenter)
                let transform = CGAffineTransform(rotationAngle: rotationAngel)
                let scaleTransform = transform.scaledBy(x: scale, y: scale)
                firstCard.transform = scaleTransform
            case .ended:
                aflerSwipedAction(firstCard)
            default:
                break
            }
        }
    }
    func aflerSwipedAction(_ card: UIView) {
        if xFromCenter > Const.actionMargin {
            rightActionFor(card)
        } else if xFromCenter < -Const.actionMargin {
            leftActionFor(card)
        } else {
            self.swipeEnded = true
            UIView.animate(withDuration: 0.3) {
                card.center = self.originalPoint
                card.transform = CGAffineTransform(rotationAngle: 0)
            }
        }
        
    }
    func rightActionFor(_ card: UIView) {
        let finishPoint = CGPoint(x: 500, y: 2.0 * yFromCenter + originalPoint.y)
        UIView.animate(withDuration: 0.3, animations: {
            card.center = finishPoint
        }) { (Bool) in
            self.delegate?.cards(self, didRightRemovedItemAt: self.currentIndex)
            self.cardSwipedAction(card)
        }
    }
    func leftActionFor(_ card: UIView) {
        let finishPoint = CGPoint(x: -500, y: 2.0 * yFromCenter + originalPoint.y)
        UIView.animate(withDuration: 0.3, animations: {
            card.center = finishPoint
        }) { (Bool) in
            self.delegate?.cards(self, didLeftRemovedItemAt: self.currentIndex)
            self.cardSwipedAction(card)
        }
    }
    func cardSwipedAction(_ card: UIView) {
        swipeEnded = true
        card.transform = CGAffineTransform(rotationAngle: 0)
        card.center = originalPoint
        let cardFrame = card.frame
        reusingView = card
        visibleCards.removeFirst()
        card.removeFromSuperview()
        var newCard: UIView?
        if let totalNumber = dataSource?.numberOfTotalCards(in: self) {
            var newIndex = currentIndex + numberOfVisibleItems
            if newIndex < totalNumber {
                newCard = dataSource?.view(for: self, index: newIndex, reusingView: reusingView)
            } else {
                if showedCyclically {
                    if totalNumber==1 {
                        newIndex = 0
                    } else {
                        newIndex %= totalNumber
                    }
                    newCard = dataSource?.view(for: self, index: newIndex, reusingView: reusingView)
                }
            }
            if let card = newCard {
                card.frame = cardFrame
                visibleCards.append(card)
            }
            delegate?.cards(self, didRemovedItemAt: currentIndex)
            currentIndex += 1
            layoutCards()
        }
    }
}


到底是什么时候调用了用户写好的代理方法,是不是明了了?

更多细节,可以在《Swift小巧库》里找到GitHub链接SwipeableCards看看。

最后感慨一下:爱起来,用起来,做起来,路就通畅了。



写着写着,慢慢就多起来,应阿牛哥之请,弄了个海淘贴:探索之旅。可以去订阅。
























都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

本帖被以下淘专辑推荐:

Anewczs 发表于 2016-10-24 09:33:42 | 显示全部楼层
32个赞!
 楼主| Ding 发表于 2016-10-24 09:42:54 | 显示全部楼层

还没写完。等我写完~
Anewczs 发表于 2016-10-24 09:46:25 | 显示全部楼层
Ding 发表于 2016-10-24 09:42
还没写完。等我写完~

不好意思,已经推了。哈哈

我也是推完一细看才发现没写完。没关系,继续更呗。
网络处男 发表于 2016-10-24 11:17:32 | 显示全部楼层
stackview吧?
 楼主| Ding 发表于 2016-10-24 11:25:35 | 显示全部楼层

这个和UIStackView没什么关系,不知道你指什么
相见不如怀念 发表于 2016-10-24 21:35:13 | 显示全部楼层
不明觉厉
移动 发表于 2016-10-25 08:19:03 | 显示全部楼层
赞一个!
 楼主| Ding 发表于 2016-10-25 09:34:27 | 显示全部楼层

看来还是没写明了。要通俗易懂,难~
还是要爱起来、用起来,用过了才明了~

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
关闭

站长推荐 上一条 /3 下一条

热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表