Search code examples
iosswiftuiviewsubviewtinder

Not able to remove custom UIView from SuperView


This is extremely odd. I am trying to remove the view from superview when I drag the view to either left or right. If the view doesn't contain any subviews then I am easily able to remove the view from the superView by using this card.removeFromSuperview() - however, what I have noticed is that if add two views as subviews inside the card view then I am not able to remove it from superView and the entire thing goes bezerk.

Here is the card view class:

class MainSwipeCardView: UIView {
//MARK: - Properties
var swipeView = UIView()
var shadowView = UIView()

var text: String?
var label = UILabel()
var bgColor : UIColor? {
    didSet {
        swipeView.backgroundColor = bgColor
    }
}


var cardsarraydata : CardDataModel? {
    didSet {
        bgColor = cardsarraydata?.backgroundColor
        label.text = cardsarraydata?.title
    }
}

var delegate : CardDelegate?

//MARK :- Init
override init(frame: CGRect) {
    super.init(frame: .zero)
    backgroundColor = .clear

    configureShadowView()
    configureSwipeView()
    configureLabelView()
    addPanGestureOnCards() 

}

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

//MARK: - Configurations
func configureShadowView() {
    shadowView.backgroundColor = .clear
    shadowView.layer.shadowColor = UIColor.black.cgColor
    shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
    shadowView.layer.shadowOpacity = 0.8
    shadowView.layer.shadowRadius = 4.0
    addSubview(shadowView)

    shadowView.translatesAutoresizingMaskIntoConstraints = false
    shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
    shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}

func configureSwipeView() {
    swipeView.layer.cornerRadius = 15
    swipeView.clipsToBounds = true
    shadowView.addSubview(swipeView)

    swipeView.translatesAutoresizingMaskIntoConstraints = false
    swipeView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
    swipeView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    swipeView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    swipeView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}

func configureLabelView() {
    swipeView.addSubview(label)
    label.backgroundColor = .white
    label.textColor = .black
    label.textAlignment = .center
    label.font = UIFont.systemFont(ofSize: 18)
    label.translatesAutoresizingMaskIntoConstraints = false
    label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
    label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    label.heightAnchor.constraint(equalToConstant: 85).isActive = true

}

func addPanGestureOnCards() {
    self.isUserInteractionEnabled = true
    addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)))
}

//MARK: - Handlers
@objc func handlePanGesture(sender: UIPanGestureRecognizer){
    let card = sender.view as! MainSwipeCardView
    let point = sender.translation(in: self)
    let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
    card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)

    switch sender.state {
    case .ended:
        if (card.center.x) > 400 {
            delegate?.swipeDidEnd(on: card)

            UIView.animate(withDuration: 0.2) {
                card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
                card.alpha = 0
                self.layoutIfNeeded()
            }
            return
        }else if card.center.x < -115 {
            delegate?.swipeDidEnd(on: card)

            UIView.animate(withDuration: 0.2) {
                card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
                card.alpha = 0
                self.layoutIfNeeded()
            }
            return
        }
        UIView.animate(withDuration: 0.2) {
            card.transform = .identity
            card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
            self.layoutIfNeeded()
        }

    default:
        break
    }

}

In this subclass I have two UIViews, I am adding the views sequentially. Then on swipeView I am adding the text and label and background color. This is how the cards look like: enter image description here

I am also using a UIPanInteraction on it and so if I drag it to left or right, then I call the delegate which removes the entire MainSwipeCardView from the container view. In doing so this is what happens: enter image description here

It keeps adding more and more in the background even though this is what I am calling in the delegate function:

 func swipeDidEnd(on card: MainSwipeCardView) {
    card.removeFromSuperview()
    print(visibleCards.count)
}

The visibleCards is essentially an array of subviews in the container view. It should decrease so for example from 3 -> 2 -> 1; but it increases in non linear way ( not able to really get a relationship out of it)

The most confusing thing is that I am actually able to run this whole code just fine if I donot add the SwipeView and shadowView properties inside the custom view and just use the customView itself to house the label and the backgroundColor. When I add these two properties, then this whole thing seem to go haywire.

Please any kind of help will be extremely appreciated. Thanks!

ContainerView code is as follows:

