Swift - animate linear diagram with minimal segment - ios

I have a linear diagram view which basically is nothing but three views. They represent the percentage of certain asset in a portfolio. So, when I click a button, I call update(multipliers: [CGFloat]) and the animation starts. However, I ran into a few issues. Imagine if I have $10000 in stocks, $5000 in bonds and $1 in ETFs. The ETF view won't be visible at all because it's so small. But even in this case I want to show a 10px or something width view. How can I achieve this? Also, my debugger keeps warning me
Unable to simultaneously satisfy constraints. Probably at least one
of the constraints in the following list is one you don't want.
which is understandable, but I don't think is fixable because of multipliers and constants not matching the width.
So, how can I solve the issue?
import UIKit
class StripView: UIView {
var stocksConstraint = NSLayoutConstraint()
var bondsConstraint = NSLayoutConstraint()
var etfConstraint = NSLayoutConstraint()
let stocksView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
let bondsView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
let etfView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
layer.cornerRadius = 4
layer.masksToBounds = true
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(multipliers: [CGFloat]) {
stocksView.removeConstraint(stocksConstraint)
stocksConstraint = stocksView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[0])
stocksConstraint.isActive = true
bondsView.removeConstraint(bondsConstraint)
bondsConstraint = bondsView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[1])
bondsConstraint.isActive = true
etfView.removeConstraint(etfConstraint)
etfConstraint = etfView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[2])
etfConstraint.isActive = true
UIView.animate(withDuration: 1.0) {
self.layoutIfNeeded()
self.stocksView.backgroundColor = .systemRed
self.bondsView.backgroundColor = .systemGreen
self.etfView.backgroundColor = .systemBlue
}
}
func setupViews() {
let views = [stocksView, bondsView, etfView]
views.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
stocksConstraint = stocksView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
bondsConstraint = bondsView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
etfConstraint = etfView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
NSLayoutConstraint.activate([
stocksView.topAnchor.constraint(equalTo: topAnchor),
stocksView.bottomAnchor.constraint(equalTo: bottomAnchor),
stocksView.leadingAnchor.constraint(equalTo: leadingAnchor),
stocksConstraint,
bondsView.topAnchor.constraint(equalTo: topAnchor),
bondsView.bottomAnchor.constraint(equalTo: bottomAnchor),
bondsView.leadingAnchor.constraint(equalTo: stocksView.trailingAnchor, constant: 2),
bondsConstraint,
etfView.topAnchor.constraint(equalTo: topAnchor),
etfView.bottomAnchor.constraint(equalTo: bottomAnchor),
etfView.leadingAnchor.constraint(equalTo: bondsView.trailingAnchor, constant: 2),
etfConstraint
])
}
}

Related

ios Swift: ScrollView with dynamic content programmatic layout

