I'm creating following view (display under In Transit) which are used to maintain my product status. See below image.
I want to create this view in UITableViewCell, I have tried by placing fixed height/width view (Circle View with different color) and horizontal gray line view and it's work fine for fixed spot point. I'm able to create this for fixed view using storyboard.
My Problem is, these are dynamic spot point view. Currently it's 4, but it can be vary based on status available in API response.
Anyone have idea? How to achieve this status spot dynamic view?.
You can achieve your thing using UICollectionView inside UITableViewCell.
First create following design for collection view cell. This collection view added inside table view cell.
CollectionViewCell:
See Constraints:
Regarding spotview and circleview you can recognise by constraints and view. So don't confuse therem otherwise all naming convention are available based on view's priority.
Now you need to take outlet of collection view inside UITableViewCell's subclass whatever you made and collection view cell's subview to UICollectionViewCell's subclass.
UITableViewCell:
class CTrackOrderInTransitTVC: UITableViewCell {
#IBOutlet weak var transitView : UIView!
#IBOutlet weak var cvTransit : UICollectionView!
var arrColors: [UIColor] = [.blue, .yellow, .green, .green]
override func awakeFromNib() {
super.awakeFromNib()
}
}
Now add following code in your collection view cell subclass, It's contains outlets of your subViews of collection view cell:
class CTrackOrderInTransitCVC: UICollectionViewCell {
#IBOutlet weak var leftView : UIView!
#IBOutlet weak var rightView : UIView!
#IBOutlet weak var spotView : UIView!
#IBOutlet weak var circleView : UIView!
}
Thereafter, you have to implemented table view datasource method load your collection view cell inside your table.
See the following code:
extension YourViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
//------------------------------------------------------------------------------
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CTrackOrderInTransitTVC", for: indexPath) as! CTrackOrderInTransitTVC
// Reload collection view to update sub views
cell.cvTransit.reloadData()
return cell
}
}
I hope this will help you.
You can do this with a UIStackView using "spacer" views.
Add a clear UIView between each "dot" view, and constrain the width of each "spacer" view equal to the first "spacer" view.
Add a UIStackView, constrain its width and centerY to the tracking line, and set the properties to:
Axis: Horizontal
Alignment: Fill
Distribution: Fill
Spacing: 0
Your code to add the "dots" will be something like this:
for i in 0..<numberOfDots {
create a dot view
add it to the stackView using .addArrangedSubview()
one fewer spacers than dots (e.g. 4 dots have a spacer between each = 3 spacers), so,
if this is NOT the last dot,
create a spacer view
add it to the stackView
}
Keep track of the spacer views, and set their width constraints each equal to the first spacer view.
Here is some starter code which may help you get going. The comments should make it clear what's being done. Everything is being done in code (no #IBOutlets) so you should be able to run it by adding a view controller in storyboard and assigning its custom class to DotsViewController. It adds the view as a "normal" subview... but of course can also be added as a subview of a cell.
class DotView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class TrackingLineView: UIView {
var theTrackingLine: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return v
}()
var theStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fill
v.spacing = 0
return v
}()
var trackingDot: DotView = {
let v = DotView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
return v
}()
let dotWidth = CGFloat(6)
let trackingDotWidth = CGFloat(20)
var trackingDotCenterX = NSLayoutConstraint()
var dotViews = [DotView]()
var trackingPosition: Int = 0 {
didSet {
let theDot = dotViews[trackingPosition]
trackingDotCenterX.isActive = false
trackingDotCenterX = trackingDot.centerXAnchor.constraint(equalTo: theDot.centerXAnchor, constant: 0.0)
trackingDotCenterX.isActive = true
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the tracking line
addSubview(theTrackingLine)
// add the "big" tracking dot
addSubview(trackingDot)
// add the stack view that will hold the small dots (and spacers)
addSubview(theStack)
// the "big" tracking dot will be positioned behind a small dot, so we need to
// keep a reference to its centerXAnchor constraint
trackingDotCenterX = trackingDot.centerXAnchor.constraint(equalTo: theTrackingLine.centerXAnchor, constant: 0.0)
NSLayoutConstraint.activate([
theTrackingLine.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0.0),
theTrackingLine.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0.0),
theTrackingLine.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0, constant: -20.0),
theTrackingLine.heightAnchor.constraint(equalToConstant: 2.0),
theStack.centerXAnchor.constraint(equalTo: theTrackingLine.centerXAnchor, constant: 0.0),
theStack.centerYAnchor.constraint(equalTo: theTrackingLine.centerYAnchor, constant: 0.0),
theStack.widthAnchor.constraint(equalTo: theTrackingLine.widthAnchor, multiplier: 1.0, constant: 0.0),
trackingDotCenterX,
trackingDot.widthAnchor.constraint(equalToConstant: trackingDotWidth),
trackingDot.heightAnchor.constraint(equalTo: trackingDot.widthAnchor, multiplier: 1.0),
trackingDot.centerYAnchor.constraint(equalTo: theTrackingLine.centerYAnchor, constant: 0.0),
])
}
func setDots(with colors: [UIColor]) -> Void {
// remove any previous dots and spacers
// (in case we're changing the number of dots after creating the view)
theStack.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
// reset the array of dot views
// (in case we're changing the number of dots after creating the view)
dotViews = [DotView]()
// we're going to set all spacer views to equal widths, so use
// this var to hold a reference to the first one we create
var firstSpacer: UIView?
colors.forEach {
c in
// create a DotView
let v = DotView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = c
// add to array so we can reference it later
dotViews.append(v)
// add it to the stack view
theStack.addArrangedSubview(v)
// dots are round (equal width to height)
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalToConstant: dotWidth),
v.heightAnchor.constraint(equalTo: v.widthAnchor, multiplier: 1.0),
])
// we use 1 fewer spacers than dots, so if this is not the last dot
if c != colors.last {
// create a spacer (clear view)
let s = UIView()
s.translatesAutoresizingMaskIntoConstraints = false
s.backgroundColor = .clear
// add it to the stack view
theStack.addArrangedSubview(s)
if firstSpacer == nil {
firstSpacer = s
} else {
// we know it's not nil, but we have to unwrap it anyway
if let fs = firstSpacer {
NSLayoutConstraint.activate([
s.widthAnchor.constraint(equalTo: fs.widthAnchor, multiplier: 1.0),
])
}
}
}
}
}
}
class DotsViewController: UIViewController {
var theButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Move Tracking Dot", for: .normal)
v.setTitleColor(.white, for: .normal)
return v
}()
var theTrackingLineView: TrackingLineView = {
let v = TrackingLineView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
return v
}()
var trackingDots: [UIColor] = [
.yellow,
.red,
.orange,
.green,
.purple,
]
var currentTrackingPosition = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 1.0, green: 0.8, blue: 0.5, alpha: 1.0)
view.addSubview(theTrackingLineView)
NSLayoutConstraint.activate([
theTrackingLineView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
theTrackingLineView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0),
theTrackingLineView.heightAnchor.constraint(equalToConstant: 100.0),
theTrackingLineView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
])
theTrackingLineView.setDots(with: trackingDots)
theTrackingLineView.trackingPosition = currentTrackingPosition
// add a button so we can move the tracking dot
view.addSubview(theButton)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 40.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
theButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: Any) -> Void {
// if we're at the last dot, reset to 0
if currentTrackingPosition < trackingDots.count - 1 {
currentTrackingPosition += 1
} else {
currentTrackingPosition = 0
}
theTrackingLineView.trackingPosition = currentTrackingPosition
UIView.animate(withDuration: 0.25, animations: {
self.view.layoutIfNeeded()
})
}
}
The result:
i recommend you use a collection view inside table cell, so that way you can define the position with a simple validation
Related
My question and code is based on this answer to one of my previous questions. I have programmatically created stackview where several labels are stored and I'm trying to make these labels clickable. I tried two different solutions:
Make clickable label. I created function and assigned it to the label in the gesture recognizer:
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag)
}
but it does not work. Then below the second way....
I thought that maybe the 1st way does not work because the labels are in UIStackView so I decided to assign click listener to the stack view and then determine on which view we clicked. At first I assigned to each of labels in the stackview tag and listened to clicks:
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapCard(sender:)))
labelsStack.addGestureRecognizer(tap)
....
#objc func didTapCard (sender: UITapGestureRecognizer) {
(sender.view as? UIStackView)?.arrangedSubviews.forEach({ label in
print((label as! UILabel).text)
})
}
but the problem is that the click listener works only on the part of the stack view and when I tried to determine on which view we clicked it was not possible.
I think that possibly the problem is with that I tried to assign one click listener to several views, but not sure that works as I thought. I'm trying to make each label in the stackview clickable, but after click I will only need getting text from the label, so that is why I used one click listener for all views.
Applying a transform to a view (button, label, view, etc) changes the visual appearance, not the structure.
Because you're working with rotated views, you need to implement hit-testing.
Quick example:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
Assuming you're still working with the MyCustomView class and layout from your previous questions, we'll build on that with a few changes for layout, and to allow tapping the labels.
Complete example:
class Step5VC: UIViewController {
// create the custom "left-side" view
let myView = MyCustomView()
// create the "main" stack view
let mainStackView = UIStackView()
// create the "bottom labels" stack view
let bottomLabelsStack = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let img = UIImage(named: "pro1") else {
fatalError("Need an image!")
}
// create the image view
let imgView = UIImageView()
imgView.contentMode = .scaleToFill
imgView.image = img
mainStackView.axis = .horizontal
bottomLabelsStack.axis = .horizontal
bottomLabelsStack.distribution = .fillEqually
// add views to the main stack view
mainStackView.addArrangedSubview(myView)
mainStackView.addArrangedSubview(imgView)
// add main stack view and bottom labels stack view to view
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
bottomLabelsStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomLabelsStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain Top/Leading/Trailing
mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
//mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// we want the image view to be 270 x 270
imgView.widthAnchor.constraint(equalToConstant: 270.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
// constrain the bottom lables to the bottom of the main stack view
// same width as the image view
// aligned trailing
bottomLabelsStack.topAnchor.constraint(equalTo: mainStackView.bottomAnchor),
bottomLabelsStack.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
bottomLabelsStack.widthAnchor.constraint(equalTo: imgView.widthAnchor),
])
// setup the left-side custom view
myView.titleText = "Gefährdung"
let titles: [String] = [
"keine / gering", "mittlere", "erhöhte", "hohe",
]
let colors: [UIColor] = [
UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
]
for (c, t) in zip(colors, titles) {
// because we'll be using hitTest in our Custom View
// we don't need to set .isUserInteractionEnabled = true
// create a "color label"
let cl = colorLabel(withColor: c, title: t, titleColor: .black)
// we're limiting the height to 270, so
// let's use a smaller font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .light)
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapRotatedLeftLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
// add the label to the custom myView
myView.addLabel(cl)
}
// rotate the left-side custom view 90-degrees counter-clockwise
myView.rotateTo(-.pi * 0.5)
// setup the bottom labels
let colorDictionary = [
"Red":UIColor.systemRed,
"Green":UIColor.systemGreen,
"Blue":UIColor.systemBlue,
]
for (myKey,myValue) in colorDictionary {
// bottom labels are not rotated, so we can add tap gesture recognizer directly
// create a "color label"
let cl = colorLabel(withColor: myValue, title: myKey, titleColor: .white)
// let's use a smaller, bold font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .bold)
// by default, .isUserInteractionEnabled is False for UILabel
// so we must set .isUserInteractionEnabled = true
cl.isUserInteractionEnabled = true
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapBottomLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
bottomLabelsStack.addArrangedSubview(cl)
}
}
#objc func didTapRotatedLeftLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Label in Rotated Custom View:", title)
// do something based on the tapped label/view
}
}
#objc func didTapBottomLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Bottom Label:", title)
// do something based on the tapped label/view
}
}
func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
let newLabel = PaddedLabel()
newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
newLabel.backgroundColor = color
newLabel.text = title
newLabel.textAlignment = .center
newLabel.textColor = titleColor
newLabel.setContentHuggingPriority(.required, for: .vertical)
return newLabel
}
}
class MyCustomView: UIView {
public var titleText: String = "" {
didSet { titleLabel.text = titleText }
}
public func addLabel(_ v: UIView) {
labelsStack.addArrangedSubview(v)
}
public func rotateTo(_ d: Double) {
// get the container view (in this case, it's the outer stack view)
if let v = subviews.first {
// set the rotation transform
if d == 0 {
self.transform = .identity
} else {
self.transform = CGAffineTransform(rotationAngle: d)
}
// remove the container view
v.removeFromSuperview()
// tell it to layout itself
v.setNeedsLayout()
v.layoutIfNeeded()
// get the frame of the container view
// apply the same transform as self
let r = v.frame.applying(self.transform)
wC.isActive = false
hC.isActive = false
// add it back
addSubview(v)
// set self's width and height anchors
// to the width and height of the container
wC = self.widthAnchor.constraint(equalToConstant: r.width)
hC = self.heightAnchor.constraint(equalToConstant: r.height)
guard let sv = v.superview else {
fatalError("no superview")
}
// apply the new constraints
NSLayoutConstraint.activate([
v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC,
outerStack.widthAnchor.constraint(equalTo: sv.heightAnchor),
])
}
}
// our subviews
private let outerStack = UIStackView()
private let titleLabel = UILabel()
private let labelsStack = UIStackView()
private var wC: NSLayoutConstraint!
private var hC: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// stack views and label properties
outerStack.axis = .vertical
outerStack.distribution = .fillEqually
labelsStack.axis = .horizontal
// let's use .fillProportionally to help fit the labels
labelsStack.distribution = .fillProportionally
titleLabel.textAlignment = .center
titleLabel.backgroundColor = .lightGray
titleLabel.textColor = .white
// add title label and labels stack to outer stack
outerStack.addArrangedSubview(titleLabel)
outerStack.addArrangedSubview(labelsStack)
outerStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(outerStack)
wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)
NSLayoutConstraint.activate([
outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC, hC,
])
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
}
class PaddedLabel: UILabel {
var padding: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: padding))
}
override var intrinsicContentSize : CGSize {
let sz = super.intrinsicContentSize
return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
}
}
The problem is with the the stackView's height. Once the label is rotated, the stackview's height is same as before and the tap gestures will only work within stackview's bounds.
I have checked it by changing the height of the stackview at the transform and observed tap gestures are working fine with the rotated label but with the part of it inside the stackview.
Now the problem is that you have to keep the bounds of the label inside the stackview either by changing it axis(again a new problem as need to handle the layout with it) or you have to handle it without the stackview.
You can check the observation by clicking the part of rotated label inside stackview and outside stackview.
Code to check it:
class ViewController: UIViewController {
var centerLabel = UILabel()
let mainStackView = UIStackView()
var stackViewHeightCons:NSLayoutConstraint?
var stackViewTopsCons:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
mainStackView.axis = .horizontal
mainStackView.alignment = .top
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackViewTopsCons = mainStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300)
stackViewTopsCons?.isActive = true
stackViewHeightCons = mainStackView.heightAnchor.constraint(equalToConstant: 30)
stackViewHeightCons?.isActive = true
centerLabel.textAlignment = .center
centerLabel.text = "Let's rotate this label"
centerLabel.backgroundColor = .green
centerLabel.tag = 11
setTapListener(centerLabel)
mainStackView.addArrangedSubview(centerLabel)
// outline the stack view so we can see its frame
mainStackView.layer.borderColor = UIColor.red.cgColor
mainStackView.layer.borderWidth = 1
}
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag ?? 0)
var yCor:CGFloat = 300
if centerLabel.transform == .identity {
centerLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
yCor = mainStackView.frame.origin.y - (centerLabel.frame.size.height/2)
} else {
centerLabel.transform = .identity
}
updateStackViewHeight(topCons: yCor)
}
private func updateStackViewHeight(topCons:CGFloat) {
stackViewTopsCons?.constant = topCons
stackViewHeightCons?.constant = centerLabel.frame.size.height
}
}
Sorry. My assumption was incorrect.
Why are you decided to use Label instead of UIButton (with transparence background color and border line)?
Also you can use UITableView instead of stack & labels
Maybe this documentation will help too (it is written that usually in one view better to keep one gesture recognizer): https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers
I have a view controller with the below UI layout.
There is a header view at the top with 3 labels, a footer view with 2 buttons at the bottom and an uitableview inbetween header view and footer view. The uitableview is dynamically loaded and on average has about 6 tableview cells. One of the buttons in the footer view is take screenshot button where i need to take the screenshot of full tableview. In small devices like iPhone 6, the height of the table is obviously small as it occupies the space between header view and footer view. So only 4 cells are visible to the user and as the user scrolls others cells are loaded into view. If the user taps take screen shot button without scrolling the table view, the last 2 cells are not captured in the screenshot. The current implementation tried to negate this by changing table view frame to table view content size before capturing screenshot and resetting frame after taking screenshot, but this approach is not working starting iOS 13 as the table view content size returns incorrect values.
Current UI layout implementation
Our first solution is to embed the tableview inside the scrollview and have the tableview's scroll disabled. By this way the tableview will be forced to render all cells at once. We used the below custom table view class to override intrinsicContentSize to make the tableview adjust itself to correct height based on it contents
class CMDynamicHeightAdjustedTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
}
Proposed UI implementation
But we are little worried about how overriding intrinsicContentSize could affect performance and other apple's internal implementations
So our second solution is to set a default initial height constraint for tableview and observe the tableview's content size keypath and update the tableview height constraint accordingly. But the content size observer gets called atleast 12-14 times before the screen elements are visible to the user.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.confirmationTableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
if object is UITableView {
if let newvalue = change?[.newKey], let newSize = newvalue as? CGSize {
self.confirmationTableViewHeightConstraint.constant = newSize.height
}
}
}
}
Will the second approach impact performance too?
What is the better approach of the two?
Is there any alternate solution?
I am not sure, but if I understood correctly when you screenshot the TableView the last 2 cells are not loaded because of the tableview being between the Header and Footer. Here are two options I would consider:
Option 1
Try to make the TableView frame start from the Header and have the height of the Unscreen.main.bounds.height - the Header view frame. This would mean that the tableView will expand toward the end of the screen. Then add the Footer over the tableView in the desired relation.
Option 2
Try before screenshooting, to reloadRows at two level below the current Level. You can get the current indexPath of the UITableView, when the TableView reloads it from its delegate, store it somewhere always the last indexPath used, and when screenshot reload the two below.
You can "temporarily" change the height of your table view, force it to update, render it to a UIImage, and then set the height back.
Assuming you have your "Header" view constrained to the top, your "Footer" view constrained to the bottom, and your table view constrained between them...
Add a class var/property for the table view's bottom constraint:
var tableBottomConstraint: NSLayoutConstraint!
then set that constraint:
tableBottomConstraint = tableView.bottomAnchor.constraint(equalTo: footerView.topAnchor, constant: 0.0)
When you want to "capture" the table:
func captureTableView() -> UIImage {
// save the table view's bottom constraint's constant
// and the contentOffset y position
let curConstant = tableBottomConstraint.constant
let curOffset = tableView.contentOffset.y
// make table view really tall, to guarantee all rows will fit
tableBottomConstraint.constant = 20000
// force it to update
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
UIGraphicsBeginImageContextWithOptions(tableView.contentSize, false, UIScreen.main.scale)
tableView.layer.render(in: UIGraphicsGetCurrentContext()!)
// get the image
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext();
// set table view state back to what it was
tableBottomConstraint.constant = curConstant
tableView.contentOffset.y = curOffset
return image
}
Here is a complete example you can run to test it:
class SimpleCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.backgroundColor = .yellow
return v
}()
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() {
theLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
class TableCapVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
// let's use 12 rows, each with 1, 2, 3 or 4 lines of text
// so it will definitely be too many rows to see on the screen
let numRows: Int = 12
var tableBottomConstraint: NSLayoutConstraint!
// we'll use this to display that captured table view image
let resultHolder = UIView()
let resultImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let headerView = myHeaderView()
let footerView = myFooterView()
[headerView, tableView, footerView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
// we will use this to change the bottom constraint of the table view
// when we want to capture it
tableBottomConstraint = tableView.bottomAnchor.constraint(equalTo: footerView.topAnchor, constant: 0.0)
NSLayoutConstraint.activate([
// constrain "header" view at the top
headerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
headerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
headerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain "fotter" view at the bottom
footerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
footerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
footerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain table view between header and footer views
tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 0.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
tableBottomConstraint,
])
tableView.register(SimpleCell.self, forCellReuseIdentifier: "c")
tableView.dataSource = self
tableView.delegate = self
// we'll add a UIImageView (in a "holder" view) on top of the table
// then show/hide it to see the results of
// the table capture
resultImageView.backgroundColor = .gray
resultImageView.layer.borderColor = UIColor.cyan.cgColor
resultImageView.layer.borderWidth = 1
resultImageView.layer.cornerRadius = 16.0
resultImageView.layer.shadowColor = UIColor.black.cgColor
resultImageView.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
resultImageView.layer.shadowRadius = 8
resultImageView.layer.shadowOpacity = 0.9
resultImageView.contentMode = .scaleAspectFit
resultHolder.alpha = 0.0
resultHolder.translatesAutoresizingMaskIntoConstraints = false
resultImageView.translatesAutoresizingMaskIntoConstraints = false
resultHolder.addSubview(resultImageView)
view.addSubview(resultHolder)
NSLayoutConstraint.activate([
// cover everything with the clear "holder" view
resultHolder.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
resultHolder.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
resultHolder.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
resultHolder.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
resultImageView.topAnchor.constraint(equalTo: resultHolder.topAnchor, constant: 20.0),
resultImageView.leadingAnchor.constraint(equalTo: resultHolder.leadingAnchor, constant: 20.0),
resultImageView.trailingAnchor.constraint(equalTo: resultHolder.trailingAnchor, constant: -20.0),
resultImageView.bottomAnchor.constraint(equalTo: resultHolder.bottomAnchor, constant: -20.0),
])
// tap image view / holder view when showing to hide it
let t = UITapGestureRecognizer(target: self, action: #selector(hideImage))
resultHolder.addGestureRecognizer(t)
}
func myHeaderView() -> UIView {
let v = UIView()
v.backgroundColor = .systemBlue
let sv = UIStackView()
sv.axis = .vertical
sv.spacing = 4
let strs: [String] = [
"\"Header\" and \"Footer\" views",
"are separate views - they are not",
".tableHeaderView / .tableFooterView",
]
strs.forEach { str in
let label = UILabel()
label.text = str
label.textAlignment = .center
label.font = .systemFont(ofSize: 13.0, weight: .regular)
label.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
sv.addArrangedSubview(label)
}
sv.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: v.topAnchor, constant: 8.0),
sv.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0),
sv.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0),
sv.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -8.0),
])
return v
}
func myFooterView() -> UIView {
let v = UIView()
v.backgroundColor = .systemPink
let sv = UIStackView()
sv.axis = .horizontal
sv.spacing = 12
sv.distribution = .fillEqually
let btn1: UIButton = {
var cfg = UIButton.Configuration.filled()
cfg.title = "Capture Table"
let b = UIButton(configuration: cfg)
b.addTarget(self, action: #selector(btn1Action(_:)), for: .touchUpInside)
return b
}()
let btn2: UIButton = {
var cfg = UIButton.Configuration.filled()
cfg.title = "Another Button"
let b = UIButton(configuration: cfg)
b.addTarget(self, action: #selector(btn2Action(_:)), for: .touchUpInside)
return b
}()
sv.addArrangedSubview(btn1)
sv.addArrangedSubview(btn2)
sv.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: v.topAnchor, constant: 8.0),
sv.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0),
sv.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0),
sv.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -8.0),
])
return v
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! SimpleCell
let nLines = indexPath.row % 4
var s: String = "Row: \(indexPath.row)"
for i in 0..<nLines {
s += "\nLine \(i+2)"
}
c.theLabel.text = s
return c
}
#objc func btn1Action(_ sender: UIButton) {
let img = captureTableView()
print("TableView Image Captured - size:", img.size)
// do something with the tableView capture
// maybe save it to documents folder?
// for this example, we will show it
resultImageView.image = img
UIView.animate(withDuration: 0.5, animations: {
self.resultHolder.alpha = 1.0
})
}
#objc func hideImage() {
UIView.animate(withDuration: 0.5, animations: {
self.resultHolder.alpha = 0.0
})
}
#objc func btn2Action(_ sender: UIButton) {
print("Another Button Tapped")
}
func captureTableView() -> UIImage {
// save the table view's bottom constraint's constant
// and the contentOffset y position
let curConstant = tableBottomConstraint.constant
let curOffset = tableView.contentOffset.y
// make table view really tall, to guarantee all rows will fit
tableBottomConstraint.constant = 20000
// force it to update
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
UIGraphicsBeginImageContextWithOptions(tableView.contentSize, false, UIScreen.main.scale)
tableView.layer.render(in: UIGraphicsGetCurrentContext()!)
// get the image
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext();
// set table view state back to what it was
tableBottomConstraint.constant = curConstant
tableView.contentOffset.y = curOffset
return image
}
}
We give the table 12 rows, each with 1, 2, 3 or 4 lines of text so it will definitely be too many rows to see on the screen. Tapping on the "Capture Table" button will capture the table to a UIImage and then display that image. Tap on the image to dismiss it:
I have a simple UICollectionView in a view controller. I am animating the top constraint of the collection view via a button. On the FIRST button tap, the collection view cells are animating quite oddly. After subsequent taps the animation is smooth.
Method to animate:
#objc func animateAction() {
UIView.animate(withDuration: 1) {
self.animateUp.toggle()
self.topConstraint.constant = self.animateUp ? 100 : self.view.bounds.height - 100
self.view.layoutIfNeeded()
}
}
Edit: What actually needs to be built:
It looks like you are animating the Top Constraint of your collection view, which changes its Height.
Collection view's only render cells when needed.
So, at the start only one (or two) cells are created. Then as you change the Height, new cells are created and added. So, you see an "odd animation."
What you want to do is NOT set a bottom constraint for your collection view. Instead, set its Height constraint, and then change the Top constraint to "slide" it up and down:
I'm assuming you're using UICollectionViewCompositionalLayout.list with appearance: .insetGrouped ...
Here is a complete example to get that result:
struct MyCVData: Hashable {
var name: String
}
class AnimCVViewController: UIViewController {
var myCollectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, MyCVData>!
var cvDataList: [MyCVData] = []
enum Section {
case main
}
var snapshot: NSDiffableDataSourceSnapshot<Section, MyCVData>!
var topConstraint: NSLayoutConstraint!
// when collection view is "Up" we want its
// Top to be 100-points from the Top of the view (safe area)
var topPosition: CGFloat = 100
// when collection view is "Down" we want its
// Top to be 80-points from the Bottom of the view (safe area)
var bottomPosition: CGFloat = 80
override func viewDidLoad() {
super.viewDidLoad()
// so we have a title if we're in a navigation controller
self.navigationController?.setNavigationBarHidden(true, animated: false)
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
configureCollectionView()
buildData()
// create an Animate button
let btn = UIButton()
btn.backgroundColor = .yellow
btn.setTitle("Animate", for: [])
btn.setTitleColor(.black, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.translatesAutoresizingMaskIntoConstraints = false
myCollectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
view.addSubview(myCollectionView)
let g = view.safeAreaLayoutGuide
// start with the collection view "Down"
topConstraint = myCollectionView.topAnchor.constraint(equalTo: g.bottomAnchor, constant: -bottomPosition)
NSLayoutConstraint.activate([
// constrain the button at the Top, 200-pts width, centered horizontally
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
btn.widthAnchor.constraint(equalToConstant: 200.0),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// button Height 10-points less than our collection view's Top Position
btn.heightAnchor.constraint(equalToConstant: topPosition - 10.0),
// activate top constraint
topConstraint,
// collection view Height should be the Height of the view (safe area)
// minus the Top Position
myCollectionView.heightAnchor.constraint(equalTo: g.heightAnchor, constant: -topPosition),
// let's use 40-points leading and trailing
myCollectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
myCollectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
// add an action for the button
btn.addTarget(self, action: #selector(animateAction), for: .touchUpInside)
}
#objc func animateAction() {
// if the topConstraint constant is -bottomPosition, that means it is "Down"
// so, if it's "Down"
// animate it so its Top is its own Height from the Bottom
// otherwise
// animate it so its Top is at bottomPosition
topConstraint.constant = topConstraint.constant == -bottomPosition ? -myCollectionView.frame.height : -bottomPosition
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
}
func configureCollectionView() {
var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
layoutConfig.backgroundColor = .red
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
myCollectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout)
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, MyCVData> { (cell, indexPath, item) in
var content = UIListContentConfiguration.cell()
content.text = item.name
content.textProperties.font.withSize(8.0)
content.textProperties.font = UIFont.preferredFont(forTextStyle: .body)
content.textProperties.adjustsFontSizeToFitWidth = false
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource<Section, MyCVData>(collectionView: myCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: MyCVData) -> UICollectionViewCell? in
// Dequeue reusable cell using cell registration (Reuse identifier no longer needed)
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
for: indexPath,
item: identifier)
return cell
}
}
func buildData() {
// create 20 data items ("Cell: 1" / "Cell: 2" / "Cell: 3" / etc...)
for i in 0..<20 {
let d = MyCVData(name: "Cell: \(i)")
cvDataList.append(d)
}
// Create a snapshot that define the current state of data source's data
self.snapshot = NSDiffableDataSourceSnapshot<Section, MyCVData>()
self.snapshot.appendSections([.main])
self.snapshot.appendItems(cvDataList, toSection: .main)
// Display data in the collection view by applying the snapshot to data source
self.dataSource.apply(self.snapshot, animatingDifferences: false)
}
}
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.
I'm still a SO and Swift newbie, so please, be patient and feel free to skip this question :-)
In the body of a XIB's awakeFromNib, I want to load some views as subviews of a UIScrollView (basically, the XIB contains a scrollview, a label and a button).
The scrollview perfectly works if in a loop I load views I create on the fly, eg.
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 150))
customView.frame = CGRect(x: i*300 , y: 0, width: 300, height: 150)
customView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(customView)
But I have a different goal.
In another XIB I have an image view and a stackview with some labels. This XIB is connected in the storyboard to a class SingleEvent that extends UIView.
I want to do the following:
use the XIB as a sort of "blueprint" and load the same view multiple times in my scrollview;
pass to any instance some data;
Is this possible?
I tried to load the content of the XIB this way:
let customView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as? SingleEvent
and this way:
let customView = SingleEvent()
The first one makes the app crash, while the second causes no issue, but I can't see any effect (it doesn't load anything).
The content of my latest SingleEvent is the following:
import UIKit
class SingleEvent: UIView {
#IBOutlet weak var label:UILabel!
#IBOutlet weak var imageView:UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
loadViewFromNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadViewFromNib()
}
func loadViewFromNib() -> UIView {
let myView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as! UIView
return myView
}
}
Thanks in advance, any help is appreciated :-)
There are a number of approaches to loading custom views (classes) from xibs. You may find this method a bit easier.
First, create your xib like this:
Note that the Class of File's Owner is the default (NSObject).
Instead, assign your custom class to the "root" view in your xib:
Now, our entire custom view class looks like this:
class SingleEvent: UIView {
#IBOutlet var topLabel: UILabel!
#IBOutlet var middleLabel: UILabel!
#IBOutlet var bottomLabel: UILabel!
#IBOutlet var imageView: UIImageView!
}
And, instead of putting loadNibNamed(...) inside our custom class, we create a UIView extension:
extension UIView {
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
}
To load and use our custom class, we can do this:
class FromXIBViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// create an instance of SingleEvent from its xib/nib
let v = UIView.fromNib() as SingleEvent
// we're going to use auto-layout & constraints
v.translatesAutoresizingMaskIntoConstraints = false
// set the text of the labels
v.topLabel?.text = "Top Label"
v.middleLabel?.text = "Middle Label"
v.bottomLabel?.text = "Bottom Label"
// set the image
v.imageView.image = UIImage(named: "myImage")
// add the SingleEvent view
view.addSubview(v)
// constrain it 200 x 200, centered X & Y
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalToConstant: 200.0),
v.heightAnchor.constraint(equalToConstant: 200.0),
v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
With a result of:
And... here is an example of loading 10 instances of SingleEvent view and adding them to a vertical scroll view:
class FromXIBViewController: UIViewController {
var theScrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
var theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20.0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scroll view to the view
view.addSubview(theScrollView)
// constrain it 40-pts on each side
NSLayoutConstraint.activate([
theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
])
// add a stack view to the scroll view
theScrollView.addSubview(theStackView)
// constrain it 20-pts on each side
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 20.0),
theStackView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: -20.0),
theStackView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 20.0),
theStackView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: -20.0),
// stackView width = scrollView width -40 (20-pts padding on left & right
theStackView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, constant: -40.0),
])
for i in 0..<10 {
// create an instance of SingleEvent from its xib/nib
let v = UIView.fromNib() as SingleEvent
// we're going to use auto-layout & constraints
v.translatesAutoresizingMaskIntoConstraints = false
// set the text of the labels
v.topLabel?.text = "Top Label: \(i)"
v.middleLabel?.text = "Middle Label: \(i)"
v.bottomLabel?.text = "Bottom Label: \(i)"
// set the image (assuming we have images named myImage0 thru myImage9
v.imageView.image = UIImage(named: "myImage\(i)")
theStackView.addArrangedSubview(v)
}
}
}
Result:
Ok, I see. The problem probably in fact that loadViewFromNib function return UIView from xib, but you doesn't use it any way.
Let's try this way:
1) Make your loadViewFromNib function static
// Return our SingleEvent instance here
static func loadViewFromNib() -> SingleEvent {
let myView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as! SingleEvent
return myView
}
2) Remove all inits in SingleEvent class
3) Init it in needed place like this:
let customView = SingleView.loadViewFromNib()
To pass data inside view you can create new function in SingleView class:
func configureView(with dataModel:DataModel) {
//Set data to IBOutlets here
}
And use it from outside like this:
let customView = SingleView.loadViewFromNib()
let dataModel = DataModel()
customView.configureView(with: dataModel)