class SwipeCardContainerView: UIView, CardDelegate {

//MARK: - Properties
var numberOfCards: Int = 0
var remainingCards: Int = 0
var cardsView : [MainSwipeCardView] = []
var numberOfAllowedCard: Int = 3

let horizontalInset: CGFloat = 8.0
let verticalInset: CGFloat = 8.0

var visibleCards : [MainSwipeCardView] {
    return subviews as? [MainSwipeCardView] ?? []
}

var datasource : CardDataSource? {
    didSet {
        loadData()
    }
}
override init(frame: CGRect) {
    super.init(frame: .zero)
    backgroundColor = .clear

}

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


// MARK: - Configuration
func loadData() {
    guard let datasource = datasource else { return }
    numberOfCards = datasource.numberOfCards()
    remainingCards = numberOfCards

    for i in 0..<min(numberOfCards,numberOfAllowedCard) {
           addCardView(at: i, card: datasource.createCard(at: i))
    }
 setNeedsLayout()

}

func addCardView(at index: Int, card: MainSwipeCardView) {
    card.delegate = self
    addCardFrame(index: index, cardView: card)
    cardsView.append(card)
    insertSubview(card, at: 0)
    remainingCards -= 1
}

func addCardFrame(index: Int, cardView: MainSwipeCardView){
    cardsView.append(cardView)

    var cardViewFrame = bounds
    let horizontalInset = (CGFloat(index) * self.horizontalInset)
    let verticalInset = CGFloat(index) * self.verticalInset

    cardViewFrame.size.width -= 2 * horizontalInset
    cardViewFrame.origin.x += horizontalInset
    cardViewFrame.origin.y += verticalInset
    cardView.frame = cardViewFrame
}

// Delegate Method
func swipeDidEnd(on card: MainSwipeCardView) {
    card.removeFromSuperview()
    print(visibleCards.count)
}

Main ViewController Code:

class ViewController: UIViewController {

//MARK: - Properties
var stackContainer : SwipeCardContainerView!
var cardDataArray : [CardDataModel] = [CardDataModel(backgroundColor: .orange, title: "Hello"),
                                       CardDataModel(backgroundColor: .red, title: "Red"),
                                       CardDataModel(backgroundColor: .blue, title: "Blue"),
                                       CardDataModel(backgroundColor: .orange, title: "Orange")]

//MARK: - Init
override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
    stackContainer = SwipeCardContainerView()
    view.addSubview(stackContainer)
    configureSwipeContainerView()
    stackContainer.translatesAutoresizingMaskIntoConstraints = false
}

//MARK : - Configurations
func configureSwipeContainerView() {
      stackContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    stackContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true
      stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
      stackContainer.heightAnchor.constraint(equalToConstant: 350).isActive = true
}

override func viewDidLayoutSubviews() {
    stackContainer.datasource = self
}



//MARK : - Handlers


}

extension ViewController : CardDataSource {
func numberOfCards() -> Int {
    return cardDataArray.count
}

func createCard(at index: Int) -> MainSwipeCardView {
    let card = MainSwipeCardView()
    card.cardsarraydata = cardDataArray[index]
    return card
}

func emptyCard() -> UIView? {
    return nil
}


}

Solution

  • I've investigated the problem. First issue is in the ViewController:

        override func viewDidLayoutSubviews() {
            stackContainer.datasource = self
        }
    

    Just remove this code. In each layout you set datasource... and loadData... this is incorrect approach, also super.viewDidLayoutSubviews() is missing...

    And also stackContainer.datasource = self:

        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
            stackContainer = SwipeCardContainerView()
            view.addSubview(stackContainer)
            configureSwipeContainerView()
            stackContainer.translatesAutoresizingMaskIntoConstraints = false
            stackContainer.datasource = self
    

    Second issue is in func loadData(), just replace it with

        func loadData() {
            guard let datasource = datasource else { return }
            setNeedsLayout()
            layoutIfNeeded()
            numberOfCards = datasource.numberOfCards()
            remainingCards = numberOfCards
    
            for i in 0..<min(numberOfCards,numberOfAllowedCard) {
                addCardView(at: i, card: datasource.createCard(at: i))
    
            }
        }
    

    or find better solution with layout of SwipeCardContainerView