Need to create custom view, just 2 buttons and some content between. Problem is about create correct layout using scrollView and subviews with dynamic content.
For example, if there will be only one Label.
What is my mistake?
Now label isn't visible, and view looks like:
Here is code:
view inits this way:
let view = MyView(frame: .zero)
view.configure(with ...) //here configures label text
selv.view.addSubView(view)
public final class MyView: UIView {
private(set) var titleLabel: UILabel?
override public init(frame: CGRect) {
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
let contentLayoutGuide = scrollView.contentLayoutGuide
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
(label's font and alignment setup)
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
super.init(frame: frame)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
self.textLabel = textLabel
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(with viewModel: someViewModel) {
titleLabel?.text = viewModel.title
}
}
If I'll add scrollView frameLayoutGuide height:
scrollView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 150),
, then all looks as expected, but I need to resize this label and all MyView height depending on content.
A UIScrollView is designed to automatically allow scrolling when its content is larger than its frame.
By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero.
If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.
If we want it to scroll when it has a lot of content, we also need to give it a maximum height.
So, if we want MyView height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:
myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
and then constrain the scroll view height in MyView like this:
let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
Here is a modification to your code - lots of comments in the code so you should be able to follow.
First, an example controller:
class MVTestVC: UIViewController {
let myView = MyView()
let sampleStrings: [String] = [
"Short string.",
"This is a longer string which should wrap onto a couple lines.",
"Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
"We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
]
var strIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 20-points on each side
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// centered vertically
myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// max 1/2 screen (view) height
myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
])
myView.backgroundColor = .white
myView.configure(with: sampleStrings[0])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
strIndex += 1
myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
}
}
and the modified MyView class:
public final class MyView: UIView {
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
// this will be used to set the scroll view height
private var svh: NSLayoutConstraint!
override public init(frame: CGRect) {
super.init(frame: frame)
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
closeButton.setTitle("X", for: [])
closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
//(label's font and alignment setup)
titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
titleLabel.numberOfLines = 0
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
successButton.setTitle("Success", for: [])
successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
// constrain the label to the scroll view's Content Layout Guide
titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
// label needs a width anchor, otherwise we'll get horizontal scrolling
titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
])
layer.cornerRadius = 12
// so we can see the framing
scrollView.backgroundColor = .red
titleLabel.backgroundColor = .green
}
public override func layoutSubviews() {
super.layoutSubviews()
// we want to update the scroll view's height constraint when the text changes
if let c = svh {
c.isActive = false
}
// on initial layout, the scroll view's content size will still be zero
// so force another layout pass
if scrollView.contentSize.height == 0 {
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
}
// constrain the scroll view's height to the height of its content
// but with a less-than-required priority so we can use a maximum height
svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//public func configure(with viewModel: someViewModel) {
// titleLabel.text = viewModel.title
//}
public func configure(with str: String) {
titleLabel.text = str
// force the scroll view to update its layout
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
// force self to update its layout
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:

How can implement two vertical button in swipe to delete in ios?

I am trying to implement swipe to delete feature with two options in tableview, one is to delete and another one is to Update.The things I want is these options should be vertical rather than horizontal.I have checked so many question but nothing find.
Thanks in advance for support.
.
As I mentioned in the comments, here is one approach:
add your buttons to the cell
add a "container" view to the cell
constrain the container view so it overlays / covers the buttons
add a Pan gesture recognizer to the container view so you can drag it left / right
as you drag it left, it will "reveal" the buttons underneath
You lose all of the built-in swipe functionality, but this is one approach that might give you the design you're going for.
First, an example of creating a "drag view":
class DragTestViewController: UIViewController {
let backgroundView = UIView()
let containerView = UIView()
// leading and trailing constraints for the drag view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(60.0)
private let origTrailing = CGFloat(-60.0)
private var currentLeading = CGFloat(60.0)
private var currentTrailing = CGFloat(-60.0)
override func viewDidLoad() {
super.viewDidLoad()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .cyan
backgroundView.clipsToBounds = true
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .red
// add a label to the container view
let exampleLabel = UILabel()
exampleLabel.translatesAutoresizingMaskIntoConstraints = false
exampleLabel.text = "Drag Me"
exampleLabel.textColor = .yellow
containerView.addSubview(exampleLabel)
backgroundView.addSubview(containerView)
view.addSubview(backgroundView)
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: origTrailing)
NSLayoutConstraint.activate([
// constrain backgroundView top to top + 80
backgroundView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80.0),
// constrain backgroundView leading / trailing to leading / trailing with 40-pt "padding"
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
// constrain height to 100
backgroundView.heightAnchor.constraint(equalToConstant: 100.0),
// constrain containerView top / bottom to backgroundView top / bottom with 8-pt padding
containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 8.0),
containerView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -8.0),
// activate leading / trailing constraints
leadingConstraint,
trailingConstraint,
// constrain the example label centered in the container view
exampleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
exampleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the containerView - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
// update current vars
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
default:
break
}
}
}
That code will produce this:
A red view with a centered label, inside a cyan view. You can drag the red "container" view left and right.
Add a view controller to a new project and assign its Custom Class to DragTestViewController from the above code. There are no #IBOutlet or #IBAction connections, so you should be able to run it as-is. See if you can drag the red view.
Using that as a starting point, we can get this:
with this code:
// simple rounded-corner shadowed view
class ShadowRoundedView: UIView {
let shadowLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.layer.addSublayer(shadowLayer)
clipsToBounds = false
backgroundColor = .clear
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowRadius = 4.0
shadowLayer.shadowOpacity = 0.6
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
}
override func layoutSubviews() {
super.layoutSubviews()
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: 16.0)
shadowLayer.path = pth.cgPath
}
}
// simple rounded button
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class DragRevealCell: UITableViewCell {
// callback closure for button taps
var callback: ((Int) -> ())?
// this will hold the "visible" labels, and will initially cover the buttons
let containerView: ShadowRoundedView = {
let v = ShadowRoundedView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// this will hold the buttons
let buttonsView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.clipsToBounds = true
return v
}()
// a "delete" button
let deleteButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete", for: [])
v.setTitleColor(.blue, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .white
return v
}()
// an "update" button
let updateButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Update", for: [])
v.setTitleColor(.white, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
// single label for this example cell
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
return v
}()
// leading and trailing constraints for the container view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(8.0)
private let origTrailing = CGFloat(-8.0)
private var currentLeading = CGFloat(0.0)
private var currentTrailing = CGFloat(0.0)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// cell background color
backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// add buttons to buttons container view
buttonsView.addSubview(deleteButton)
buttonsView.addSubview(updateButton)
// add label to container view -- this is where you would add all your labels, stack views, image views, etc.
containerView.addSubview(myLabel)
// add buttons view first
addSubview(buttonsView)
// add container view second - this will "overlay" it on top of the buttons view
addSubview(containerView)
// containerView leading / trailing constraints - these will be updated as we drag
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: origTrailing)
// needed to avoid layout warnings
let bottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0)
bottomConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
leadingConstraint,
trailingConstraint,
bottomConstraint,
myLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
myLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
myLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
myLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8.0),
myLabel.heightAnchor.constraint(equalToConstant: 120.0),
buttonsView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
buttonsView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
deleteButton.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: 0.0),
deleteButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
deleteButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: 0.0),
updateButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
updateButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.topAnchor.constraint(equalTo: deleteButton.bottomAnchor, constant: 12.0),
updateButton.heightAnchor.constraint(equalTo: deleteButton.heightAnchor),
updateButton.widthAnchor.constraint(equalTo: deleteButton.widthAnchor),
deleteButton.widthAnchor.constraint(equalToConstant: 120.0),
deleteButton.heightAnchor.constraint(equalToConstant: 40.0),
])
// delete button border
deleteButton.layer.borderColor = UIColor.blue.cgColor
deleteButton.layer.borderWidth = 1.0
// targets for button taps
deleteButton.addTarget(self, action: #selector(self.deleteTapped(_:)), for: .touchUpInside)
updateButton.addTarget(self, action: #selector(self.updateTapped(_:)), for: .touchUpInside)
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the container view - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
// don't allow drag-to-the-right
if currentLeading + translation.x <= origLeading {
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
}
default:
// if the drag-left did not fully reveal the buttons, animate the container view back in place
if containerView.frame.maxX > buttonsView.frame.minX {
self.leadingConstraint.constant = self.origLeading
self.trailingConstraint.constant = self.origTrailing
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
}, completion: { _ in
//self.dragX = 0.0
})
}
}
}
#objc func deleteTapped(_ sender: Any?) -> Void {
callback?(0)
}
#objc func updateTapped(_ sender: Any?) -> Void {
callback?(1)
}
}
class DragRevealTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DragRevealCell.self, forCellReuseIdentifier: "DragRevealCell")
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "DragRevealCell", for: indexPath) as! DragRevealCell
c.myLabel.text = "Row \(indexPath.row)" + "\n" + "This is where you would populate the cell's labels, image views, any other UI elements, etc."
c.selectionStyle = .none
c.callback = { value in
if value == 0 {
print("Delete action")
} else {
print("Update action")
}
}
return c
}
}
Add a UITableViewController the project and assign its Custom Class to DragRevealTableViewController from the above code. Again, there are no #IBOutlet or #IBAction connections, so you should be able to run it as-is.
NOTE: This is example code only, and should not be considered "production ready"!!! It is only partially implemented and will likely need quite a bit more work. But, it may give you a good starting point.

