Strange behavior of Height constraint of UITableView cell in IOS 15 - ios

After upgrade on IOS 15 my previous code start works strange.
I have UITableView with cells with embedded views. When it loads in viewDidLoad cell's height constrain that I setup in storyboard become active and cell become close. Tap on cell makes height constraint become not active and cell open.
Height constraint for foldView
But in IOS 15 cell always open when loading. Even if height constraint is active.
here my code that makes it open or close:
var expanded: Bool = false {
didSet {
let duration = CATransaction.animationDuration()
UIView.animate(withDuration: duration) {
self.arrow.transform = self.expanded ? CGAffineTransform(rotationAngle: .pi / 2) : .identity
self.height.priority = .defaultHigh
self.height.isActive = !self.expanded
self.foldView.alpha = self.expanded ? 1 : 0
self.container.layoutIfNeeded()
}
}
}
and this is how I add embedded view:
func addEmbeddedView(_ view: UIView) {
self.foldView.subviews.forEach { $0.removeFromSuperview() }
self.foldView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
view.leadingAnchor.constraint(equalTo: self.foldView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: self.foldView.trailingAnchor),
view.topAnchor.constraint(equalTo: self.foldView.topAnchor),
view.bottomAnchor.constraint(equalTo: self.foldView.bottomAnchor, constant: -9)
]
constraints[3].priority = .defaultHigh
NSLayoutConstraint.activate(constraints)
}
BUT! if I tap on cell from
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
tableView.beginUpdates()
cell.expanded.toggle()
tableView.fixCellBounds()
tableView.endUpdates()
}
all works as expected and cell open and close in IOS 15 too.
Any suggestions what it could be?
As I assume in IOS 15 was some changes in AutoLayout that may affect my code..
Here my full custom cell class:
import UIKit
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var headerLabel: UILabel!
#IBOutlet weak var descriptionLabel: UILabel!
#IBOutlet weak var arrowImage: UIImageView!
#IBOutlet weak var container: UIView!
#IBOutlet weak var foldView: UIView!
#IBOutlet weak var arrow: UIImageView!
let layer1 = CALayer(), layer2 = CALayer(), layer3 = CALayer()
var expanded: Bool = false {
didSet {
let duration = CATransaction.animationDuration()
UIView.animate(withDuration: duration) {
self.arrow.transform = self.expanded ? CGAffineTransform(rotationAngle: .pi / 2) : .identity
self.height.priority = .defaultHigh
self.height.isActive = !self.expanded
self.foldView.alpha = self.expanded ? 1 : 0
self.container.layoutIfNeeded()
}
}
}
#IBOutlet weak var height: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
self.expanded = false
container.layer.cornerRadius = 8
container.layer.cornerCurve = .continuous
container.backgroundColor = Colors_8_5.offWhite.color
self.contentView.backgroundColor = .clear
self.container.clipsToBounds = true
self.contentView.clipsToBounds = false
self.clipsToBounds = false
self.selectionStyle = .none
self.backgroundColor = .clear
[layer1, layer2, layer3].forEach {
$0.masksToBounds = false
$0.backgroundColor = UIColor.clear.cgColor
self.contentView.layer.insertSublayer($0, at: 0)
}
}
func configure(with model: CustomCellModel) {
self.headerLabel.attributedText = model.header
self.descriptionLabel.attributedText = model.text
self.descriptionLabel.isHidden = model.text == nil
}
override func setSelected(_ selected: Bool, animated: Bool) {
}
func addEmbeddedView(_ view: UIView) {
self.foldView.subviews.forEach { $0.removeFromSuperview() }
self.foldView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
view.leadingAnchor.constraint(equalTo: self.foldView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: self.foldView.trailingAnchor),
view.topAnchor.constraint(equalTo: self.foldView.topAnchor),
view.bottomAnchor.constraint(equalTo: self.foldView.bottomAnchor, constant: -9)
]
constraints[3].priority = .defaultHigh
NSLayoutConstraint.activate(constraints)
}
override func layoutSubviews() {
super.layoutSubviews()
[layer1, layer2, layer3].forEach {
$0.frame = container.frame
}
layer1.CustomapplySketchShadow(color: .black, alpha: 0.04, x: 0, y: 4, blur: 8, spread: 0)
layer2.CustomapplySketchShadow(color: .black, alpha: 0.04, x: 0, y: 16, blur: 24, spread: 0)
layer3.CustomapplySketchShadow(color: .black, alpha: 0.04, x: 0, y: 24, blur: 32, spread: 0)
}
}
extension CALayer {
func CustomapplySketchShadow(
color: UIColor = .black,
alpha: Float = 0.2,
x: CGFloat = 0,
y: CGFloat = 2,
blur: CGFloat = 4,
spread: CGFloat = 0)
{
shadowColor = color.cgColor
shadowOpacity = alpha
shadowOffset = CGSize(width: x, height: y)
shadowRadius = blur / 2
if spread == 0 {
shadowPath = UIBezierPath(rect: self.bounds).cgPath
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
shadowPath = UIBezierPath(rect: rect).cgPath
}
}
}

