I currently have a UIScrollView with a UIImageView (top image) positioned below the UINavigationBar. However, I want to position the UIImageView at the very top of the screen (bottom image). Is there a way to implement this?
What I've tried so far: I added a UIScrollView extension (source) that is supposed to scroll down to the view parameter provided, but it hasn't worked for me.
extension UIScrollView {
// Scroll to a specific view so that it's top is at the top our scrollview
func scrollToView(view:UIView, animated: Bool) {
if let origin = view.superview {
// Get the Y position of your child view
let childStartPoint = origin.convert(view.frame.origin, to: self)
// Scroll to a rectangle starting at the Y of your subview, with a height of the scrollview
self.scrollRectToVisible(CGRect(x:0, y:childStartPoint.y,width: 1,height: self.frame.height), animated: animated)
}
}
// Bonus: Scroll to top
func scrollToTop(animated: Bool) {
let topOffset = CGPoint(x: 0, y: -contentInset.top)
setContentOffset(topOffset, animated: animated)
}
// Bonus: Scroll to bottom
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
if(bottomOffset.y > 0) {
setContentOffset(bottomOffset, animated: true)
}
}
}
class MealDetailsVC: UIViewController {
private var mealInfo: MealInfo
init(mealInfo: MealInfo) {
self.mealInfo = mealInfo
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
scrollView.scrollToView(view: iv, animated: false) // used extension from above
}
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
return scrollView
}()
lazy var iv: UIImageView = {
let iv = UIImageView()
iv.image = Image.defaultMealImage!
iv.contentMode = .scaleAspectFill
return iv
}()
}
extension MealDetailsVC {
func setupViews() {
addBackButton()
addSubviews()
autoLayoutViews()
constrainSubviews()
}
fileprivate func addBackButton() {
...
}
#objc func goBack(sender: UIBarButtonItem) {
...
}
fileprivate func addSubviews() {
view.addSubview(scrollView)
scrollView.addSubview(iv)
}
fileprivate func autoLayoutViews() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
iv.translatesAutoresizingMaskIntoConstraints = false
}
fileprivate func constrainSubviews() {
NSLayoutConstraint.activate([
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
NSLayoutConstraint.activate([
iv.topAnchor.constraint(equalTo: scrollView.topAnchor),
iv.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
iv.heightAnchor.constraint(equalTo: iv.widthAnchor, multiplier: 0.6)
])
}
}
This may help.
scrollView.contentInsetAdjustmentBehavior = .never
For more information,
This property specifies how the safe area insets are used to modify the content area of the scroll view. The default value of this property is UIScrollViewContentInsetAdjustmentAutomatic.
Don't set a height anchor and add a imageView centerXAnchor equal to ScrollView centerXAnchor constraint. set imageView.contentMode to .scaleAspectFit
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.
Help me in one of the two ways maybe:
How to solve the problem? or
How to understand the error message?
Project summary
So I'm learning about inputAccessoryView by making a tiny project, which has only one UIButton. Tapping the button summons the keyboard with inputAccessoryView which contains 1 UITextField and 1 UIButton. The UITextField in the inputAccessoryView will be the final firstResponder that is responsible for the keyboard with that inputAccessoryView
The error message
API error: <_UIKBCompatInputView: 0x7fcefb418290; frame = (0 0; 0 0); layer = <CALayer: 0x60000295a5e0>> returned 0 width, assuming UIViewNoIntrinsicMetric
The code
is very straightforward as below
The custom UIView is used as inputAccessoryView. It installs 2 UI outlets, and tell responder chain that it canBecomeFirstResponder.
class CustomTextFieldView: UIView {
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
let textField:UITextField = {
let textField = UITextField()
textField.placeholder = "placeholder"
return textField
}()
required init?(coder: NSCoder) {
super.init(coder: coder)
initSetup()
}
override init(frame:CGRect) {
super.init(frame: frame)
initSetup()
}
convenience init() {
self.init(frame: .zero)
}
func initSetup() {
addSubview(doneButton)
addSubview(textField)
}
func autosizing(to vc: UIViewController) {
frame = CGRect(x: 0, y: 0, width: vc.view.frame.size.width, height: 40)
let totalWidth = frame.size.width - 40
doneButton.frame = CGRect(x: totalWidth * 4 / 5 + 20,
y: 0,
width: totalWidth / 5,
height: frame.size.height)
textField.frame = CGRect(x: 20,
y: 0,
width: totalWidth * 4 / 5,
height: frame.size.height)
}
override var canBecomeFirstResponder: Bool { true }
override var intrinsicContentSize: CGSize {
CGSize(width: 400, height: 40)
} // overriding this variable seems to have no effect.
}
Main VC uses the custom UIView as inputAccessoryView. The UITextField in the inputAccessoryView becomes the real firstResponder in the end, I believe.
class ViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customView.autosizing(to: self)
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}
I've seen people on the internet says this error message will go away if I run on a physical phone. I didn't go away when I tried.
I override intrinsicContentSize of the custom view, but it has no effect.
The error message shows twice together when I tap summon.
What "frame" or "layer" does the error message refer to? Does it refer to the custom view's frame and layer?
If we use Debug View Hierarchy we can see that _UIKBCompatInputView is part of the (internal) view hierarchy of the keyboard.
It's not unusual to see constraint errors / warnings with internal views.
Since frame and/or intrinsic content size seem to have no effect, I don't think it can be avoided (nor does it seem to need to be).
As a side note, you can keep the "Done" button round by using auto-layout constraints. Here's an example:
class CustomTextFieldView: UIView {
let textField: UITextField = {
let tf = UITextField()
tf.font = .systemFont(ofSize: 16)
tf.autocorrectionType = .no
tf.returnKeyType = .done
tf.placeholder = "placeholder"
// textField backgroundColor so we can see its frame
tf.backgroundColor = .yellow
return tf
}()
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
autoresizingMask = [.flexibleHeight, .flexibleWidth]
[doneButton, textField].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
NSLayoutConstraint.activate([
// constrain doneButton
// Trailing: 20-pts from trailing
doneButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// Top and Bottom 8-pts from top and bottom
doneButton.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
doneButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
// Width equal to default height
// this will keep the button round instead of oval
doneButton.widthAnchor.constraint(equalTo: doneButton.heightAnchor),
// constrain textField
// Leading: 20-pts from leading
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// Trailing: 8-pts from doneButton leading
textField.trailingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: -8.0),
// vertically centered
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class CustomTextFieldViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}
I am trying to animate a view on screen in a similar way to how action sheets appear.
My router presents CustomCardViewController which has an overlay background.
After a short delay I'd like containerView too animate into view from the bottom.
Instead what is happening however is it just appears in place. There is no animation between the transition.
final class CustomCardViewController: UIViewController {
private let backgroundMask: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .init(white: 0, alpha: 0.3)
return view
}()
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.transform = .init(translationX: 0, y: view.frame.height)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
backgroundMask.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTapToDismiss)))
modalPresentationStyle = .overFullScreen
[backgroundMask, containerView].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
backgroundMask.topAnchor.constraint(equalTo: view.topAnchor),
backgroundMask.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundMask.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backgroundMask.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -24),
containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200)
])
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.containerView.transform = .identity
}, completion: nil)
}
}
private extension CustomCardViewController {
#objc func onTapToDismiss() {
dismiss(animated: false, completion: nil)
}
}
You need to call your animation block from another lifecycle method. You can't trigger this from viewDidLoad as the view has only loaded, there is nothing on screen yet.
Try using viewDidAppear
For animation, you need to update the constant of the constrain that you want to animate. Here, since you're trying to animate from the bottom, you need to update a vertical constrain in this case, the bottom constraint. Here's the code for animation:
final class CustomCardViewController: UIViewController {
//..
override func viewDidLoad() {
super.viewDidLoad()
NSLayoutConstraint.activate([
// remove bottom constraint from here
])
containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 250)
containerViewBottomConstraint?.isActive = true
}
var containerViewBottomConstraint: NSLayoutConstraint? // declare bottom constraint
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
containerViewBottomConstraint?.constant = -24
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
}
}
I have extended UIView:
class A: UIView {
private var _height: NSLayoutConstraint!
private var _centerLabel: UILabel = {
let lb = UILabel()
lb.text = "Some text"
lb.adjustsFontSizeToFitWidth = true
lb.textAlignment = .center
lb.translatesAutoresizingMaskIntoConstraints = false
return lb
}()
func open() {
self._height.constant = 50
self.superview?.layoutIfNeeded()
}
func close() {
UIView.animate(withDuration: 1, delay: 1, options: .curveLinear, animations: {
self._height.constant = 0
self.superview?.layoutIfNeeded()
}, completion: nil)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(_centerLabel)
_height = self.heightAnchor.constraint(equalToConstant: 0)
NSLayoutConstraint.activate([
_centerLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16),
_centerLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16),
_centerLabel.topAnchor.constraint(lessThanOrEqualTo: self.topAnchor, constant: 16),
_centerLabel.bottomAnchor.constraint(greaterThanOrEqualTo: self.bottomAnchor, constant: -16),
_height
])
}
}
MainViewController:
class MainViewController: UIViewController {
#IBOutlet weak var a: A!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func open(_ sender: Any) {
a.open()
}
#IBAction func close(_ sender: Any) {
a.close()
}
}
If I call close(), instance of UIView A animates correctly, all other constraint attached to it as well. However, content of A (_centerLabel) disappears immediatelly, its height is not animated at all. Why?