I want to have a UILabel in the middle of the screen that starts from the left of the screen. The UILabel will get updated every second from a Timer to simulate a typewriter effect. So to display "Album releases", the UILabel will show this series of text:
A
Al
Alb
Albu
Album
Album
Album r
Album re
Album rel
Album rele
Album relea
Album releas
Album release
Album releases
The label has a black background. So I want to make sure the width fits the text. But if the text is too long then the width should go up to the screen width and then breaks into a second line. I tried using numberOfLines = 0 and sizeToFit to achieve that. But the issue is that for some reason, one letter is being written on a line. I cannot manage to fix that. Here's some code:
private let taglinesLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.textColor = .white
label.backgroundColor = .black
label.numberOfLines = 0
return label
}()
override func layoutSubviews() {
...
taglinesLabel.frame = CGRect(x: 0, y: 200, width: 0, height: 0)
...
}
public func showText() {
...
startTimer()
...
}
func startTimer () {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(0.1),
target : self,
selector : #selector(onTimer),
userInfo : nil,
repeats : true)
}
#objc private func onTimer() {
let tagline = taglines[taglineIndex]
if(taglineCharacterIndex < tagline.count) {
let substring = tagline.prefix(taglineCharacterIndex)
print(substring)
taglinesLabel.text = String(substring)
taglinesLabel.sizeToFit()
taglineCharacterIndex += 1
}
}
How to constraint the UILabel to expand its width and not go to the next line like shown in the picture?
Couple things...
First, use auto-layout and constraints instead of .sizeToFit().
Second, let's replace the UILabel with a non-editable, non-scrolling UITextView because:
It gives us a visually nice "padding" around the text
It keeps the text at the top (if we have set a height for the view)
It avoids a weird "line jumping" when the text wraps
So, start with an example controller that puts an instance of our custom TypingView 40-points from the Top and from each side. We'll also give it four sample "taglines" and we'll start the timer in viewDidAppear:
class ViewController: UIViewController {
let testView = TypingView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// no bottom or height constraint
// we let the TypingView set its own height
])
testView.taglines = [
"First sample string.",
"This is the Second sample tagline.",
"This tagline will be long enough that it will wrap onto at least two lines.",
"Here is the final tagline.",
]
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
testView.startTimer()
}
}
Now, our custom UIView subclass:
class TypingView: UIView {
public var taglines: [String] = []
private var taglineIndex: Int = 0
private var timer: Timer!
private var taglineCharacterIndex: Int = 0
private let taglinesLabel: UITextView = {
let v = UITextView()
v.font = .systemFont(ofSize: 17.0, weight: .regular)
v.textColor = .white
v.backgroundColor = .black
v.isScrollEnabled = false
v.isEditable = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
backgroundColor = .black
taglinesLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(taglinesLabel)
NSLayoutConstraint.activate([
taglinesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
taglinesLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
taglinesLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
taglinesLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
func startTimer () {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(0.1),
target : self,
selector : #selector(onTimer),
userInfo : nil,
repeats : true)
}
#objc private func onTimer() {
guard taglineIndex < taglines.count else {
timer.invalidate()
timer = nil
return
}
let tagline = taglines[taglineIndex]
if taglineCharacterIndex < tagline.count + 1 {
let substring = tagline.prefix(taglineCharacterIndex)
taglinesLabel.text = String(substring)
taglineCharacterIndex += 1
} else if taglineCharacterIndex < tagline.count + 6 {
// this will provide a half-second "pause" before going to the next tagline
taglineCharacterIndex += 1
} else {
taglineIndex += 1
taglineCharacterIndex = 0
}
}
}
It will look like this (adding 1 character every 1/10th second):
and with enough text to wrap:
Edit - in response to comment...
To get the view to expand horizontally as the text gets longer, change the trailing constraint (in ViewController) from:
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
to:
testView.trailingAnchor.constraint(lessThanOrEqualTo: g.trailingAnchor, constant: -40.0),
Now it will grow wider with each character, but only until it reaches 40-points from the right side.
You could also replace the trailing constraint with a width constraint if that would better suit your needs.
For example:
testView.widthAnchor.constraint(lessThanOrEqualToConstant: 240.0),
would limit its width to 240-points.
Related
I have a simple component, a label and an imageview.
They are put in horizontal position. I need to center that and if my label gets more text, the component will change too and the position of my image will change. I have to do this programmatically and I’m really new to programmatically constraints.
For more explanation this is an example:
And this is my code right now:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
progressText.sizeToFit()
progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_success")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView.image = resizedImage
iconView.sizeToFit()
iconView.layoutIfNeeded()
return iconView
}()
let iconView2: UIImageView = {
let iconView2 = UIImageView()
iconView2.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_error")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView2.image = resizedImage
iconView2.sizeToFit()
iconView2.layoutIfNeeded()
return iconView2
}()
private func setupView() {
let overlayView = UIView()
overlayView.translatesAutoresizingMaskIntoConstraints = false
addSubview(overlayView)
overlayView.addSubview(iconView)
overlayView.addSubview(iconView2)
NSLayoutConstraint.activate([
iconView.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
iconView.centerYAnchor.constraint(equalTo: iconView.centerYAnchor),
progressIndicator.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
progressIndicator.centerYAnchor.constraint(equalTo: iconView2.centerYAnchor),
iconView2.topAnchor.constraint(equalTo: self.topAnchor, constant: 20),
iconView2.widthAnchor.constraint(equalToConstant: 10),
iconView2.heightAnchor.constraint(equalToConstant: 10),
iconView2.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -10)
])
addSubview(progressText)
NSLayoutConstraint.activate([
progressText.leadingAnchor.constraint(equalTo: overlayView.trailingAnchor, constant: 15),
progressText.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor)
])
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Also, this is how I set this in a parent view:
private func setupProgressBar() {
self.codeValidationTextField.addSubview(progressBar)
progressBar.translatesAutoresizingMaskIntoConstraints = false
progressBar.leadingAnchor.constraint(equalTo: codeValidationTextField.leadingAnchor, constant: 10).isActive = true
progressBar.bottomAnchor.constraint(equalTo: codeValidationTextField.bottomAnchor, constant: -33).isActive = true
progressBar.widthAnchor.constraint(equalTo: codeValidationTextField.widthAnchor, multiplier: 0.5)
progressBar.progressIndicator.isHidden = false
progressBar.iconView.isHidden = true
progressBar.progressText.isHidden = false
}
It's not entirely clear what you are trying to do...
your code references a progressIndicator when setting constraints, but there is no progressIndicator identified.
there is no reason to be "scaling" your images... the image view will do that for you.
you're adding 2 "icon" image views, but the layout doesn't make much sense.
there is no need to use a overlayView -- we can add the subviews to the custom view itself (and, the code you've shown doesn't setup that overlayView correctly, so no idea if it is supposed to be doing anything).
Note that it helps to include comments in your code, telling yourself what you expect to happen.
And, a tip: during development, give your UI elements contrasting background colors to make it easy to see the framing.
So, let's simplify this and start with a single image view and label:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
// no need for any of this
//progressText.sizeToFit()
//progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
if let image = UIImage(named: "feedback_success") {
iconView.image = image
} else {
// could not load image, so set background color
iconView.backgroundColor = .systemRed
}
// no need for any of this
//let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
//let resizedImage = renderer.image { _ in
// image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
//}
//iconView.image = resizedImage
//iconView.sizeToFit()
//iconView.layoutIfNeeded()
return iconView
}()
private func setupView() {
// no need for this... add subviews to self
//let overlayView = UIView()
//overlayView.translatesAutoresizingMaskIntoConstraints = false
//addSubview(overlayView)
self.addSubview(iconView)
self.addSubview(progressText)
NSLayoutConstraint.activate([
// 20-points on top and bottom of iconView
iconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.0),
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.0),
// align iconView to leading-edge of self
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
// iconView 16 x 16
iconView.widthAnchor.constraint(equalToConstant: 16),
iconView.heightAnchor.constraint(equalToConstant: 16),
// progressText centered vertically
progressText.centerYAnchor.constraint(equalTo: self.centerYAnchor),
// progressText 15-points from iconView trailing edge
progressText.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 15.0),
// align progressText to trailing-edge of self
progressText.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
])
// during development, use some background colors so we can see the framing
progressText.backgroundColor = .cyan
self.backgroundColor = .systemGreen
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
}
And an example view controller showing it. Each tap anywhere in the view will cycle through several "example" progress strings:
class PVTestViewController: UIViewController {
let progressBar = PVProgressIndicator()
let sampleStrings: [String] = [
"Short",
"Medium Length",
"A Longer Sample String",
"This one should be long enough that it will word wrap onto more than one line.",
"Let's make this sample string\nuse\n4 lines\nof text.",
"If the progress text might be very, very long... so long that it could wrap onto more than four lines (like this), then some additional constraints will need to be set on the elements in the progressBar view.",
]
var sampleIDX: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
progressBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressBar)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain progressBar Top 20-points from top of safe area
progressBar.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// centered horizontally
progressBar.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// let's set the max width to 75% of safe area width
progressBar.widthAnchor.constraint(lessThanOrEqualTo: g.widthAnchor, multiplier: 0.75),
// progressBar sets it's own height
])
updateProgress()
// instruction label
let v = UILabel()
v.text = "Tap anywhere to cycle through the sample progress strings."
v.numberOfLines = 0
v.textAlignment = .center
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
func updateProgress() {
progressBar.progressText.text = sampleStrings[sampleIDX % sampleStrings.count]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
sampleIDX += 1
updateProgress()
}
}
When you start, it will look like this:
Each tap cycles to the next sample string...
I recently started with iOS development, and I'm currently working on an existing iOS Swift app with the intention of adding additional functionality. The current view contains a custom header and footer view, and the idea is for me to add the new slider with discrete steps in between, which worked. However, now I would also like to add labels to describe the discrete UISlider, for example having "Min" and "Max" to the left and right respectively, as well as the value of current value of the slider:
To achieve this, I was thinking to define a UITableView and a custom cell where I would insert the slider, while the labels could be defined in a row above or below the slider row. In my recent attempt I tried to define the table view and simply add the same slider element to a row, but I'm unsure how to proceed.
In addition, there is no Storyboard, everything has to be done programatically. Here is the sample code for my current version:
Slider and slider view definition:
private var sliderView = UIView()
private var discreteSlider = UISlider()
private let step: Float = 1 // for UISlider to snap in steps
Table view definition:
// temporary table view rows. For testing the table view
private let myArray: NSArray = ["firstRow", "secondRow"]
private lazy var tableView: UITableView = {
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height / 3
let yPos = headerHeight
myTableView = UITableView(frame: CGRect(x: 0, y: yPos, width: displayWidth, height: displayHeight))
myTableView.backgroundColor = .clear
myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
myTableView.dataSource = self
myTableView.delegate = self
return myTableView
}()
Loading the views:
private func setUpView() {
// define slider
discreteSlider = UISlider(frame:CGRect(x: 0, y: 0, width: 250, height: 20))
// define slider properties
discreteSlider.center = self.view.center
discreteSlider.minimumValue = 1
discreteSlider.maximumValue = 5
discreteSlider.isContinuous = true
discreteSlider.tintColor = UIColor.purple
// add behavior
discreteSlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
sliderView.addSubviews(discreteSlider) // add the slider to its view
UIView.animate(withDuration: 0.8) {
self.discreteSlider.setValue(2.0, animated: true)
}
//////
// Add the slider, labels to table rows here
// Add the table view to the main view
view.addSubviews(headerView, tableView, footerView)
//////
//current version without the table
//view.addSubviews(headerView, sliderView, footerView)
headerView.title = "View Title". // header configuration
}
Class extension for the table view:
extension MyViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Num: \(indexPath.row)")
print("Value: \(myArray[indexPath.row])")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath)
cell.textLabel!.text = "\(myArray[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
Furthermore, if there is a better solution that the UITableView approach, I would be willing to try. I also started to look over UICollectionView. Thanks!
While you could put these elements in different rows / cells of a table view, that's not what table views are designed for and there is a much better approach.
Create a UIView subclass and use auto-layout constraints to position the elements:
We use a horizontal UIStackView for the "step" labels... Distribution is set to .equalSpacing and we constrain the labels to all be equal widths.
We constrain the slider above the stack view, constraining its Leading and Trailing to the centerX of the first and last step labels (with +/- offsets for the width of the thumb).
We constrain the centerX of the Min and Max labels to the centerX of the first and last step labels.
Here is an example:
class MySliderView: UIView {
private var discreteSlider = UISlider()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let minVal: Int = 1
let maxVal: Int = 5
// slider properties
discreteSlider.minimumValue = Float(minVal)
discreteSlider.maximumValue = Float(maxVal)
discreteSlider.isContinuous = true
discreteSlider.tintColor = UIColor.purple
let stepStack = UIStackView()
stepStack.distribution = .equalSpacing
for i in minVal...maxVal {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.textColor = .systemRed
stepStack.addArrangedSubview(v)
}
// references to first and last step label
guard let firstLabel = stepStack.arrangedSubviews.first,
let lastLabel = stepStack.arrangedSubviews.last
else {
// this will never happen, but we want to
// properly unwrap the labels
return
}
// make all step labels the same width
stepStack.arrangedSubviews.dropFirst().forEach { v in
v.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}
let minLabel = UILabel()
minLabel.text = "Min"
minLabel.textAlignment = .center
minLabel.textColor = .systemRed
let maxLabel = UILabel()
maxLabel.text = "Max"
maxLabel.textAlignment = .center
maxLabel.textColor = .systemRed
// add the labels and the slider to self
[minLabel, maxLabel, discreteSlider, stepStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
// now we setup the layout
NSLayoutConstraint.activate([
// start with the step labels stackView
// we'll give it 40-pts leading and trailing "padding"
stepStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40.0),
stepStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40.0),
// and 20-pts from the bottom
stepStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// now constrain the slider leading and trailing to the
// horizontal center of first and last step labels
// accounting for width of thumb (assuming a default UISlider)
discreteSlider.leadingAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: -14.0),
discreteSlider.trailingAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 14.0),
// and 20-pts above the steps stackView
discreteSlider.bottomAnchor.constraint(equalTo: stepStack.topAnchor, constant: -20.0),
// constrain Min and Max labels centered to first and last step labels
minLabel.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: 0.0),
maxLabel.centerXAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 0.0),
// and 20-pts above the steps slider
minLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
maxLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
// and 20-pts top "padding"
minLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
])
// add behavior
discreteSlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
discreteSlider.addTarget(self, action: #selector(self.sliderThumbReleased(_:)), for: .touchUpInside)
}
// so we can set the slider value from the controller
public func setSliderValue(_ val: Float) -> Void {
discreteSlider.setValue(val, animated: true)
}
#objc func sliderValueDidChange(_ sender: UISlider) -> Void {
print("Slider dragging value:", sender.value)
}
#objc func sliderThumbReleased(_ sender: UISlider) -> Void {
// "snap" to discreet step position
sender.setValue(Float(lroundf(sender.value)), animated: true)
print("Slider dragging end value:", sender.value)
}
}
and it ends up looking like this:
Note that the target action for the slider value change is contained inside our custom class.
So, we need to provide functionality so our class can inform the controller when the slider value has changed.
The best way to do that is with closures...
We'll define the closures at the top of our MySliderView class:
class MySliderView: UIView {
// this closure will be used to inform the controller that
// the slider value changed
var sliderDraggingClosure: ((Float)->())?
var sliderReleasedClosure: ((Float)->())?
then in our slider action funcs, we can use that closure to "call back" to the controller:
#objc func sliderValueDidChange(_ sender: UISlider) -> Void {
// tell the controller
sliderDraggingClosure?(sender.value)
}
#objc func sliderThumbReleased(_ sender: UISlider) -> Void {
// "snap" to discreet step position
sender.setValue(Float(lroundf(sender.value)), animated: true)
// tell the controller
sliderReleasedClosure?(sender.value)
}
and then in our view controller's viewDidLoad() func, we setup the closures:
// set the slider closures
mySliderView.sliderDraggingClosure = { [weak self] val in
print("Slider dragging value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
mySliderView.sliderReleasedClosure = { [weak self] val in
print("Slider dragging end value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
Here's the complete modified class (Edited to include Tap behavior):
class MySliderView: UIView {
// this closure will be used to inform the controller that
// the slider value changed
var sliderDraggingClosure: ((Float)->())?
var sliderReleasedClosure: ((Float)->())?
private var discreteSlider = UISlider()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let minVal: Int = 1
let maxVal: Int = 5
// slider properties
discreteSlider.minimumValue = Float(minVal)
discreteSlider.maximumValue = Float(maxVal)
discreteSlider.isContinuous = true
discreteSlider.tintColor = UIColor.purple
let stepStack = UIStackView()
stepStack.distribution = .equalSpacing
for i in minVal...maxVal {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.textColor = .systemRed
stepStack.addArrangedSubview(v)
}
// references to first and last step label
guard let firstLabel = stepStack.arrangedSubviews.first,
let lastLabel = stepStack.arrangedSubviews.last
else {
// this will never happen, but we want to
// properly unwrap the labels
return
}
// make all step labels the same width
stepStack.arrangedSubviews.dropFirst().forEach { v in
v.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}
let minLabel = UILabel()
minLabel.text = "Min"
minLabel.textAlignment = .center
minLabel.textColor = .systemRed
let maxLabel = UILabel()
maxLabel.text = "Max"
maxLabel.textAlignment = .center
maxLabel.textColor = .systemRed
// add the labels and the slider to self
[minLabel, maxLabel, discreteSlider, stepStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
// now we setup the layout
NSLayoutConstraint.activate([
// start with the step labels stackView
// we'll give it 40-pts leading and trailing "padding"
stepStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40.0),
stepStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40.0),
// and 20-pts from the bottom
stepStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// now constrain the slider leading and trailing to the
// horizontal center of first and last step labels
// accounting for width of thumb (assuming a default UISlider)
discreteSlider.leadingAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: -14.0),
discreteSlider.trailingAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 14.0),
// and 20-pts above the steps stackView
discreteSlider.bottomAnchor.constraint(equalTo: stepStack.topAnchor, constant: -20.0),
// constrain Min and Max labels centered to first and last step labels
minLabel.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: 0.0),
maxLabel.centerXAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 0.0),
// and 20-pts above the steps slider
minLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
maxLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
// and 20-pts top "padding"
minLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
])
// add behavior
discreteSlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
discreteSlider.addTarget(self, action: #selector(self.sliderThumbReleased(_:)), for: .touchUpInside)
// add tap gesture so user can either
// Drag the Thumb or
// Tap the slider bar
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sliderTapped))
discreteSlider.addGestureRecognizer(tapGestureRecognizer)
}
// so we can set the slider value from the controller
public func setSliderValue(_ val: Float) -> Void {
discreteSlider.setValue(val, animated: true)
}
#objc func sliderValueDidChange(_ sender: UISlider) -> Void {
// tell the controller
sliderDraggingClosure?(sender.value)
}
#objc func sliderThumbReleased(_ sender: UISlider) -> Void {
// "snap" to discreet step position
sender.setValue(Float(sender.value.rounded()), animated: true)
// tell the controller
sliderReleasedClosure?(sender.value)
}
#objc func sliderTapped(_ gesture: UITapGestureRecognizer) {
guard gesture.state == .ended else { return }
guard let slider = gesture.view as? UISlider else { return }
// get tapped point
let pt: CGPoint = gesture.location(in: slider)
let widthOfSlider: CGFloat = slider.bounds.size.width
// calculate tapped point as percentage of width
let pct = pt.x / widthOfSlider
// convert to min/max value range
let pctRange = pct * CGFloat(slider.maximumValue - slider.minimumValue) + CGFloat(slider.minimumValue)
// "snap" to discreet step position
let newValue = Float(pctRange.rounded())
slider.setValue(newValue, animated: true)
// tell the controller
sliderReleasedClosure?(newValue)
}
}
along with an example view controller:
class SliderTestViewController: UIViewController {
let mySliderView = MySliderView()
override func viewDidLoad() {
super.viewDidLoad()
mySliderView.translatesAutoresizingMaskIntoConstraints = false
mySliderView.backgroundColor = .darkGray
view.addSubview(mySliderView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's put our custom slider view
// 40-pts from the top with
// 8-pts leading and trailing
mySliderView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
mySliderView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
mySliderView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
// we don't need Bottom or Height constraints, because our custom view's content
// will determine its Height
])
// set the slider closures
mySliderView.sliderDraggingClosure = { [weak self] val in
print("Slider dragging value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
mySliderView.sliderReleasedClosure = { [weak self] val in
print("Slider dragging end value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start the slider at 4
UIView.animate(withDuration: 0.8) {
self.mySliderView.setSliderValue(4)
}
}
}
Edit 2
If you want to make the slider "tappable area" larger, use a subclassed UISlider and override point(inside, ...).
Example 1 - expand tap area 10-pts on each side, 15-pts top and bottom:
class ExpandedTouchSlider: UISlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// expand tap area 10-pts on each side, 15-pts top and bottom
let bounds: CGRect = self.bounds.insetBy(dx: -10.0, dy: -15.0)
return bounds.contains(point)
}
}
Example 2 - expand tap area vertically to superview height:
class ExpandedTouchSlider: UISlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds: CGRect = self.bounds
if let sv = superview {
// expand tap area vertically to superview height
let svRect = sv.bounds
let f = self.frame
bounds.origin.y -= f.origin.y
bounds.size.height = svRect.height
}
return bounds.contains(point)
}
}
Example 3 - expand tap area both horizontally and vertically to include entire superview:
class ExpandedTouchSlider: UISlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds: CGRect = self.bounds
if let sv = superview {
// expand tap area both horizontally and vertically
// to include entire superview
let svRect = sv.bounds
let f = self.frame
bounds.origin.x -= f.origin.x
bounds.origin.y -= f.origin.y
bounds.size.width = svRect.width
bounds.size.height = svRect.height
}
return bounds.contains(point)
}
}
Note that if you're expanding the tap area horizontally (so the user can tap off the left/right ends of the slider), you'll also want to make sure your percentage / value calculation does not produce a value lower than the min, or higher than the max.
I have some view that consists from one stackview. Every n second I should update this view with data from server. When the new data come, I clean stackView (with method removeFromSuperView of its children) and add arrangedSubviews again to update UI. Sometimes, server sends the same data as old data. But doing this update operation, my view is kinda shuddering. Its kinda dragging and shuddering every time I clean and add views to my stackview. Of course, I can only update my UI if oldData != newData. But this competition is difficult and hard to find correctly. So, how to update stackview with new data without shuddering and blinking?
Here is my code:
func configure(_ items: [Item]) {
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
items.forEach {
let someView = SomeView()
someView.configure($0)
stackView.addArrangedSubview(someView)
}
}
Instead of removing / re-adding views to the stack view...
only add new views if there are more Items than current arranged subviews
set the data - configure() - each arranged subview
hide existing arranged subviews if there are more than needed
For example:
func configure(_ items: [Item]) {
// if we have fewer arranged subviews than items
// add new ones
while stackView.arrangedSubviews.count < items.count {
stackView.addArrangedSubview(SomeView())
}
// hide any existing arranged subviews if we have too many
for i in 0..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].isHidden = i >= items.count
}
// update the existing arranged subviews with the new data
for (thisItem, thisView) in zip(items, stackView.arrangedSubviews) {
// unwrap the arranged subview
guard let v = thisView as? SomeView else {
continue
}
v.configure(thisItem)
}
}
Here's a complete example - the number of "items" and their data will change every 1.5 seconds:
Sample struct Item with two strings:
struct Item {
var title: String = ""
var desc: String = ""
}
Sample "SomeView" class with two labels:
class SomeView: UIView {
let titleLabel = UILabel()
let descLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .systemYellow
titleLabel.font = .boldSystemFont(ofSize: 18.0)
descLabel.font = .italicSystemFont(ofSize: 15.0)
[titleLabel, descLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white // so we can see the label frames at run-time
addSubview(v)
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
descLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4.0),
descLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
descLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
descLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
func configure(_ item: Item) -> Void {
titleLabel.text = item.title
descLabel.text = item.desc
}
}
Sample "NeoView" class with the stack view:
class NeoView: UIView {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
func configure(_ items: [Item]) {
// if we have fewer arranged subviews than items
// add new ones
while stackView.arrangedSubviews.count < items.count {
stackView.addArrangedSubview(SomeView())
}
// hide any existing arranged subviews if we have too many
for i in 0..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].isHidden = i >= items.count
}
// update the existing arranged subviews with the new data
for (thisItem, thisView) in zip(items, stackView.arrangedSubviews) {
// unwrap the arranged subview
guard let v = thisView as? SomeView else {
continue
}
v.configure(thisItem)
}
}
}
Example controller class:
class NeoTestViewController: UIViewController {
let neoView: NeoView = {
let v = NeoView()
v.backgroundColor = .systemTeal
return v
}()
var items: [[Item]] = []
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
neoView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(neoView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
neoView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
neoView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
neoView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// no bottom constraint
])
items = sampleData()
var idx: Int = 0
self.neoView.configure(self.items[idx % self.items.count])
// update neoView with a new set of items every 1.5 seconds
Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { timer in
idx += 1
self.neoView.configure(self.items[idx % self.items.count])
}
}
func sampleData() -> [[Item]] {
// we'll create 8 sets of [Item] arrays
// each with a different number of Items
let numItems: [Int] = [
4, 2, 6, 3, 7, 8, 3, 5,
]
var items: [[Item]] = []
var i: Int = 1
numItems.forEach { n in
var theseItems: [Item] = []
for j in 1...n {
let thisItem = Item(title: "Set \(i) item \(j) title.", desc: "Set \(i) item \(j) description.")
theseItems.append(thisItem)
}
theseItems[0].title = "Set \(i) has \(n) items."
items.append(theseItems)
i += 1
}
return items
}
}
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 have a file for the View section of app, where i have all the labels and images that i intent to use, this is what i have in my DetailViewTableCell class, which inherits from UIView.
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
}
Now i move to my DetailViewController class, here i try and add the label, the label is added but it appears always at top left corner at 0,0 coordinate, when i try and add constraints for position, i always get error, now i can try
detailMain.detailName.frame.origin.x = 30
but i get error:
Constraint items must each be a view or layout guide.
In any case i do not wish to use this approach but more something like this
NSLayoutConstraint(item: detailMain.detailName, attribute: .leading, relatedBy: .equal, toItem: detailMain.detailName.superview, attribute: .leading, multiplier: 1, constant: 20).isActive = true
but i get the same above error, my over all code is this:
self.view.addSubview(detailMain.detailName)
detailMain.detailName.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailName.heightAnchor.constraint(equalToConstant: 25).isActive = true
detailMain.detailName.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
detailMain.detailName.font = UIFont(name: "Rubik-Medium", size: 30)
detailMain.detailName.backgroundColor = UIColor.white
detailMain.detailName.textColor = UIColor.black
Which works perfectly fine but the moment i try and constraints, the error come up, this is how the app shows up with out constraints and name at top most left corner
////////UPDATE
So here is my new DetailViewTableCell,
import UIKit
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and this 2 lines is what i add to my Detail view controller in viewDidLoad
let v = DetailViewTableCell()
detailTableView.tableHeaderView = v
Also i add this function
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = detailTableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
detailTableView.tableHeaderView = headerView
detailTableView.layoutIfNeeded()
}
}
then in my viewForHeaderInSection inbuilt function i add this
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
tableView.rowHeight = 80
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailMainImage.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
detailMain.detailMainImage.image = UIImage(named: restaurant.image)
detailMain.detailMainImage.contentMode = .scaleAspectFill
detailMain.detailMainImage.clipsToBounds = true
headerView.addSubview(detailMain.detailMainImage)
//Add the name
detailMain.detailName.text = restaurant.name
headerView.addSubview(detailMain.detailName)
return headerView
}
but still same position , is there any thing i add to add or remove from my code
You didn't show where you *want the labels, but this should get you going...
In your "header view" class:
add your elements: detailMainImage, detailName, etc...
set their properties and constraints as desired
You can get auto-layout to use the constraints you've setup in the header view to automatically determine its height:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
So, here's a complete example:
class TestHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// instantiate the header view
let v = DetailTableHeaderView()
// set it as the tableHeaderView
tableView.tableHeaderView = v
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
}
class DetailTableHeaderView: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// give the heart image view a background color so we can see its frame
detailHeart.backgroundColor = .red
if let img = UIImage(named: "teacup") {
detailMainImage.image = img
}
// give labels some text so we can see them
detailType.text = "Detail Type"
detailName.text = "Detail Name"
// setup fonts for labels as desired
//detailName.font = UIFont(name: "Rubik-Medium", size: 30)
// I don't have "Rubik" so this is with the system font
detailName.font = UIFont.systemFont(ofSize: 30, weight: .bold)
detailName.backgroundColor = UIColor.white
detailName.textColor = UIColor.black
detailType.textColor = .white
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// give main image a height
// set its Priority to 999 to prevent layout constraint conflict warnings
let mainImageHeightAnchor = detailMainImage.heightAnchor.constraint(equalToConstant: 300.0)
mainImageHeightAnchor.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
mainImageHeightAnchor,
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor),
])
}
}
For this example, I just set the background color of the "heart" image view to red, and I clipped the teacup out of your image:
And this is the result:
Edit - to use the custom view as a Section header view...
Use the same DetailTableHeaderView class from above, but change the table view controller as follows:
class TestSectionHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 300
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let v = DetailTableHeaderView()
// for example implementation...
if let img = UIImage(named: "teacup") {
v.detailMainImage.image = img
}
v.detailName.text = "Testing the Name"
// for your implementation...
//if let img = UIImage(named: restaurant.image) {
// v.detailMainImage.image = img
//}
//v.detailName.text = restaurant.name
return v
}
return nil;
}
}