Related

CAShapeLayer with different Colors

I have a CAShapeLayer based on this answer that animates along with a UISlider.
It works fine but as the shapeLayer follows along its just 1 red CAGradientLayer color. What I want is the shapeLayer to change colors based on certain points of the slider. An example is at 0.4 - 0.5 it's red, 0.7-0.8 red, 0.9-0.95 red. Those aren't actual values, the actual values will vary. I figure that any time it doesn't meet the condition to turn red it should probably just be a clear color, which will just show the black track underneath it. The result would look something like this (never mind the shape)
The red colors are based on the user scrubbing the slider and the letting go. The different positions of the slider that determine the red color is based on whatever condition. How can I do this.
UISlider
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.minimumTrackTintColor = .blue
s.maximumTrackTintColor = .white
s.minimumValue = 0
s.maximumValue = 1
s.addTarget(self, action: #selector(onSliderChange), for: .valueChanged)
return s
s.addTarget(self, action: #selector(onSliderEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
return s
}()
lazy var progressView: GradientProgressView = {
let v = GradientProgressView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
#objc fileprivate func onSliderChange(_ slider: UISlider) {
let condition: Bool = // ...
let value = slider.value
progressView.setProgress(CGFloat(value), someCondition: condition, slider_X_Position: slider_X_PositionInView())
}
#objc fileprivate func onSliderEnded(_ slider: UISlider) {
let value = slider.value
progressView.resetProgress(CGFloat(value))
}
// ... progressView is the same width as the the slider
func slider_X_PositionInView() -> CGFloat {
let trackRect = slider.trackRect(forBounds: slider.bounds)
let thumbRect = slider.thumbRect(forBounds: slider.bounds,
trackRect: trackRect,
value: slider.value)
let convertedThumbRect = slider.convert(thumbRect, to: self.view)
return convertedThumbRect.midX
}
GradientProgressView:
public class GradientProgressView: UIView {
var shapeLayer: CAShapeLayer = {
// ...
}()
private var trackLayer: CAShapeLayer = {
let trackLayer = CAShapeLayer()
trackLayer.strokeColor = UIColor.black.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
return trackLayer
}()
private var gradient: CAGradientLayer = {
let gradient = CAGradientLayer()
let redColor = UIColor.red.cgColor
gradient.colors = [redColor, redColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
return gradient
}()
// ... add the above layers as subLayers to self ...
func updatePaths() { // added in layoutSubviews
let lineWidth = bounds.height / 2
trackLayer.lineWidth = lineWidth * 0.75
shapeLayer.lineWidth = lineWidth
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))
trackLayer.path = path.cgPath
shapeLayer.path = path.cgPath
gradient.frame = bounds
gradient.mask = shapeLayer
shapeLayer.duration = 1
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
}
public func setProgress(_ progress: CGFloat, someCondition: Bool, slider_X_Position: CGFloat) {
// slider_X_Position might help with shapeLayer's x position for the colors ???
if someCondition {
// redColor until the user lets go
} else {
// otherwise always a clearColor
}
shapeLayer.strokeEnd = progress
}
}
public func resetProgress(_ progress: CGFloat) {
// change to clearColor after finger is lifted
}
}
To get this:
We can use a CAShapeLayer for the red "boxes" and a CALayer as a .mask on that shape layer.
To reveal / cover the boxes, we set the frame of the mask layer to a percentage of the width of the bounds.
Here's a complete example:
class StepView: UIView {
public var progress: CGFloat = 0 {
didSet {
setNeedsLayout()
}
}
public var steps: [[CGFloat]] = [[0.0, 1.0]] {
didSet {
setNeedsLayout()
}
}
public var color: UIColor = .red {
didSet {
stepLayer.fillColor = color.cgColor
}
}
private let stepLayer = CAShapeLayer()
private let maskLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
layer.addSublayer(stepLayer)
stepLayer.fillColor = color.cgColor
stepLayer.mask = maskLayer
// mask layer can use any solid color
maskLayer.backgroundColor = UIColor.white.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
stepLayer.frame = bounds
let pth = UIBezierPath()
steps.forEach { pair in
// rectangle for each "percentage pair"
let w = bounds.width * (pair[1] - pair[0])
let b = UIBezierPath(rect: CGRect(x: bounds.width * pair[0], y: 0, width: w, height: bounds.height))
pth.append(b)
}
stepLayer.path = pth.cgPath
// update frame of mask layer
var r = bounds
r.size.width = bounds.width * progress
maskLayer.frame = r
}
}
class StepVC: UIViewController {
let stepView = StepView()
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(slider)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
slider.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let steps: [[CGFloat]] = [
[0.1, 0.3],
[0.4, 0.5],
[0.7, 0.8],
[0.9, 0.95],
]
stepView.steps = steps
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
}
#objc func sliderChanged(_ sender: UISlider) {
// disable CALayer "built-in" animations
CATransaction.setDisableActions(true)
stepView.progress = CGFloat(sender.value)
CATransaction.commit()
}
}
Edit
I'm still not clear on your 0.4 - 0.8 requirement, but maybe this will help get you on your way:
Please note: this is Example Code Only!!!
struct RecordingStep {
var color: UIColor = .black
var start: Float = 0
var end: Float = 0
var layer: CALayer!
}
class StepView2: UIView {
public var progress: Float = 0 {
didSet {
// move the progress layer
progressLayer.position.x = bounds.width * CGFloat(progress)
// if we're recording
if isRecording {
let i = theSteps.count - 1
guard i > -1 else { return }
// update current "step" end
theSteps[i].end = progress
setNeedsLayout()
}
}
}
private var isRecording: Bool = false
private var theSteps: [RecordingStep] = []
private let progressLayer = CAShapeLayer()
public func startRecording(_ color: UIColor) {
// create a new "Recording Step"
var st = RecordingStep()
st.color = color
st.start = progress
st.end = progress
let l = CALayer()
l.backgroundColor = st.color.cgColor
layer.insertSublayer(l, below: progressLayer)
st.layer = l
theSteps.append(st)
isRecording = true
}
public func stopRecording() {
isRecording = false
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
progressLayer.lineWidth = 3
progressLayer.strokeColor = UIColor.green.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(progressLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// only set the progessLayer frame if the bounds height has changed
if progressLayer.frame.height != bounds.height + 7.0 {
let r: CGRect = CGRect(origin: .zero, size: CGSize(width: 7.0, height: bounds.height + 7.0))
let pth = UIBezierPath(roundedRect: r, cornerRadius: 3.5)
progressLayer.frame = r
progressLayer.position = CGPoint(x: 0, y: bounds.midY)
progressLayer.path = pth.cgPath
}
theSteps.forEach { st in
let x = bounds.width * CGFloat(st.start)
let w = bounds.width * CGFloat(st.end - st.start)
let r = CGRect(x: x, y: 0.0, width: w, height: bounds.height)
st.layer.frame = r
}
}
}
class Step2VC: UIViewController {
let stepView = StepView2()
let actionButton: UIButton = {
let b = UIButton()
b.backgroundColor = .lightGray
b.setImage(UIImage(systemName: "play.fill"), for: [])
b.tintColor = .systemGreen
return b
}()
var timer: Timer!
let colors: [UIColor] = [
.red, .systemBlue, .yellow, .cyan, .magenta, .orange,
]
var colorIdx: Int = -1
var action: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
actionButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(actionButton)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
actionButton.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
actionButton.widthAnchor.constraint(equalToConstant: 80.0),
actionButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
actionButton.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
}
#objc func timerFunc(_ timer: Timer) {
// don't set progress > 1.0
stepView.progress = min(stepView.progress + 0.005, 1.0)
if stepView.progress >= 1.0 {
timer.invalidate()
actionButton.isHidden = true
}
}
#objc func btnTap(_ sender: UIButton) {
switch action {
case 0:
// this will run for 15 seconds
timer = Timer.scheduledTimer(timeInterval: 0.075, target: self, selector: #selector(timerFunc(_:)), userInfo: nil, repeats: true)
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
case 1:
colorIdx += 1
stepView.startRecording(colors[colorIdx % colors.count])
actionButton.setImage(UIImage(systemName: "stop.circle"), for: [])
actionButton.tintColor = .black
action = 2
case 2:
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
default:
()
}
}
}
For future reference, when posting here, it's probably a good idea to fully explain what you're trying to do. Showing code you're working on is important, but if it's really only sorta related to your actual goal, it makes this process pretty difficult.