Swift: UIStackView of UIControls with selector method that doesn't fire

Introduction
I'm creating an app which uses a custom view in which I have a UIStackView to sort out 5 UIControls. When a user taps one of the UIControls an underscore line gets animated, sliding under the tapped UIControl.
However, for some reason the method/selector for these UIControls no longer gets called. I believe this has to do with that I updated my Mac to the macOS (and Xcode) update released this week (wk.44). (updated from swift 4.2 to swift 4.2.1). Before the updated this animation and selector worked perfectly. But I'm not sure. And I'm now completely stuck on what I'm doing wrong.
Context
I created a playground and scaled down everything as much as I could and the issue persists.
I have tried to define the UIStackView in the global scope of my SetupView class but it doesn't change anything. So I believe it is not an issue of the stackView or its subviews being deallocated?
Below I've provided my UIControl subclass and my SetupView (UIView subclass) that I use. I've created a playground so you may copy paste in Xcode playground to test if you want.
Question
Why doesn't the method goalViewControlTapped(_ sender: SetupViewControl) get called?
Code
import UIKit
import PlaygroundSupport
class SetupViewControl: UIControl {
let titleLabel : UILabel = {
let lbl = UILabel()
lbl.font = UIFont(name: "Futura", size: 14)
lbl.textColor = .white
lbl.backgroundColor = .clear
lbl.textAlignment = .center
lbl.translatesAutoresizingMaskIntoConstraints = false
return lbl
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupLabel()
layer.cornerRadius = 5
}
fileprivate func setupLabel() {
addSubview(titleLabel)
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isHighlighted: Bool {
didSet {
UIView.animate(withDuration: 0.12) {
self.backgroundColor = self.isHighlighted ? UIColor.lightGray : UIColor.clear
}
}
}
}
class SetupView: UIView {
let dataModel : [String] = ["2 weeks", "1 month", "2 months", "6 months", "1 year"]
var selectionLineCenterX : NSLayoutConstraint!
let selectionLine = UIView()
let labelZero = SetupViewControl()
let labelOne = SetupViewControl()
let labelTwo = SetupViewControl()
let labelThree = SetupViewControl()
let labelFour = SetupViewControl()
let labelFive = SetupViewControl()
lazy var controlArray = [self.labelZero, self.labelOne, self.labelTwo, self.labelThree, self.labelFour, self.labelFive]
init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
self.backgroundColor = color
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setupView() {
layer.cornerRadius = 0
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
setupLabelText()
setupControlsInStackView()
}
fileprivate func setupLabelText() {
for num in 0...(dataModel.count - 1) {
controlArray[num].titleLabel.text = dataModel[num]
}
}
// let stackView = UIStackView(frame: .zero) I have tried to declare the stackView here but it doesn't fix my issue.
func setupControlsInStackView() {
var stackViewArray = [SetupViewControl]()
for num in 0...(dataModel.count - 1) {
controlArray[num].isUserInteractionEnabled = true
controlArray[num].addTarget(self, action: #selector(goalViewControlTapped(_:)), for: .touchUpInside)
stackViewArray.append(controlArray[num])
}
let stackView = UIStackView(arrangedSubviews: stackViewArray)
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8).isActive = true
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8).isActive = true
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 15).isActive = true
addSubview(selectionLine)
selectionLine.backgroundColor = .white
selectionLine.translatesAutoresizingMaskIntoConstraints = false
selectionLine.heightAnchor.constraint(equalToConstant: 1).isActive = true
selectionLine.topAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
selectionLine.widthAnchor.constraint(equalToConstant: 50).isActive = true
selectionLineCenterX = selectionLine.centerXAnchor.constraint(equalTo: leadingAnchor, constant: -100)
selectionLineCenterX.isActive = true
}
#objc fileprivate func goalViewControlTapped(_ sender: SetupViewControl) {
print("This is not getting printed!!!")
selectionLineCenterX.isActive = false
selectionLineCenterX = selectionLine.centerXAnchor.constraint(equalTo: sender.centerXAnchor)
selectionLineCenterX.isActive = true
UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: nil)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let testView = SetupView(frame: .zero, color: UIColor.blue)
view.addSubview(testView)
testView.translatesAutoresizingMaskIntoConstraints = false
testView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
testView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
testView.heightAnchor.constraint(equalToConstant: 100).isActive = true
testView.widthAnchor.constraint(equalToConstant: 365).isActive = true
}
}
// For live view in playground
let vc = ViewController()
vc.preferredContentSize = CGSize(width: 375, height: 812)
PlaygroundPage.current.liveView = vc
Thanks for reading my question.
Does your UIStackView show as having an ambiguous layout when you open the view debugger? If so, that may be causing the internal views to not receive the touch events.
You can provide UIStackView with either:
x and y constraints only
or
x, y, width and height.
In the above case the height constraint is missing.

