I've been trying to lean more about subclassing certain objects.
Now I've subclassed a UIView which has a PanGestureRecognizer for swiping left and right.
Can't seem to find the problem. It won't even move the UIView. I've tried looking the lifecycle of an UIView to set the isUserInteractionEnabled to true, but with no result. See the code below:
VC
import UIKit
class SwipeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addNewProfile()
}
private func addNewProfile() {
let swipeView = SwiperView(frame: CGRect(x: self.view.bounds.width / 2 - 150, y: self.view.bounds.height / 2 - 75, width: 300, height: 150))
swipeView.parentView = self.view
swipeView.delegate = self
swipeView.shadow = true
swipeView.isUserInteractionEnabled = true
swipeView.backgroundColor = UIColor.white
swipeView.alpha = 0.0
view.addSubview(swipeView)
UIView.animate(withDuration: 0.3, animations: {
swipeView.alpha = 1.0
}, completion: { (succeed) in
swipeView.isUserInteractionEnabled = true
})
}
}
//MARK: - ChosenSwipeResultDelegate
extension SwipeViewController: ChosenSwipeResultDelegate {
func pickedLeftSide() {
}
func pickedRightSide() {
}
}
SwiperView
import UIKit
protocol ChosenSwipeResultDelegate {
func pickedLeftSide()
func pickedRightSide()
}
#IBDesignable class SwiperView: UIView {
private var _shadow: Bool!
private var _parentView: UIView!
var delegate: ChosenSwipeResultDelegate?
var parentView: UIView {
set {
_parentView = newValue
}
get {
return _parentView
}
}
#IBInspectable var shadow: Bool {
get {
return layer.shadowOpacity > 0.0
}
set {
if newValue == true {
addShadow()
}
}
}
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
if shadow == false {
layer.masksToBounds = true
}
}
}
override func setNeedsLayout() {
super.setNeedsLayout()
isUserInteractionEnabled = true
}
override func awakeFromNib() {
super.awakeFromNib()
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
func dragging(gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: parentView)
let tinderView = gesture.view!
tinderView.center = CGPoint(x: parentView.bounds.width / 2 + translation.x, y: parentView.bounds.height / 2 + translation.y)
let xFromCenter = tinderView.center.x - parentView.bounds.width / 2
let scale = min(100 / abs(xFromCenter), 1)
var rotation = CGAffineTransform(rotationAngle: xFromCenter / 200)
let stretch = rotation.scaledBy(x: scale, y: scale)
tinderView.transform = stretch
if gesture.state == .ended {
if tinderView.center.x < 100 {
print("left")
UIView.animate(withDuration: 0.3, animations: {
tinderView.alpha = 0.0
}, completion: { (succeed) in
self.delegate?.pickedLeftSide()
})
} else if tinderView.center.x > parentView.bounds.width - 100 {
print("right")
UIView.animate(withDuration: 0.3, animations: {
tinderView.alpha = 0.0
}, completion: { (succeed) in
self.delegate?.pickedRightSide()
})
} else {
print("Not chosen")
rotation = CGAffineTransform(rotationAngle: 0)
let stretch = rotation.scaledBy(x: 1, y: 1)
tinderView.transform = stretch
tinderView.center = CGPoint(x: parentView.bounds.width / 2, y: parentView.bounds.height / 2)
}
}
}
private func addShadow(shadowColor: CGColor = UIColor.black.cgColor, shadowOffset: CGSize = CGSize(width: 1.0, height: 2.0), shadowOpacity: Float = 0.4, shadowRadius: CGFloat = 3.0) {
layer.shadowColor = shadowColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
}
}
add these code to your SwiperView see if it solves the problem
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
Related
I have a StackContainerView inside my main view controller called TodayPicksViewController. I am trying to programmatically set the StackContainerView to fill up the whole view controller side to side, with around 50 from top and bottom (just like a Tinder card).
However, as I try to implement constraints relative to safe area as follows(as other answers on StackOverflow suggest), turned out the StackContainerView doesn't show up at all. I don't know where the problem is.
Please advice.
Code of my main view controller, TodayPicksViewController:
class TodayPicksViewController: UIViewController {
//MARK: - Properties
var viewModelData = [CardsDataModel(bgColor: UIColor(red:0.96, green:0.81, blue:0.46, alpha:1.0), text: "Hamburger", image: "hamburger"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.64, blue:0.96, alpha:1.0), text: "Puppy", image: "puppy"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.63, blue:0.49, alpha:1.0), text: "Poop", image: "poop"),
CardsDataModel(bgColor: UIColor(red:0.69, green:0.52, blue:0.38, alpha:1.0), text: "Panda", image: "panda"),
CardsDataModel(bgColor: UIColor(red:0.90, green:0.99, blue:0.97, alpha:1.0), text: "Subway", image: "subway"),
CardsDataModel(bgColor: UIColor(red:0.83, green:0.82, blue:0.69, alpha:1.0), text: "Robot", image: "robot")]
var stackContainer : StackContainerView!
private let spinner = JGProgressHUD(style: .dark)
private var users = [[String: String]]()
private var results = [SearchResult]()
private var hasFetched = false
var divisor: CGFloat!
private let noResultsLabel: UILabel = {
let label = UILabel()
label.isHidden = true
label.text = "No Results"
label.textAlignment = .center
label.textColor = .green
label.font = .systemFont(ofSize: 21, weight: .medium)
return label
}()
override func loadView() {
view = UIView()
stackContainer = StackContainerView()
view.addSubview(stackContainer)
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(noResultsLabel)
configureStackContainer()
stackContainer.dataSource = self
}
#IBAction func panMatch(_ sender: UIPanGestureRecognizer) {
let match = sender.view!
let point = sender.translation(in: view)
let xFromCenter = match.center.x - view.center.x
print(xFromCenter)
match.center = CGPoint(x: view.center.x + point.x, y: view.center.y + point.y)
match.transform = CGAffineTransform(rotationAngle: xFromCenter/divisor)
if sender.state == UIGestureRecognizer.State.ended {
if match.center.x < 75 {
// Move off to the left side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x - 200, y: match.center.y + 75)
match.alpha = 0
})
return
} else if match.center.x > (view.frame.width - 75) {
// Move off to the right side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x + 200, y: match.center.y + 75)
match.alpha = 0
})
return
}
// resetCard()
}
}
private var loginObserver: NSObjectProtocol?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
validateAuth()
}
private func validateAuth() {
if FirebaseAuth.Auth.auth().currentUser == nil {
let vc = SignInViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: false)
}
}
#objc private func pageControlDidChange(_ sender: UIPageControl) {
let current = sender.currentPage
// scrollView.setContentOffset(CGPoint(x: CGFloat(current) * view.frame.size.width,
// y: 70), animated: true)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
//MARK: - Configurations
func configureStackContainer() {
stackContainer.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -60).isActive = true
// stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
// stackContainer.heightAnchor.constraint(equalToConstant: 400).isActive = true
stackContainer.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
stackContainer.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
stackContainer.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
stackContainer.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
func updateUI() {
if results.isEmpty {
noResultsLabel.isHidden = false
}
else {
noResultsLabel.isHidden = true
}
}
func calcAge(birthday: Date) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
// let birthdayDate = dateFormater.date(from: birthday)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthday, to: now, options: [])
let age = calcAge.year
return age!
}
extension TodayPicksViewController : SwipeCardsDataSource {
func numberOfCardsToShow() -> Int {
return viewModelData.count
}
func card(at index: Int) -> SwipeCardView {
let card = SwipeCardView()
card.dataSource = viewModelData[index]
return card
}
func emptyView() -> UIView? {
return nil
}
}
Probably doesn't matter, but here is my code for the StackContainerView:
class StackContainerView: UIView, SwipeCardsDelegate {
//MARK: - Properties
var numberOfCardsToShow: Int = 0
var cardsToBeVisible: Int = 3
var cardViews : [SwipeCardView] = []
var remainingcards: Int = 0
let horizontalInset: CGFloat = 10.0
let verticalInset: CGFloat = 10.0
var visibleCards: [SwipeCardView] {
return subviews as? [SwipeCardView] ?? []
}
var dataSource: SwipeCardsDataSource? {
didSet {
reloadData()
}
}
//MARK: - Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reloadData() {
removeAllCardViews()
guard let datasource = dataSource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCardsToShow = datasource.numberOfCardsToShow()
remainingcards = numberOfCardsToShow
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) {
addCardView(cardView: datasource.card(at: i), atIndex: i )
}
}
//MARK: - Configurations
private func addCardView(cardView: SwipeCardView, atIndex index: Int) {
cardView.delegate = self
addCardFrame(index: index, cardView: cardView)
cardViews.append(cardView)
insertSubview(cardView, at: 0)
remainingcards -= 1
}
func addCardFrame(index: Int, cardView: SwipeCardView) {
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
}
private func removeAllCardViews() {
for cardView in visibleCards {
cardView.removeFromSuperview()
}
cardViews = []
}
func swipeDidEnd(on view: SwipeCardView) {
guard let datasource = dataSource else { return }
view.removeFromSuperview()
if remainingcards > 0 {
let newIndex = datasource.numberOfCardsToShow() - remainingcards
addCardView(cardView: datasource.card(at: newIndex), atIndex: 2)
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}else {
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}
}
}
According to the apple developer doc for loadView(), they said "The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property." This might be the cause of the problem. I would recommend you to perform the view set up operations in viewDidLoad or other proper lifecycle methods. Based on my understanding, this line view = UIView() isn't necessary. In your configureStackContainer() func, you set the centerX and centerY anchor and then set the top, leading, trailing, bottom anchor again. This may also raise the constraint conflicts. I think you don't need to specify centerX and centerY anchor if you want to constraint with top, leading, trailing and bottom and vice versa. I hope this will be helpful.
I rewrote a custom activity indicator that was originally in an Objc file into Swift. The activity indicator appears on scene but the animation isn't occurring.
I need some help figuring out why the animation isn't occurring:
vc:
class ViewController: UIViewController {
fileprivate lazy var customActivityView: CustomActivityView = {
let customActivityView = CustomActivityView()
customActivityView.translatesAutoresizingMaskIntoConstraints = false
customActivityView.delegate = self
customActivityView.numberOfCircles = 3
customActivityView.radius = 20
customActivityView.internalSpacing = 3
customActivityView.startAnimating()
return customActivityView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
}
fileprivate func setAnchors() {
view.addSubview(customActivityView)
customActivityView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
customActivityView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
customActivityView.widthAnchor.constraint(equalToConstant: 100).isActive = true
customActivityView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
}
extension ViewController: CustomActivityViewDelegate {
func activityIndicatorView(activityIndicatorView: CustomActivityView, circleBackgroundColorAtIndex index: Int) -> UIColor {
let red = CGFloat(Double((arc4random() % 256)) / 255.0)
let green = CGFloat(Double((arc4random() % 256)) / 255.0)
let blue = CGFloat(Double((arc4random() % 256)) / 255.0)
let alpha: CGFloat = 1
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
Swift file:
import UIKit
protocol CustomActivityViewDelegate: class {
func activityIndicatorView(activityIndicatorView: CustomActivityView, circleBackgroundColorAtIndex index: Int) -> UIColor
}
class CustomActivityView: UIView {
var numberOfCircles: Int = 0
var internalSpacing: CGFloat = 0
var radius: CGFloat = 0
var delay: CGFloat = 0
var duration: CFTimeInterval = 0
var defaultColor = UIColor.systemPink
var isAnimating = false
weak var delegate: CustomActivityViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
setupDefaults()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupDefaults()
fatalError("init(coder:) has not been implemented")
}
func setupDefaults() {
self.translatesAutoresizingMaskIntoConstraints = false
numberOfCircles = 5
internalSpacing = 5
radius = 10
delay = 0.2
duration = 0.8
}
func createCircleWithRadius(radius: CGFloat, color: UIColor, positionX: CGFloat) -> UIView {
let circle = UIView(frame: CGRect(x: positionX, y: 0, width: radius * 2, height: radius * 2))
circle.backgroundColor = color
circle.layer.cornerRadius = radius
circle.translatesAutoresizingMaskIntoConstraints = false;
return circle
}
func createAnimationWithDuration(duration: CFTimeInterval, delay: CGFloat) -> CABasicAnimation {
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
anim.fromValue = 0.0
anim.toValue = 1.0
anim.autoreverses = true
anim.duration = duration
anim.isRemovedOnCompletion = false
anim.beginTime = CACurrentMediaTime()+Double(delay)
anim.repeatCount = .infinity
anim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
return anim;
}
func addCircles() {
for i in 0..<numberOfCircles {
var color: UIColor?
color = delegate?.activityIndicatorView(activityIndicatorView: self, circleBackgroundColorAtIndex: i)
let circle = createCircleWithRadius(radius: radius,
color: ((color == nil) ? self.defaultColor : color)!,
positionX: CGFloat(i) * ((2 * self.radius) + self.internalSpacing))
circle.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
circle.layer.add(self.createAnimationWithDuration(duration: self.duration,
delay: CGFloat(i) * self.delay), forKey: "scale")
self.addSubview(circle)
}
}
func removeCircles() {
self.subviews.forEach({ $0.removeFromSuperview() })
}
#objc func startAnimating() {
if !isAnimating {
addCircles()
self.isHidden = false
isAnimating = true
}
}
#objc func stopAnimating() {
if isAnimating {
removeCircles()
self.isHidden = true
isAnimating = false
}
}
func intrinsicContentSize() -> CGSize {
let width: CGFloat = (CGFloat(self.numberOfCircles) * ((2 * self.radius) + self.internalSpacing)) - self.internalSpacing
let height: CGFloat = radius * 2
return CGSize(width: width, height: height)
}
func setNumberOfCircles(numberOfCircles: Int) {
self.numberOfCircles = numberOfCircles
self.invalidateIntrinsicContentSize()
}
func setRadius(radius: CGFloat) {
self.radius = radius
self.invalidateIntrinsicContentSize()
}
func setInternalSpacing(internalSpacing: CGFloat) {
self.internalSpacing = internalSpacing
self.invalidateIntrinsicContentSize()
}
}
I used the wrong key path for the animation:
I used
// incorrect
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
but I should've used
// correct
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
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:
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:
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
}
}
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
I would like to create the stepper menu in IOS using swift, But I'm facing some issues. Here are the issues.
1) Portrait and landscape stepper menu is not propper.
2) How to set default step position with the method below method, It's working when button clicked. But I want to set when menu loads the first time.
self.stepView.setSelectedPosition(index: 2)
3) If it reached the position last, I would like to change the color for complete path parentPathRect.
4) Progress animation CABasicAnimation is not like the progress bar, I want to show the animation.
5) It should not remove the selected position color when changing the orientation.
As per my organization rules should not use third-party frameworks.
Can anyone help me with the solution? Or is there any alternative solution for this?
Here is my code:
import UIKit
class ViewController: UIViewController, StepMenuDelegate {
#IBOutlet weak var stepView: StepView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.stepView.delegate = self;
self.stepView.titles = ["1", "2", "3"]
self.stepView.lineWidth = 8
self.stepView.offSet = 8
self.stepView.setSelectedPosition(index: 2)
}
func didSelectItem(atIndex index: NSInteger) {
print(index)
}
}
protocol StepMenuDelegate {
func didSelectItem(atIndex index: NSInteger)
}
class StepView: UIView {
var delegate : StepMenuDelegate!
var titles: [String] = [] {
didSet(values) {
setup()
setupItems()
}
}
var lineWidth: CGFloat = 8 {
didSet(values) {
setup()
}
}
var offSet: CGFloat = 8 {
didSet(values) {
self.itemOffset = offSet * 4
setup()
}
}
private var selectedIndex : NSInteger!
private var itemOffset : CGFloat = 8 {
didSet (value) {
setup()
setupItems()
}
}
private var path : UIBezierPath!
var selectedLayer : CAShapeLayer!
private var parentPathRect : CGRect!
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
self.setup()
setupItems()
}
func setup() {
self.removeAllButtonsAndLayes()
let layer = CAShapeLayer()
self.parentPathRect = CGRect(origin: CGPoint(x: offSet, y: self.bounds.midY - (self.lineWidth/2) ), size: CGSize(width: self.bounds.width - (offSet * 2), height: lineWidth))
path = UIBezierPath(roundedRect: self.parentPathRect, cornerRadius: 2)
layer.path = path.cgPath
layer.fillColor = UIColor.orange.cgColor
layer.lineCap = .butt
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowOffset = CGSize(width: 1, height: 2)
layer.shadowOpacity = 0.1
layer.shadowRadius = 2
self.layer.addSublayer(layer)
}
func setupItems() {
removeAllButtonsAndLayes()
let itemRect = CGRect(x: self.itemOffset, y: 0, width: 34, height: 34)
let totalWidth = self.bounds.width
let itemWidth = totalWidth / CGFloat(self.titles.count);
for i in 0..<self.titles.count {
let button = UIButton()
var xPos: CGFloat = itemOffset
self.addSubview(button)
xPos += (CGFloat(i) * itemWidth);
xPos += itemOffset/3
button.translatesAutoresizingMaskIntoConstraints = false
button.leftAnchor.constraint(equalTo: self.leftAnchor, constant: xPos).isActive = true
button.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0).isActive = true
button.heightAnchor.constraint(equalToConstant: itemRect.height).isActive = true
button.widthAnchor.constraint(equalToConstant: itemRect.width).isActive = true
button.backgroundColor = UIColor.red
button.layer.zPosition = 1
button.layer.cornerRadius = itemRect.height/2
let name : String = self.titles[i]
button.tag = i
button.setTitle(name, for: .normal)
button.addTarget(self, action: #selector(selectedItemEvent(sender:)), for: .touchUpInside)
if self.selectedIndex != nil {
if button.tag == self.selectedIndex {
selectedItemEvent(sender: button)
}
}
}
}
#objc func selectedItemEvent(sender:UIButton) {
if self.selectedLayer != nil {
selectedLayer.removeFromSuperlayer()
}
self.delegate.didSelectItem(atIndex: sender.tag)
let fromRect = self.parentPathRect.origin
self.selectedLayer = CAShapeLayer()
let rect = CGRect(origin: fromRect, size: CGSize(width:sender.frame.origin.x - 4, height: 8))
let path = UIBezierPath(roundedRect: rect, cornerRadius: 4)
self.selectedLayer.path = path.cgPath
self.selectedLayer.lineCap = .round
self.selectedLayer.fillColor = UIColor.orange.cgColor
let animation = CABasicAnimation(keyPath: "fillColor")
animation.toValue = UIColor.blue.cgColor
animation.duration = 0.2
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.selectedLayer.add(animation, forKey: "fillColor")
self.layer.addSublayer(self.selectedLayer)
}
func removeAllButtonsAndLayes() {
for button in self.subviews {
if button is UIButton {
button.removeFromSuperview()
}
}
}
func setSelectedPosition(index:NSInteger) {
self.selectedIndex = index
}
}
Here I found One way to achieve the solution.!!
https://gist.github.com/DamodarGit/7f0f484708f60c996772ae28e5e1c615
Welcome to suggestions or code changes.!!
I'm a beginner of Swift 3 and I plan to write several Custom Control (IBDesignable) in XCode 8. When I write a custom text field, I want to add right placeholder to it. However, the right placeholder would only display successfully when this text field has no constraint. I don't know what happened to it.
I hope someone can help me fix this bug, thanks a lot.
import UIKit
#IBDesignable
class RMLDesignableUITextField: UITextField {
// MARK: PROPERTIES
#IBInspectable var insetX: CGFloat = 0
#IBInspectable var insetY: CGFloat = 0
#IBInspectable var placeholderColor: UIColor = UIColor.white {
didSet {
if let placeholder = self.placeholder {
let attributes = [NSForegroundColorAttributeName: placeholderColor]
attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes)
}
}
}
// MARK: Border
var bottomBorder = CALayer()
var rightBorder = CALayer()
var topBorder = CALayer()
var leftBorder = CALayer()
#IBInspectable var showsTopBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsBottomBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsLeftBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsRightBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear
#IBInspectable var borderWidth: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
}
}
#IBInspectable var rightPlaceholder: String = "" {
didSet {
rightPlaceholderLabel.text = rightPlaceholder
}
}
fileprivate var fakePlaceholderLabel: UILabel!
fileprivate var rightPlaceholderLabel: UILabel!
fileprivate var translateX: CGFloat!
{
get {
let attributes = [NSFontAttributeName: font!]
let rightPlaceholderTextSize = rightPlaceholderLabel.text!.size(attributes: attributes)
let rightPlaceholderTextWidth = rightPlaceholderTextSize.width
let translateX = textRect(forBounds: bounds).width - rightPlaceholderTextWidth
return translateX
}
}
// MARK: Initializers
override init(frame: CGRect) {
super.init(frame: frame)
// self.setNeedsDisplay()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
// self.setNeedsDisplay()
fakePlaceholderLabel = UILabel(frame: placeholderRect(forBounds: bounds))
fakePlaceholderLabel.font = font
fakePlaceholderLabel.text = placeholder
fakePlaceholderLabel.textColor = UIColor.lightGray
fakePlaceholderLabel.alpha = 0.0
rightPlaceholderLabel = UILabel(frame: placeholderRect(forBounds: bounds))
rightPlaceholderLabel.font = font
rightPlaceholderLabel.text = rightPlaceholder
rightPlaceholderLabel.textColor = UIColor.lightGray
rightPlaceholderLabel.alpha = 0.0
}
override func layoutSubviews() {
super.layoutSubviews()
addSubview(fakePlaceholderLabel)
addSubview(rightPlaceholderLabel)
setupSubviews()
setNeedsDisplay()
}
}
// MARK: - Lifecycle
extension RMLDesignableUITextField {
override func awakeFromNib() {
super.awakeFromNib()
}
override var intrinsicContentSize : CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: UIViewNoIntrinsicMetric)
}
override func prepareForInterfaceBuilder() {
//setupSubviews()
}
}
// MARK: - Delegate Methods
extension RMLDesignableUITextField {
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
}
// MARK: - UITextField Observing
extension RMLDesignableUITextField {
override internal func willMove(toSuperview newSuperview: UIView!) {
if newSuperview != nil {
NotificationCenter.default.addObserver(self, selector: #selector(RMLDesignableUITextField.didBeginEditing(_:)), name: NSNotification.Name.UITextFieldTextDidBeginEditing, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(RMLDesignableUITextField.didEndEditing(_:)), name: NSNotification.Name.UITextFieldTextDidEndEditing, object: self)
} else {
NotificationCenter.default.removeObserver(self)
}
}
func didBeginEditing(_ notification: Notification) {
placeholder = nil
if notification.object as! RMLDesignableUITextField === self{
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: .curveEaseIn, animations: { () -> Void in
if self.text!.isEmpty {
self.fakePlaceholderLabel.transform = self.fakePlaceholderLabel.transform.translatedBy(x: self.translateX, y: 0.0)
self.fakePlaceholderLabel.alpha = 0.0
self.rightPlaceholderLabel.transform = self.rightPlaceholderLabel.transform.translatedBy(x: self.translateX, y: 0.0)
self.rightPlaceholderLabel.alpha = 1.0
}
}, completion: nil)
}
}
func didEndEditing(_ notification: Notification) {
if notification.object as! RMLDesignableUITextField === self {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: .curveEaseIn, animations: { () -> Void in
if self.text!.isEmpty {
self.fakePlaceholderLabel.transform = self.fakePlaceholderLabel.transform.translatedBy(x: -self.translateX, y: 0.0)
self.fakePlaceholderLabel.alpha = 1.0
self.rightPlaceholderLabel.transform = self.rightPlaceholderLabel.transform.translatedBy(x: -self.translateX, y: 0.0)
self.rightPlaceholderLabel.alpha = 0.0
}
}, completion: nil)
}
}
}
// MARK: - Methods
extension RMLDesignableUITextField {
fileprivate func pnpAddBorder(_ border: CALayer, color: UIColor, frame: CGRect) {
border.backgroundColor = color.cgColor
border.frame = frame
layer.addSublayer(border)
// layer.masksToBounds = true
}
func pnpAddTopBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(topBorder, color: color, frame: CGRect(x: 0, y: 0, width: frame.width, height: width))
}
func pnpAddBottomBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(bottomBorder, color: color, frame: CGRect(x: 0, y: frame.height - width, width: frame.width, height: width))
}
func pnpAddLeftBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(leftBorder, color: color, frame: CGRect(x: 0, y: 0, width: width, height: frame.height))
}
func pnpAddRightBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(rightBorder, color: color, frame: CGRect(x: frame.width - width, y: 0, width: width, height: frame.height))
}
func setupSubviews() {
// if (showsTopBorder || showsBottomBorder || showsLeftBorder || showsRightBorder) && borderWidth == 0 {
// borderWidth = 1
// }
if showsTopBorder {
pnpAddTopBorder(borderWidth, color: borderColor)
} else {
topBorder.removeFromSuperlayer()
}
if showsBottomBorder {
pnpAddBottomBorder(borderWidth, color: borderColor)
} else {
bottomBorder.removeFromSuperlayer()
}
if showsLeftBorder {
pnpAddLeftBorder(borderWidth, color: borderColor)
} else {
leftBorder.removeFromSuperlayer()
}
if showsRightBorder {
pnpAddRightBorder(borderWidth, color: borderColor)
} else {
rightBorder.removeFromSuperlayer()
}
self.setNeedsDisplay()
}
}