How to change page control dot size and spacing in swift?

I want customize page control like a image.
I've already search that, but there are only deal scale.
I want change width, height, spacing.
How can I do that?
I tried this
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
subviews.forEach {
$0.frame.size = ($0 == currentDot) ? CGSize(width: 16, height: 4) : CGSize(width: 8, height: 4)
$0.layer.cornerRadius = 2
}
}
}
But how to change distance??
#oddK Can you try with this below answer. It's my assumption.
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
let spacing = 5.0
subviews.forEach {
$0.frame = ($0 == currentDot) ? CGRect(x: 0, y: 0, width: 16, height: 4) : CGRect(x: spacing, y: 0, width: 8, height: 4)
//$0.frame.size = ($0 == currentDot) ? CGSize(width: 16, height: 4) : CGSize(width: 8, height: 4)
$0.layer.cornerRadius = 2
}
}
}
The default UIPageControll is not flexible.
class ExtendedpageControll: UIView{
var numberOfPage: Int
var currentpage : Int = 0{didSet{reloadView()}}
var currentIndicatorColor: UIColor = .black
var indicatorColor: UIColor = UIColor(white: 0.9, alpha: 1)
var circleIndicator: Bool = false
private var dotView = [UIView]()
private let spacing: CGFloat = 6
private lazy var extraWidth: CGFloat = circleIndicator ? 6 : 4
init(numberOfPages: Int,currentPage: Int,isCircular: Bool){
self.numberOfPage = numberOfPages
self.currentpage = currentPage
self.circleIndicator = isCircular
super.init(frame: .zero)
configView()
}
required init?(coder: NSCoder) {fatalError("not implemented")}
private func configView(){
backgroundColor = .clear
(0..<numberOfPage).forEach { _ in
let view = UIView()
addSubview(view)
dotView.append(view)
}
}
private func reloadView(){
dotView.forEach{$0.backgroundColor = indicatorColor}
dotView[currentpage].backgroundColor = currentIndicatorColor
UIView.animate(withDuration: 0.2) {
self.dotView[self.currentpage].frame.origin.x = self.dotView[self.currentpage].frame.origin.x - self.extraWidth
self.dotView[self.currentpage].frame.size.width = self.dotView[self.currentpage].frame.size.width + (self.extraWidth * 2)
}
}
override func layoutSubviews() {
super.layoutSubviews()
for (i,view) in dotView.enumerated(){
view.clipsToBounds = true
view.layer.cornerRadius = bounds.height / 2
let width: CGFloat = circleIndicator ? self.bounds.height : CGFloat(self.bounds.width / CGFloat(self.numberOfPage) - self.spacing) - self.extraWidth
UIView.animate(withDuration: 0.2) {
view.frame = CGRect(x: ((self.bounds.width / CGFloat(self.numberOfPage)) * CGFloat(i)) + self.spacing, y: 0, width: width , height: self.bounds.height)
}
}
reloadView()
}
}
Usage: If you want to link ExtendedpageControll to a View Such as CollectionView Just Do like this: (item is your Datamodel)
class SampleViewController: UIViewController{
let colectionView = UICollectionView()
lazy var pageControll: ExtendedpageControll = {
let pc = ExtendedpageControll(numberOfPages: items.count, currentPage: 0,isCircular: true)
pc.currentIndicatorColor = .black
return pc
}()
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if pageControll.currentpage == indexPath.row {
guard let visible = self.collectionView.visibleCells.first else { return }
guard let index = self.collectionView.indexPath(for: visible)?.row else { return }
pageControll.currentpage = index
}
}
}
inside init, you can set the shape of the indicator to be circular or extended via isCircular.

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:
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

