Place a UIButton at bottom of UIScrollView with constraints - ios

I have a view with three textfield/textview fields in it, and in order to make the view scrollable when the keyboard appears, I have put all the elements inside a scrollview. The elements where not outside the frame before, so the contentSize should be the same size as the full screen size and then when the keyboard appears, so when the keyboard appears i update the bottom constraints for the scrollview to be -keyboardHeight from the view bottom and then it is scrollable above the keyboard..
This all works fine, the problem is adding a button to the bottom of the scrollview that leading/trailing to the sides and bottom of the view.
See pictures:
Current view, with button hidden behind navigation bar
Where i would like the button to be. Approx 20 margin to right.left.bottom
I am using SnapKit to set my constraints, and for the button I would like to do something like this:
sharebutton.snp.makeConstraints { (make) in
make.width.left.right.equalToSuperview()
make.bottom.equalToSuperview().offset(-20)
}
(The Superview/the scrollview is already set as 20 margin from sides)
//EDIT: Code added ->
override func viewDidLayoutSubviews() {
scrollview.contentSize = CGSize(width: centerView.frame.width, height: view.frame.height)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "title"
view.backgroundColor = .white
view.addSubview(scrollview)
scrollview.addSubview(sharebutton)
scrollview.addSubview(subjectField)
scrollview.addSubview(messageField)
scrollview.addSubview(datepicker)
scrollview.addSubview(addressTable)
addressTable.dataSource = self
addressTable.delegate = self
addressTable.separatorStyle = .none
addressTable.keyboardDismissMode = .onDrag
addressTable.separatorStyle = .singleLineEtched
subjectField.placeholder = "content"
datepicker.setTitle("topbutton", for: .normal)
datepicker.setTitleColor(.black, for: .normal)
datepicker.setTitleColor(UIColor.ME.border, for: .highlighted)
datepicker.titleLabel?.font = UIFont.ME.button
datepicker.layer.borderWidth = 1
datepicker.layer.cornerRadius = 3
datepicker.layer.borderColor = UIColor.ME.border.cgColor
datepicker.addTarget(self, action: #selector(showDates), for: .touchUpInside)
sharebutton.setTitle("Share", for: .normal)
sharebutton.backgroundColor = UIColor.ME.buttonMainBlue
sharebutton.addTarget(self, action: #selector(shareClicked), for: .touchUpInside)
scrollview.backgroundColor = .red
scrollview.snp.makeConstraints { (make) in
make.top.bottom.equalToSuperview()
make.left.width.right.equalTo(centerView)
}
datepicker.snp.makeConstraints { (make) in
make.top.equalToSuperview().offset(paddingGlobal)
make.left.width.right.equalToSuperview()
make.height.equalTo(50)
}
addressTable.snp.makeConstraints { (make) in
make.top.equalTo(datepicker.snp.bottom)
make.left.right.equalTo(datepicker)
make.height.equalTo(200)
}
subjectField.snp.makeConstraints { (make) in
make.top.equalTo(datepicker.snp.bottom).offset(20)
make.left.right.equalToSuperview()
}
messageField.snp.makeConstraints { (make) in
make.top.equalTo(subjectField.snp.bottom).offset(20)
make.height.equalTo(200)
make.left.right.equalToSuperview()
}
sharebutton.snp.makeConstraints { (make) in
make.width.left.right.equalToSuperview()
make.bottom.equalToSuperview().offset(-20)
}
}
Any thoughts ?

You need to add a UIView (for ex name it containerView) inside a scrollView subview first then add all your uielement(eg. textfield/textview, UIButton) to the subView of that containerView.

as Govind commented, I could solve it by placing my subviews in a container view.

Using StoryBoard and Constaint Xcode 11.5 iOS 13
StoryBoard View Controller Scene

Related

Swift custom UICollectionViewCell subViews disappear when imageView image set at ViewController

I faced weird issue while handling UICollectionView
I Created simple custom UICollectionViewCell, which has only one imageView and Label:
There's default placeholder image for Cell's imageView and updating imageView.image from collectionView(_:cellForItemAt:). But When image is set, all subview of cell disappears:
(Cells are not disappear at same time because downloading & setting image is async)
Note: Sample data I used is not wrong (Same data works for TableView in same app)
Why this happens and how can I fix it?
this is Sample data I used:
let movies = [
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955)
]
this is my part of ViewController:
lazy var collectionView = { () -> UICollectionView in
// FlowLayout
var flowLayout = UICollectionViewFlowLayout()
flowLayout.headerReferenceSize = CGSize(width: self.preferredContentSize.width, height: 180)
flowLayout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
flowLayout.minimumInteritemSpacing = 20
flowLayout.minimumLineSpacing = 20
// Collection View
var collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: flowLayout)
collectionView.register(DiscoverCollectionViewCell.self, forCellWithReuseIdentifier: identifiers.discover_collection_cell)
collectionView.register(DiscoverCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: identifiers.discover_collection_header)
collectionView.backgroundColor = UIColor(named: Colors.background)
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Discover"
collectionView.dataSource = self
collectionView.delegate = self
self.view.backgroundColor = UIColor(named: Colors.background)
self.view.addSubview(collectionView)
collectionView.snp.makeConstraints { $0.edges.equalTo(self.view.safeAreaLayoutGuide) }
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Sample Cell
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifiers.discover_collection_cell, for: indexPath) as? DiscoverCollectionViewCell else { return DiscoverCollectionViewCell() }
let movie = movies[indexPath.row]
cell.movieTitle.text = movie.title
DispatchQueue.global().async {
guard let imageURL = URL(string: "https://image.tmdb.org/t/p/original/\(movie.posterPath)") else { return }
guard let imageData = try? Data(contentsOf: imageURL) else { return }
DispatchQueue.main.sync {
cell.posterImage.image = UIImage(data: imageData)
}
}
return cell
}
and this is my custom CollectionViewCell, I used Snapkit, Then library:
class DiscoverCollectionViewCell: UICollectionViewCell {
//MARK: Create properties
lazy var posterImage = UIImageView().then {
$0.image = UIImage(named: "img_placeholder")
$0.contentMode = .scaleAspectFit
}
lazy var movieTitle = UILabel().then {
$0.font = UIFont.systemFont(ofSize: 15)
$0.textColor = .white
$0.numberOfLines = 2
$0.minimumScaleFactor = 10
}
override init(frame: CGRect) {
super.init(frame: frame)
// add to view
self.addSubview(posterImage)
self.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
}
movieTitle.snp.makeConstraints { make in
make.top.equalTo(posterImage.snp.bottom).offset(5)
make.bottom.greaterThanOrEqualToSuperview()
make.leading.equalTo(posterImage.snp.leading)
make.trailing.equalTo(posterImage.snp.trailing)
}
self.backgroundColor = .blue
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Two issues with your cell's layout...
// add to view
self.addSubview(posterImage)
self.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
}
You should always add UI elements to the cell's .contentView, not to the cell itself.
You did not constrain the bottom of the image view.
// add to ContentView!
self.contentView.addSubview(posterImage)
self.contentView.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
make.top.left.right.bottom.equalToSuperview()
}
Edit
You were missing a couple things from your post (including how you're setting your cell / item size), so while the above changes do fix the image not showing at all, it's not quite what you're going for.
I'm assuming you're setting the flow layout .itemSize somewhere, so your original constraints - without adding .bottom. to the image view constraints - were close...
When you add an image to a UIImageView, the intrinsicContentSize becomes the size of the image. Your constraints are controlling the width, but...
This constraint on your label:
make.bottom.greaterThanOrEqualToSuperview()
means "put the Bottom of the label at the Bottom of its superview or farther down!"
When your image loads, it sets the image view Height to its own Height and pushes the label way down past the bottom of the cell.
That line needs to be:
make.bottom.equalToSuperview()
That will prevent the Bottom of the label from moving.
Next, you need to tell auto-layout "don't compress or stretch the label vertically":
// prevent label from stretching vertically
movieTitle.setContentHuggingPriority(.required, for: .vertical)
// prevent label from compressing vertically
movieTitle.setContentCompressionResistancePriority(.required, for: .vertical)
Without that, the label will be compressed down to Zero height.
I find it very helpful to add comments so I know what I'm expecting to happen:
override init(frame: CGRect) {
super.init(frame: frame)
// add to ContentView
self.contentView.addSubview(posterImage)
self.contentView.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
// constrain image view to
// Top / Left / Right of contentView
make.top.left.right.equalToSuperview()
}
// prevent label from stretching vertically
movieTitle.setContentHuggingPriority(.required, for: .vertical)
// prevent label from compressing vertically
movieTitle.setContentCompressionResistancePriority(.required, for: .vertical)
movieTitle.snp.makeConstraints { make in
// constrain Top of label to Bottom of image view
// because we've set Hugging and Compression Resistance on the label,
// this will "pull down" the bottom of the image view
make.top.equalTo(posterImage.snp.bottom).offset(5)
// constrain Bottom of label to Bottom of contentView
// must be EQUAL TO
//make.bottom.greaterThanOrEqualToSuperview()
make.bottom.equalToSuperview()
// Leading / Trailing equal to image view
make.leading.equalTo(posterImage.snp.leading)
make.trailing.equalTo(posterImage.snp.trailing)
}
self.backgroundColor = .blue
}
Now we get this result:
and after the images download:
One final thing - although you may have already done something to address this...
As you see in those screenshots, setting .numberOfLines = 2 on a label does not force a 2-line height... it only limits it to 2 lines. If a Movie Title is short, the label height will be shorter as seen in the 2nd cell.
One way to fix that would be to constrain the label height to something like 2.5 lines by adding this to your init:
if let font = movieTitle.font {
movieTitle.snp.makeConstraints { make in
make.height.equalTo(font.lineHeight * 2.5)
}
}
That will give this output:
Although I am not sure, Because collection view cells are being reused the init of cells only gets called at the first time, not the time when image data is getting loaded from the server.
Try moving your layout-related code(specifically adding subviews and constraining them) in a different method of the cell and call it every time image gets loaded.

