facebookarchive / AsyncDisplayKit

Smooth asynchronous user interfaces for iOS apps.
http://asyncdisplaykit.org
Other
13.4k stars 2.2k forks source link

Customized ASMultiplexImageNode with additional subnodes and layers Terminated in ASTableView #2004

Closed iAlirezaKML closed 7 years ago

iAlirezaKML commented 8 years ago

I've created a subclass of ASMultiplexImageNode which has a button to control download and show progress. When using it inside a simple view (node), it works fine. But when using it inside ASCellNode, confronting Assertion failure in ... ASDisplayNode.mm:520 then Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: ''.

I've commented the working code inside of simple node in ViewController class.

\ Also There is another problem (I think), multiplexImageNode:didStartDownloadOfImageWithIdentifier: get called once for thumb image and twice for the original image.

Here's the code:

import UIKit
import AsyncDisplayKit

struct ImageURLs {
    var origURL: NSURL?
    var thumbURL: NSURL?
}

private enum ImageType: String {
    case Thumb = "thumb"
    case Original = "orig"

    static let allRawValues = [Original.rawValue, Thumb.rawValue]
}

class ImageDownloadButton: ASButtonNode {
    private let circlePathLayer = CAShapeLayer()
    private let rotateAnimation: CABasicAnimation
    private let rotateAnimationKey = "indeterminateAnimation"
    private var rotateAnimating = false

    private var resumeAction = {}
    private var cancelAction = {}

    private var progress: CGFloat {
        set {
            if newValue > 1 {
                circlePathLayer.strokeEnd = 1
            } else if newValue < 0 {
                circlePathLayer.strokeEnd = 0
            } else {
                circlePathLayer.strokeEnd = newValue
            }
        }
        get {
            return circlePathLayer.strokeEnd
        }
    }

    override init() {
        rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.byValue = 2*M_PI
        rotateAnimation.duration = 5
        rotateAnimation.repeatCount = Float.infinity
        super.init()
        setImage(UIImage(named: "ic-dl"), forState: .Normal)
        setImage(UIImage(named: "ic-cancel-dl"), forState: .Selected)
        setBackgroundImage(UIImage(named: "ic-dl-back"), forState: .Normal)
        setBackgroundImage(UIImage(named: "ic-cancel-dl-back"), forState: .Selected)
        circlePathLayer.frame = bounds
        circlePathLayer.lineWidth = 2.5
        circlePathLayer.lineCap = kCALineCapRound
        circlePathLayer.fillColor = UIColor.clearColor().CGColor
        circlePathLayer.strokeColor = UIColor.whiteColor().CGColor
        layer.addSublayer(circlePathLayer)
        progress = 0
        selected = true
        addTarget(self, action: #selector(ImageDownloadButton.tapAction), forControlEvents: .TouchUpInside)
    }

    override func layoutDidFinish() {
        super.layoutDidFinish()
        let margin: CGFloat = 6
        circlePathLayer.frame = bounds
        var pathFrame = bounds
        pathFrame.origin.x += margin/2
        pathFrame.origin.y += margin/2
        pathFrame.size.width -= margin
        pathFrame.size.height -= margin
        circlePathLayer.path = UIBezierPath(ovalInRect: pathFrame).CGPath
    }

    func tapAction() {
        if selected {
            cancelAction()
        } else {
            resumeAction()
        }
    }

    private func stopRotate() {
        if rotateAnimating {
            circlePathLayer.removeAnimationForKey(rotateAnimationKey)
            rotateAnimating = false
        }
    }

    private func startRotate() {
        if !rotateAnimating {
            rotateAnimating = true
            circlePathLayer.strokeEnd = 0.66
            circlePathLayer.addAnimation(rotateAnimation, forKey: rotateAnimationKey)
        }
    }
}

class NetworkImageButtonNode: ASMultiplexImageNode {
    private let downloadButton = ImageDownloadButton()
    private let blurNode = ASDisplayNode(viewBlock: {
        let blurEffect = UIBlurEffect(style: .Light)
        let blurView = UIVisualEffectView(effect: blurEffect)
        return blurView
    })

    var urls = ImageURLs() {
        didSet {
            reloadImageIdentifierSources()
        }
    }

    init() {
        super.init(cache: nil, downloader: ASBasicImageDownloader.sharedImageDownloader())
        contentMode = .ScaleAspectFit
        downloadsIntermediateImages = true
        imageIdentifiers = ImageType.allRawValues
        backgroundColor = UIColor(white: 0.8, alpha: 0.6)
        dataSource = self
        delegate = self

        downloadButton.cancelAction = {
            self.clearFetchedData()
            self.downloadButton.selected = false
        }
        downloadButton.resumeAction = {
            self.reloadImageIdentifierSources()
            self.downloadButton.selected = true
        }

        addSubnode(blurNode)
        addSubnode(downloadButton)

        layoutSpecBlock = { _ in
            let layout = ASStaticLayoutSpec(children: [self.downloadButton])
            let backSpec = ASBackgroundLayoutSpec(child: layout, background: self.blurNode)
            let insetSpec = ASRelativeLayoutSpec(horizontalPosition: .Center, verticalPosition: .Center, sizingOption:  .MinimumWidth, child: backSpec)
            let sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(50, 50))
            self.downloadButton.sizeRange = sizeRange
            return insetSpec
        }
    }