CollectionView Cell Rounded Corners not Working

I have a prototype cell for a collection view and am trying to make the edges rounded, but I can't figure out how to use self.layer.cornerRadius = 3.0 and make it work. Could anyone tell me what I'm doing wrong?
import UIKit
final class TagCell: UICollectionViewCell {
let textLabel: UILabel
override init(frame: CGRect) {
self.textLabel = UILabel(frame: .zero)
self.textLabel.setFont(14)
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
self.textLabel.textAlignment = .center
self.textLabel.backgroundColor = .red
self.textLabel.layer.cornerRadius = 2.0
super.init(frame: frame)
self.contentView.layer.cornerRadius = 2.0
self.contentView.clipsToBounds = true
self.contentView.addSubview(textLabel)
NSLayoutConstraint.activate([
self.textLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 16),
self.textLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -16),
self.textLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 16),
self.textLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -16),
])
}
required init?(coder decoder: NSCoder) {
fatalError()
}
}
it's better to create a subView inside the contentView and apply the corner to it
self.contentView.layer.cornerRadius = 2.0
self.contentView.clipsToBounds = true

How to position a subView below a subView with dynamic height?

I have the following UIView:
It is a UIView which contains three subviews. A regular UILabel (Hello World) at the top, a custom UIViewController (CategoryList) which contains a CollectionView with buttons (alpha,beta, ...) and another custom UIViewController with just a label (SALUT).
I do auto-layout programmatically and position SALUT (var sCtrl) below the CategoryList (var catList) with
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
This results in the picture above, where SALUT is not positioned below the category list as I would like it to be. I sort of understand why since when I set the constraint the buttons are not yet laid out properly in the CollectionView and thus the bottom anchor of the catList is not set.
In the CategoryList:UIVIewController I have the following in order to get the collection view to get the correct height.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
}
This works but since viewDidLayoutSubviews() is called after the constraint is set on SALUT the SALUT position is wrong. How can I get it right in an easy way? (I guess I could make a controller for my main view and check there when subviews are laid out and then update subview positions but this seems overly complicated to me).
The full code for the main view is below so you can see the layout positioning code. If needed I can also provide the subviews code but I suppose it shouldn't be needed since they should be considered black boxes...
class MyView: UIView {
let label = UILabel()
var sCtrl : SubController
var catList : CategoryList
var topConstraint = NSLayoutConstraint()
override init(frame: CGRect) {
sCtrl = SubController()
catList = CategoryList()
super.init(frame: frame)
setup()
}
private func setup() {
self.backgroundColor = UIColor.green
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.yellow
label.text = "Hello world"
label.textAlignment = .center
self.addSubview(label)
catList.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(catList.view)
sCtrl.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(sCtrl.view)
let margins = self.layoutMarginsGuide
label.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
label.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 0).isActive = true
label.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0).isActive = true
label.heightAnchor.constraint(equalToConstant: 100.0).isActive=true
catList.view.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
catList.view.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 1.0).isActive = true
catList.view.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 1.0).isActive = true
catList.view.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant:0).isActive = true
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let v = MyView(frame: CGRect(x: 0, y: 0, width: 400, height: 600))
Ok, I found the issue. I had missed to add the height constraint to the CategoryList's own view. Adding this it works just fine.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
//THE MISSING LINE!!
self.view.heightAnchor.constraint(equalTo: self.collectionView.heightAnchor, constant: 0).isActive = true
}

Resources