ios - switch within navigation item bar

I want to set a UISwitch within a UINavigationBar. But when I try place my finger on the switch and drag it to "switch" on and off the view is not responding.
This is what i have.
https://github.com/rchampa/views-within-navigationItem
As already stated in the comments above your GitHub project does not contain any data. Nevertheless everything works as expected (and seems cleaner to me) if you set the custom UIBarButtonItem up programmatically:
override func viewDidLoad() {
super.viewDidLoad()
setupBarButtonItem()
}
private func setupBarButtonItem() {
let offLabel = UILabel()
offLabel.font = UIFont.boldSystemFont(ofSize: UIFont.smallSystemFontSize)
offLabel.text = "OFF"
let onLabel = UILabel()
onLabel.font = UIFont.boldSystemFont(ofSize: UIFont.smallSystemFontSize)
onLabel.text = "ON"
let toggle = UISwitch()
toggle.addTarget(self, action: #selector(toggleValueChanged(_:)), for: .valueChanged)
let stackView = UIStackView(arrangedSubviews: [offLabel, toggle, onLabel])
stackView.spacing = 8
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: stackView)
}
#objc func toggleValueChanged(_ toggle: UISwitch) {
print("new value: \(toggle.isOn)")
}
Update:
I made it work via storyboard too. In contrast to setting it up programmatically you have to embed the UIStackView into a regular UIView to be able to add it as a UIBarButtonItem in storyboard. Then I added top, leading, bottom and trailing constraints (each with a constant of 0) from the UIStackView to its superview. To get rid of the storyboard warnings and errors at design time (at runtime it works without any problems) you have to manually calculate and set the width for the outer view (which contains the UIStackView) that is needed to enclose all of it subviews (offLabel.width + spacing + toggle.width + spacing + onLabel.width).