    override func layoutDidFinish() {
        super.layoutDidFinish()
        blurNode.bounds = bounds
    }
}

extension NetworkImageButtonNode: ASMultiplexImageNodeDataSource {
    func multiplexImageNode(imageNode: ASMultiplexImageNode, URLForImageIdentifier imageIdentifier: ASImageIdentifier) ->   NSURL? {
        guard let identifier = imageIdentifier as? String, type = ImageType(rawValue: identifier) else { return nil }
        switch type {
        case .Original: return urls.origURL
        case .Thumb: return urls.thumbURL
        }
    }
}

extension NetworkImageButtonNode: ASMultiplexImageNodeDelegate {
    func multiplexImageNode(imageNode: ASMultiplexImageNode, didStartDownloadOfImageWithIdentifier imageIdentifier:     AnyObject) {
        downloadButton.startRotate()
        print(imageIdentifier)
    }

    func multiplexImageNode(imageNode: ASMultiplexImageNode, didUpdateDownloadProgress downloadProgress: CGFloat,   forImageWithIdentifier imageIdentifier: ASImageIdentifier) {
        if let imageIdentifier = imageIdentifier as? String where imageIdentifier == ImageType.Original.rawValue {
            downloadButton.progress = downloadProgress
        }
    }

    func multiplexImageNodeDidFinishDisplay(imageNode: ASMultiplexImageNode) {
        if let displayedImageIdentifier = displayedImageIdentifier as? String where displayedImageIdentifier ==     ImageType.Original.rawValue {
            downloadButton.stopRotate()
            UIView.animateWithDuration(0.15, animations: {
                self.downloadButton.alpha = 0
                (self.blurNode.view as? UIVisualEffectView)?.effect = nil
                }, completion: { _ in
                    self.downloadButton.hidden = true
                    self.blurNode.hidden = true
            })
        }
    }
}

class ViewController: ASViewController {
//  let imageNode: NetworkImageButtonNode

    init() {
//      imageNode = NetworkImageButtonNode()
//      imageNode.userInteractionEnabled = true
        let tableNode = ASTableNode(style: .Grouped)
        super.init(node: tableNode)
        tableNode.dataSource = self
//      node.addSubnode(imageNode)
        node.backgroundColor = UIColor.whiteColor()

//      node.layoutSpecBlock = { node, range -> ASLayoutSpec in
//          let insets = UIEdgeInsetsMake(40, 40, 40, 40)
//          let insetSpec = ASInsetLayoutSpec(insets: insets, child: self.imageNode)
//          return insetSpec
//      }

//      performSelector(#selector(ViewController.igniteDownload), withObject: nil, afterDelay: 2)
    }

//  func igniteDownload() {
//      let origURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/9/94/Sanzio_01.jpg")
//      let thumbURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/94/    Sanzio_01.jpg/30px-Sanzio_01.jpg")
//      imageNode.urls = ImageURLs(origURL: origURL, thumbURL: thumbURL)
//  }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension ViewController: ASTableDataSource {
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(tableView: ASTableView, nodeBlockForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock {
        return {
            let node = ImageCellNode()
            return node
        }
    }
}

class ImageCellNode: ASCellNode {
    let imageNode = NetworkImageButtonNode()

    override init() {
        super.init()
        imageNode.userInteractionEnabled = true
        let origURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/9/94/Sanzio_01.jpg")
        let thumbURL = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/94/    Sanzio_01.jpg/30px-Sanzio_01.jpg")
        imageNode.urls = ImageURLs(origURL: origURL, thumbURL: thumbURL)
        addSubnode(imageNode)
    }

    override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec {
        let staticSpec = ASStaticLayoutSpec(children: [imageNode])
        let insets = UIEdgeInsetsMake(4, 8, 4, 8)
        let insetSpec = ASInsetLayoutSpec(insets: insets, child: staticSpec)
        imageNode.preferredFrameSize = CGSizeMake(constrainedSize.max.width, 200)
        return insetSpec
    }
}
hannahmbanana commented 8 years ago

@peymayesh: Could you create a sample project please? cmd + D on your project folder and then remove any unnecessary code.

iAlirezaKML commented 8 years ago

@hannahmbanana It's not a big deal, Just replace this code in application:didFinishLaunchingWithOptions: and put the attached icons in your Assets.xcassets.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    window = UIWindow(frame: UIScreen.mainScreen().bounds)
    window?.rootViewController = ViewController()
    window?.makeKeyAndVisible()

    return true
}

Assets.zip

iAlirezaKML commented 8 years ago

@hannahmbanana I've attached the exact sample project you wanted. I really appreciate if you can tell me what should I do. I'm playing with threads and GCD for two days, but I couldn't make it work :( NetworkImageButtonNode.zip

Adlai-Holler commented 7 years ago

@peymayesh Hi! Sorry this one slipped through the cracks.

I believe the issue here is that you're accessing self.layer inside ASDisplayNode.init() which is called on a background thread sometimes – such as when using ASCellNode. If you move all the CABasicAnimation and CALayer related code out from init and into didLoad (which is always run on the main thread) you will stop hitting that assertion.

If you have more questions, please check out our Slack which has a ton of helpful people working together http://asyncdisplaykit.org/slack . Cheers!