IOS Step menu using swift

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.!!

change inputAccessoryView's height issue in iOS 8.4

I made a MessageComposerView like this
import UIKit
protocol MessageComposerDelegate: NSObjectProtocol {
func messageDidChange(messageView: MessageComposerView, height: CGFloat)
func postMessage(message: String)
}
private let maxTextViewHeight: CGFloat = 100.0
class MessageComposerView: UIView, UITextViewDelegate {
#IBOutlet weak var textView: SAMTextView!
#IBOutlet weak var sendButton: UIButton!
weak var delegate:MessageComposerDelegate?
#IBAction func sendButtonAction(sender: UIButton) {
delegate?.postMessage(textView.text)
}
override func awakeFromNib() {
let borderColor = UIColor(white: 0.9, alpha: 1).CGColor
let layer = CALayer()
layer.backgroundColor = borderColor
layer.frame = CGRectMake(0, 0, CGRectGetWidth(UIScreen.mainScreen().bounds), 1.0)
self.layer.addSublayer(layer)
textView.backgroundColor = UIColor.whiteColor()
textView.layer.borderColor = borderColor
textView.layer.borderWidth = 1.0
textView.layer.cornerRadius = 5.0
textView.delegate = self
textView.layoutManager.allowsNonContiguousLayout = false
sendButton.backgroundColor = Color.Red.colorValue
sendButton.tintColor = UIColor.whiteColor()
sendButton.layer.cornerRadius = 5.0
}
//MARK: TextViewDelegate
func textViewDidChange(textView: UITextView) {
self.sendButton.enabled = !self.textView.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty
textView.scrollEnabled = textView.contentSize.height >= maxTextViewHeight
let height = textView.scrollEnabled ? textView.contentSize.height : textView.intrinsicContentSize().height
delegate?.messageDidChange(self, height: min(height, maxTextViewHeight) + 16.0)
if let selectedTextRange = self.textView.selectedTextRange {
let caretRect = self.textView.caretRectForPosition(selectedTextRange.end);
let height = self.textView.textContainerInset.bottom + caretRect.size.height
self.textView.scrollRectToVisible(CGRectMake(caretRect.origin.x, caretRect.origin.y, caretRect.size.width, height), animated: true)
}
}
}
and in the ViewController when I increase the height of MessageComposerView:
func messageDidChange(messageView: MessageComposerView, height: CGFloat) {
messageView.bounds.size.height = height
self.reloadInputViews()
messageView.textView.reloadInputViews()
}
In iOS 9,It works well.
but in iOS 8.4,The textView and Button not change their frame rightly
So What should I do to fix it in iOS 8.4,Thank you!

Resources