How to add a floating button in UITableViewController which does not scroll when table is scrolled?

Currently there is a UITableViewContoller with many sections and rows.
What is the best way to add a button which floats on top of the table view. This button should not scroll when the cells are scrolled.
Currently I have the following code and with this the button still scrolls:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(viewForFooter)
}
the viewForFooter is a separate view which contains the button which needs to be floating. Thank you. Any help would be greatly appreciated.
Here is an option if you really don't want to use a UIView with subviews...
override func viewDidLoad() {
super.viewDidLoad()
if let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window {
print("adding view-with-button to keyWindow")
window.addSubview(viewForFooter)
viewForFooter.topAnchor.constraint(equalTo: window.topAnchor, constant: 120).isActive = true
viewForFooter.leftAnchor.constraint(equalTo: window.leftAnchor, constant: 40).isActive = true
}
// other stuff...
}
This will add the view as a subview of the "keyWindow" at 40,120, and will "hover" there while you scroll the table. I am assuming your viewForFooter is properly instantiated and you have the necessary constraints set up correctly.
The easiest way is to use a viewController that will contain the tableView as a subView and then You can add you floating button as a subview of the viewController
override func viewWillAppear(_ animated: Bool) {
self.btnPickup = UIButton()
self.btnPickup?.frame = CGRect(x: self.view.frame.size.width - 75, y: self.view.frame.size.height - 150, width: 50, height: 50)
// self.btnPickup?.setTitle("+", for: .normal)
self.btnPickup?.setBackgroundImage(#imageLiteral(resourceName: "add_user"), for: .normal)
self.btnPickup?.titleLabel?.textAlignment = .center
self.btnPickup?.titleLabel?.font = UIFont(name: (self.btnPickup?.titleLabel?.font.fontName)!, size: 50)
// self.btnPickup?.layer.borderColor = UIColor.black.cgColor
self.btnPickup?.backgroundColor = UIColor.white
// self.btnPickup?.layer.borderWidth = 1
self.btnPickup?.layer.cornerRadius = 25
self.btnPickup?.clipsToBounds = true
// self.btnPickup?.setTitleColor(UIColor.white, for: .normal)
self.btnPickup?.addTarget(self, action: #selector(DirectoryViewController.btnTapped(sender:)), for: .touchUpInside)
print(self.navigationController?.view.subviews.count ?? "error")
self.navigationController?.view.superview?.insertSubview(self.btnPickup!, at: (self.navigationController?.view.subviews.count)!)
}
override func viewWillDisappear(_ animated: Bool) {
self.btnPickup?.removeFromSuperview()
}
I had exactly this problem. I'm using Interface Builder
and Auto Layout to place it so, it is not a programtic solution like the ones suggest by my colleagues:
The button must be placed at the same level as the UITableView. Take care where you place it in the hierarchy as depicted below: It can not be below the Table View in the hierarchy.

iOS Button not work in custom view

There are two custom views - viewA and viewB.ViewB is a small view added in viewA. At first viewA is filled in the whole screen. ViewB is located in the bottom of viewA(out of the screen). When click a button in viewA, viewB bottom constant of constraints will be -100, thus viewB will display in the bottom of the screen. But there is a button in viewB did not response its selector. Here is my code:
In ViewA
let viewB: ViewB = {
let view = ViewB(
view.setup()
return view
}()
viewB constraints:
viewB.snp.makeConstraints { (make) in
make.trailing.leading.equalTo(self)
make.height.equalTo(100)
make.top.equalTo(self.snp.bottom)
}
when click button to arouse viewB
UIView.animate(withDuration: 0.5) {
self.snp.updateConstraints({ (make) in
make.bottom.equalTo(-100)
})
}
self.layoutIfNeeded()
In viewB:
class ViewB: UIView {
let b: UIButton = {
let button = UIButton(type: UIButtonType.custom)
button.addTarget(self, action: #selector(btnClicked), for: UIControlEvents.touchUpInside)
button.backgroundColor = UIColor.green
button.setTitle("Button", for: .normal)
return button
}()
func btnClicked() {
print("btnClicked")
}
func setup() {
addSubview(b)
b.snp.makeConstraints { (make) in
make.leading.top.bottom.equalTo(self)
make.width.equalTo(100)
}
}
}
Your code by itself works fine. So the issue is probably something like the button being overlaid by another view which takes the touch actions. You can test this using the view debugger or provide a link to your project so one of us can take a look to see what is going on.
i think , your view is going out of its super View or Any other view is coming on its upper layer. please see by this
superview.clipsToBounds : YES

Creating 2 views in view using programatically contraints

I'm trying to add 2 views to a CallOutView. The pushButton should be at the bottom with a static height of 20. The topView should then fill the rest. I've tried to do this programmatically using SnapKit. However it seems like the pushbutton just fills everything? what am i doing wrong?
callOutView = UIView(frame: CGRectMake(-70+(self.frame.width/2), -65, 140, 60))
callOutView!.backgroundColor = UIColor.clearColor()
callOutView?.clipsToBounds = true
callOutView?.layer.cornerRadius = 6
self.addSubview(callOutView!)
let topView = UIView()
topView.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.8)
callOutView?.addSubview(topView)
let pushButton = UIButton()
pushButton.backgroundColor = UIColor(rgba: "#09316e").colorWithAlphaComponent(0.8)
pushButton.setTitle("Se Mere", forState: UIControlState.Normal)
pushButton.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)
pushButton.titleLabel?.font = UIFont.systemFontOfSize(8)
callOutView?.addSubview(pushButton)
topView.snp_makeConstraints { (make) -> Void in
make.top.equalTo(callOutView!).offset(0)
make.left.equalTo(callOutView!).offset(0)
make.bottom.equalTo(pushButton).offset(0)
make.right.equalTo(callOutView!).offset(0)
make.height.equalTo(40)
}
pushButton.snp_makeConstraints { (make) -> Void in
make.height.equalTo(20)
make.top.equalTo(topView).offset(0)
make.left.equalTo(callOutView!).offset(0)
make.bottom.equalTo(0).offset(0)
make.right.equalTo(callOutView!).offset(0)
}
The problem is that there are some constraints conflicting with each other, which will make the autolayout system break some of them.
topView.snp_makeConstraints { (make) -> Void in
...
make.bottom.equalTo(pushButton).offset(0)
...
}
pushButton.snp_makeConstraints { (make) -> Void in
...
make.top.equalTo(topView).offset(0)
...
}
The two constraints above tell the autolayout system:
Place topView and pushButton with top edge aligned.
Place topView and pushButton with bottom edge aligned.
Which is impossible if you give topView and pushButton different heights.
Also, this is not what you want obviously. What you want is "place pushButton right below the topView".
Here's the modified code to make pushButton being placed right below the topView:
topView.snp_makeConstraints { (make) -> Void in
...
make.bottom.equalTo(pushButton.snp_top).offset(0)
...
}
pushButton.snp_makeConstraints { (make) -> Void in
...
make.top.equalTo(topView.snp_bottom).offset(0)
...
}

Resources