Swift: Stacking subviews dynamically inside a superview - ios

I am trying to create a login in page that is supposed to look like the one listed below. The code I have written should produce this view minus the logo and minus [Login|Register] toggle button. The height and width of my box is also different, but I'm not concerned about that.
Currently I am getting this output. I'm concerned about how the words are all overlapping each other at the top.
In the code I listed below I create my 3 text fields, my button and the container for my text fields. I believe there is something wrong in the function fieldConstraints. In this function I look through an array of all my text fields and assign the neccessary constraints to them. They all get the same constraints except that the topAnchor of each text field after the first one is set equal to the bottomAnchor of the separator that was placed underneath the text field before. Those blue lines in between the text fields are the separators.
Main class
class SignIn: UIViewController {
override func loadView() {
super.loadView()
let inputContainer = inputDataContainer()
constraintsToCenterSubview(forView: inputContainer, width: 100, height: 100)
let nameField = field(for: "Name")
let emailField = field(for: "Email address")
let passField = field(for: "Password")
let fields = [nameField, emailField, passField]
let button = loginButton()
fieldConstraints(subviews: fields, superview: inputContainer)
self.centerViewBelow(forView: button, whichIsBelow: inputContainer, increaseWidthBy: 0)
}
func inputDataContainer() -> UIView{
let inputView = UIView(frame: CGRect(x: self.view.center.x, y: self.view.center.y, width: CGFloat(100), height: CGFloat(100)))
inputView.backgroundColor = UIColor.white
inputView.translatesAutoresizingMaskIntoConstraints = false
inputView.layer.cornerRadius = 5
inputView.layer.masksToBounds = true
self.view.addSubview(inputView)
//inputView = centerViewBelow(forView: inputView, whichIsBelow: self.view, increaseWidthBy: 100)
return inputView
}
func loginButton() -> UIButton {
let button = UIButton()
button.backgroundColor = UIColor(r: 80, g: 101, b: 161)
button.setTitle("Submit", for: [])
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(UIColor.white, for: [])
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
self.view.addSubview(button)
return button
}
func field(for name: String) -> UITextField{
let tf = UITextField()
tf.placeholder = name
tf.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(tf)
return tf
}
func fieldSep() -> UIView {
let view = UIView()
view.backgroundColor = UIColor(r: 220, g: 220, b: 220)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}
func fieldConstraints(subviews: [UIView], superview: UIView) {
var sep: UIView?
let len = subviews.endIndex
for (idx, subview) in subviews.enumerated(){
superview.addSubview(subview)
subview.leftAnchor.constraint(equalTo: superview.leftAnchor)
subview.rightAnchor.constraint(equalTo: superview.rightAnchor)
subview.widthAnchor.constraint(equalTo: superview.widthAnchor)
subview.heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: CGFloat(1/len))
if (sep != nil){
subview.topAnchor.constraint(equalTo: sep!.bottomAnchor)
}else{
subview.topAnchor.constraint(equalTo: superview.topAnchor)
}
sep = fieldSep()
if idx < subviews.endIndex-1 {
self.view.addSubview(sep!)
sep?.leftAnchor.constraint(equalTo: superview.leftAnchor)
sep?.rightAnchor.constraint(equalTo: superview.rightAnchor)
sep?.topAnchor.constraint(equalTo: subview.bottomAnchor)
}
}
}
}
Extensions
extension UIColor {
convenience init(r:CGFloat, g:CGFloat, b: CGFloat) {
self.init(red: r/255, green: g/255, blue: b/255, alpha: 1)
}
}
extension UIViewController {
func centerViewBelow(forView view: UIView, whichIsBelow topView: UIView, increaseWidthBy constant: CGFloat){
let topConstraint = NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .bottom, multiplier: 1, constant: 20)
let widthConstraint = NSLayoutConstraint(item: view, attribute: .width, relatedBy: .equal, toItem: topView, attribute: .width, multiplier: 1, constant: constant)
let centerConstraint = NSLayoutConstraint(item: view, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let heightConstraint = NSLayoutConstraint(item: view, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 50)
NSLayoutConstraint.activate([topConstraint, widthConstraint, centerConstraint, heightConstraint])
//return view
}
func constraintsToCenterSubview(forView view: UIView, width: Int, height: Int){
let centerXConstraint = NSLayoutConstraint(item: view, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let centerYConstraint = NSLayoutConstraint(item: view, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0)
let widthConstraint = NSLayoutConstraint(item: view, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: CGFloat(width))
let heightConstraint = NSLayoutConstraint(item: view, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: CGFloat(height))
NSLayoutConstraint.activate([centerXConstraint, centerYConstraint, widthConstraint, heightConstraint])
}
}
Thank you
----------------------------------------------------------------------------------------------------------
Update
So I was pretty much able to do it by changing my container into a stack view. But this made it so that my corners were no longer rounded. Does anyone know how to fix this?
func inputDataContainer() -> UIStackView{
let inputView = UIStackView(frame: CGRect(x: self.view.center.x, y: self.view.center.y, width: CGFloat(100), height: CGFloat(100)))
inputView.backgroundColor = UIColor.white
inputView.translatesAutoresizingMaskIntoConstraints = false
inputView.layer.cornerRadius = 5
inputView.layer.masksToBounds = true
inputView.distribution = .fillEqually
inputView.axis = .vertical
inputView.spacing = 1
self.view.addSubview(inputView)
//inputView = centerViewBelow(forView: inputView, whichIsBelow: self.view, increaseWidthBy: 100)
return inputView
}
func fieldConstraints(subviews: [UIView], superview: UIStackView) {
for subview in subviews{
superview.addArrangedSubview(subview)
subview.clipsToBounds = true
}
}
Example Screen Shot of Current App

Try to give height for
sep?.heightAnchor.constraint(equalToConstant:1.0).isActive = true
also for all constraints in fieldConstraints you forget
.isActive = true
or use NSLayoutConstraint.activate , like
NSLayoutConstraint.activate([
subview.leftAnchor.constraint(equalTo: superview.leftAnchor),
subview.rightAnchor.constraint(equalTo: superview.rightAnchor)
subview.widthAnchor.constraint(equalTo: superview.widthAnchor),
subview.heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: CGFloat(1/len))
])
This approach will work but it will be better to use a vertical stackview with distribution .fillEqually and it'll will partition them and add like
fileds.forEach { stackview.addArrangedSubview($0) }

use UIStackView. it will help you save time to build a complicated UI.

You will have a little trouble using a UIStackView. You could embed it in a UIView and give that view the rounded corners, but then you have to add padding to the fields within the stack view in order to get your separator lines.
Here is another approach that might work better for you. It's closer to your original code, adding the fields and separator views to a UIView:
extension UIColor {
convenience init(r:CGFloat, g:CGFloat, b: CGFloat) {
self.init(red: r/255, green: g/255, blue: b/255, alpha: 1)
}
}
class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(r: 65, g: 92, b: 144)
let nameField = field(for: "Name")
let sep1 = fieldSep()
let emailField = field(for: "Email address")
let sep2 = fieldSep()
let passField = field(for: "Password")
let views = [nameField, sep1, emailField, sep2, passField]
let inputView = inputDataContainer(with: views)
view.addSubview(inputView)
NSLayoutConstraint.activate([
inputView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0),
inputView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0),
inputView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0),
])
}
func field(for s: String) -> UITextField {
let f = UITextField()
f.translatesAutoresizingMaskIntoConstraints = false
f.placeholder = s
f.borderStyle = .none
return f
}
func fieldSep() -> UIView {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
v.backgroundColor = UIColor(r: 220, g: 220, b: 220)
return v
}
func inputDataContainer(with subviews: [UIView]) -> UIView {
let horizontalPadding: CGFloat = 8.0
let verticalSpacing: CGFloat = 8.0
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .white
containerView.layer.cornerRadius = 5
var previousView: UIView?
for subview in subviews{
containerView.addSubview(subview)
// if it's a text field, we want padding on left and right
if subview is UITextField {
subview.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: horizontalPadding).isActive = true
subview.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -horizontalPadding).isActive = true
} else {
subview.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0).isActive = true
subview.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0).isActive = true
}
if subview == subviews.first {
// if it's the first subview, constrain to top of container
subview.topAnchor.constraint(equalTo: containerView.topAnchor, constant: verticalSpacing).isActive = true
} else {
// unwrap previousView and constrain subview to bottom of previous subview
if let pv = previousView {
subview.topAnchor.constraint(equalTo: pv.bottomAnchor, constant: verticalSpacing).isActive = true
}
}
if subview == subviews.last {
// if it's the last subview, constrain to bottom of container
subview.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -verticalSpacing).isActive = true
}
// save reference to current subview
previousView = subview
}
return containerView
}
}
And the result:

Related

I'm currently having trouble updating constraints for a UIVIew in Autolayout

I've created a custom View that has subviews added to it and a UIPanGestureRecognizer is added to each UIView object but when I select the UIView after I update the constraints there is a delay. If I drag the UIView (sView) off the screen, the next time I drag the view in either direction there is a delay of a few seconds before the pan gesture recognizer registers. It doesn't register the pan gesture immediately.
I've read up on auto layout and believe its configured correctly in the animation callback method. I've created a delegate in the view controller that has access to the updated constraints, and sView.leftConstraint and sView.rightConstraint are fields I have added to a custom view that represents a draggable UIView.
The ScrollViewController adopts a protocol so that it can access the view that was selected and update the constraints accordingly.
Any help would be greatly appreciated :)
extension ScrollViewController: DragDelegate {
func draggedLeft(sView: DraggableView) {
print("Dragged Left")
confirmationLabel.text = ""
sView.selectionLabel.text = "Not Selected"
sView.cornerRadius = 7
sView.rightConstraint?.isActive = false
sView.leftConstraint?.isActive = false
sView.leftConstraint?.constant = 1
sView.leftConstraint?.isActive = true
// sView.widthConstraint?.constant = 300
UIView.animate(withDuration: 0.3) {
sView.updateConstraints()
self.contentView.setNeedsLayout()
self.view.layoutIfNeeded()
}
}
}
class OrderSelectionView: UIView {
var dView : DraggableView?
var dView2 : DraggableView?
let screenWidth = UIScreen.main.bounds.width
//I need to set up all the constraints in this class
//This will act almost like a container view
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
let margins = self.layoutMarginsGuide
self.translatesAutoresizingMaskIntoConstraints = false
dView = DraggableView(frame: frame)
dView2 = DraggableView(frame: frame)
guard let dragView = dView else {return}
guard let dragView2 = dView2 else {return}
dragView.translatesAutoresizingMaskIntoConstraints = false
dragView.topAnchor.constraint(equalTo: laundryLabel.bottomAnchor, constant: 10).isActive = true
let rightAnchor = NSLayoutConstraint(item: dragView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0)
let leftAnchor = NSLayoutConstraint(item: dragView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0)
dragView.widthAnchor.constraint(equalToConstant: 150).isActive = true
dragView.heightAnchor.constraint(equalToConstant: 40).isActive = true
dragView.leftConstraint = leftAnchor
dragView.leftConstraint?.isActive = true
dragView.rightConstraint = rightAnchor
self.addSubview(dragView2)
dragView2.translatesAutoresizingMaskIntoConstraints = false
let laundryLabel2 = createLaundryLabel(lTitle: "Dry", constant: 100)
dragView2.translatesAutoresizingMaskIntoConstraints = false
dragView2.topAnchor.constraint(equalTo: laundryLabel2.bottomAnchor, constant: 10).isActive = true
let rightAnchor2 = NSLayoutConstraint(item: dragView2, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: -50)
let leftAnchor2 = NSLayoutConstraint(item: dragView2, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0)
dragView2.widthAnchor.constraint(equalToConstant: 150).isActive = true
dragView2.heightAnchor.constraint(equalToConstant: 40).isActive = true
dragView2.leftConstraint = leftAnchor2
dragView2.leftConstraint?.isActive = true
dragView2.rightConstraint = rightAnchor2
}

Programmatically create UIView with size based on subviews(labels)

I have a scenario where I'm needed to create an UIView completely programatically. This view is displayed as an overlay over a video feed and it contains 2 UILabels as subviews.
These labels can have varying widths and heights so I don't want to create a fixed frame for the UIView or the labels.
I've done this with programatic constrains without issue, but the problem is that the views are glitching when moving the phone and the framework that I'm using that does the video feed recommends not to use autolayout because of this exact problem.
My current code:
I create the view
func ar(_ arViewController: ARViewController, viewForAnnotation: ARAnnotation) -> ARAnnotationView {
let annotationView = AnnotationView() //*this is the view, inherits from UIView
annotationView.annotation = viewForAnnotation
annotationView.delegate = self
return annotationView
}
And the actual view code
class AnnotationView: ARAnnotationView {
var titleLabel: UILabel?
var distanceLabel: UILabel!
var delegate: AnnotationViewDelegate?
override var annotation: ARAnnotation? {
didSet {
loadUI()
}
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
setupUI()
}
private func setupUI() {
layer.cornerRadius = 5
layer.masksToBounds = true
backgroundColor = .transparentWhite
}
private func loadUI() {
titleLabel?.removeFromSuperview()
distanceLabel?.removeFromSuperview()
let label = UILabel()
label.font = UIFont(name: "AvenirNext-Medium", size: 16.0) ?? UIFont.systemFont(ofSize: 14)
label.numberOfLines = 2
label.textColor = UIColor.white
self.titleLabel = label
distanceLabel = UILabel()
distanceLabel.textColor = .cbPPGreen
distanceLabel.font = UIFont(name: "AvenirNext-Medium", size: 14.0) ?? UIFont.systemFont(ofSize: 12)
distanceLabel.textAlignment = .center
self.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
distanceLabel.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(label)
self.addSubview(distanceLabel)
if self.constraints.isEmpty {
NSLayoutConstraint.activate([
NSLayoutConstraint(item: label, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 5),
NSLayoutConstraint(item: label, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: -5),
NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: distanceLabel, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: distanceLabel, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: distanceLabel, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0),
NSLayoutConstraint(item: distanceLabel, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0),
NSLayoutConstraint(item: distanceLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 15),
])
}
if let annotation = annotation as? BikeStationAR {
titleLabel?.text = annotation.address
distanceLabel?.text = String(format: "%.2f km", annotation.distanceFromUser / 1000)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
delegate?.didTouch(annotationView: self)
}
}
So my problem is now: how can I achieve the same behavior, without using autolayout?
Edit: here is a video of the behavior https://streamable.com/s/4zusq/dvbgnb
Edit 2: The solution is based on Mahgol Fa's answer and using a stack view or vertical positioning in order not to set the frame for the individual labels. So the final implementation is
private func loadUI() {
titleLabel?.removeFromSuperview()
distanceLabel?.removeFromSuperview()
let label = UILabel()
label.font = titleViewFont
label.numberOfLines = 0
label.textColor = UIColor.white
label.textAlignment = .center
self.titleLabel = label
distanceLabel = UILabel()
distanceLabel.textColor = .cbPPGreen
distanceLabel.font = subtitleViewFont
distanceLabel.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
distanceLabel.translatesAutoresizingMaskIntoConstraints = false
if let annotation = annotation as? BikeStationAR {
let title = annotation.address
let subtitle = String(format: "%.2f km", annotation.distanceFromUser / 1000)
let fontAttributeTitle = [NSAttributedString.Key.font: titleViewFont]
let titleSize = title.size(withAttributes: fontAttributeTitle)
let fontAttributeSubtitle = [NSAttributedString.Key.font: subtitleViewFont]
let subtitleSize = annotation.address.size(withAttributes: fontAttributeSubtitle)
titleLabel?.text = title
distanceLabel?.text = subtitle
let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: titleSize.width + 5, height: titleSize.height + subtitleSize.height + 5))
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.alignment = .fill
stackView.addArrangedSubview(titleLabel ?? UIView())
stackView.addArrangedSubview(distanceLabel)
frame = stackView.bounds
addSubview(stackView)
}
}
This way you can get the width of your lablels texts:
let fontAttributes1= [NSAttributedStringKey.font: your declared font in your code]
let size1 = (“your string” as String).size(withAttributes: fontAttributes1)
let fontAttributes2= [NSAttributedStringKey.font: your declared font in your code]
let size2 = (“your string” as String).size(withAttributes: fontAttributes2)
Then :
YourView.frame = CGRect( width: size1.width + size2.width + some spacing , height : ....)

Customize UISearchBar in Swift 4

I need to delete the background of the search bar.
The part where text is entered (white color) make it higher.
put a green border around the white part.
customize the font.
I need this:
What I have achieved is this:
My code:
#IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
self.searchBar.layer.borderColor = #colorLiteral(red: 0.2352941176, green: 0.7254901961, blue: 0.3921568627, alpha: 1)
self.searchBar.layer.borderWidth = 1
self.searchBar.clipsToBounds = true
self.searchBar.layer.cornerRadius = 20
}
Try using this code:
class JSSearchView : UIView {
var searchBar : UISearchBar!
override func awakeFromNib()
{
// the actual search barw
self.searchBar = UISearchBar(frame: self.frame)
self.searchBar.clipsToBounds = true
// the smaller the number in relation to the view, the more subtle
// the rounding -- https://www.hackingwithswift.com/example-code/calayer/how-to-round-the-corners-of-a-uiview
self.searchBar.layer.cornerRadius = 5
self.addSubview(self.searchBar)
self.searchBar.translatesAutoresizingMaskIntoConstraints = false
let leadingConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 20)
let trailingConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: -20)
let yConstraint = NSLayoutConstraint(item: self.searchBar, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
self.addConstraints([yConstraint, leadingConstraint, trailingConstraint])
self.searchBar.backgroundColor = UIColor.clear
self.searchBar.setBackgroundImage(UIImage(), for: .any, barMetrics: .default)
self.searchBar.tintColor = UIColor.clear
self.searchBar.isTranslucent = true
// https://stackoverflow.com/questions/21191801/how-to-add-a-1-pixel-gray-border-around-a-uisearchbar-textfield/21192270
for s in self.searchBar.subviews[0].subviews {
if s is UITextField {
s.layer.borderWidth = 1.0
s.layer.cornerRadius = 10
s.layer.borderColor = UIColor.green.cgColor
}
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// the half height green background you wanted...
let topRect = CGRect(origin: .zero, size: CGSize(width: self.frame.size.width, height: (self.frame.height / 2)))
UIColor.green.set()
UIRectFill(topRect)
}
}
And to use it, drop a custom view into your xib or storyboard file and simply set the custom class type to the name of the class (JSSearchView).

inputAccessoryViewController height modification

I'm trying to use inputAccessoryViewController in my app, but faced a problem with changing height of accessory view. I tried to change frame/bounds of the view, I also tried to handle height of the accessory view using constraints. But nothing worked well.
InputViewController code:
import UIKit
import RxSwift
import RxCocoa
class InputViewController: UIInputViewController {
private var separatorView: UIView?
private var answerTextView: ConstrainedTextView?
private var closeButton: UIButton?
private var tipLabel: UILabel?
// private var generalHeightConstraint: NSLayoutConstraint?
private var separatorHeightConstraint: NSLayoutConstraint?
private var answerTextViewBottomConstraint: NSLayoutConstraint?
private let junk = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureView()
}
private func configureView() {
// view.autoresizingMask = .flexibleHeight
view.backgroundColor = UIColor.white
view.frame = CGRect(x: 0, y: 0, width: view.superview?.bounds.width ?? 100, height: 70)
// view.translatesAutoresizingMaskIntoConstraints = false
// generalHeightConstraint = AutoLayoutSetAttribute(view, .height, 70)
// Separator
separatorView = UIView()
separatorView?.backgroundColor = UIColor.gray
separatorView?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(separatorView!)
AutoLayoutEqualizeSuper(separatorView, .left, 0)
AutoLayoutEqualizeSuper(separatorView, .right, 0)
AutoLayoutEqualizeSuper(separatorView, .top, 0)
separatorHeightConstraint = AutoLayoutSetAttribute(separatorView, .height, 1)
// Close Button
closeButton = UIButton(type: .system)
closeButton?.setTitle("Hide", for: .normal)
closeButton?.titleLabel?.font = UIFont.systemFont(ofSize: 17)
closeButton?.translatesAutoresizingMaskIntoConstraints = false
closeButton?.addTarget(self, action: #selector(dismissKeyboard), for: .touchUpInside)
view.addSubview(closeButton!)
AutoLayoutSetAttribute(closeButton, .width, 70)
AutoLayoutEqualizeSuper(closeButton, .right, -5)
view.addConstraint(NSLayoutConstraint(item: closeButton!, attribute: .top, relatedBy: .equal, toItem: separatorView, attribute: .bottom, multiplier: 1, constant: 0))
// Tip Label
tipLabel = UILabel()
tipLabel?.textColor = UIColor.darkGray
tipLabel?.text = "Input answer:"
tipLabel?.font = UIFont.systemFont(ofSize: 17)
tipLabel?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tipLabel!)
AutoLayoutEqualizeSuper(tipLabel, .left, 5)
AutoLayoutEqualize(tipLabel, separatorView, .top, 0)
view.addConstraint(NSLayoutConstraint(item: tipLabel!, attribute: .right, relatedBy: .equal, toItem: closeButton, attribute: .left, multiplier: 1, constant: 0))
// Text View
answerTextView = ConstrainedTextView()
answerTextView?.backgroundColor = UIColor.white
answerTextView?.delegate = self
answerTextView?.scrollsToTop = false
answerTextView?.showsVerticalScrollIndicator = false
answerTextView?.font = REG_FONT(15)
answerTextView?.maxLines = 5
answerTextView?.translatesAutoresizingMaskIntoConstraints = false
answerTextView?.layer.masksToBounds = true
answerTextView?.layer.cornerRadius = 7
answerTextView?.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.7).cgColor
answerTextView?.layer.borderWidth = 1
view.addSubview(answerTextView!)
AutoLayoutEqualizeSuper(answerTextView, .left, 5)
AutoLayoutEqualizeSuper(answerTextView, .right, -5)
answerTextViewBottomConstraint = AutoLayoutEqualizeSuper(answerTextView, .bottom, -5)
view.addConstraint(NSLayoutConstraint(item: answerTextView!, attribute: .top, relatedBy: .equal, toItem: tipLabel, attribute: .bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: answerTextView!, attribute: .top, relatedBy: .equal, toItem: closeButton, attribute: .bottom, multiplier: 1, constant: 0))
answerTextView?
.rx
.observe(CGRect.self, "bounds")
.distinctUntilChanged {
$0?.size.height == $1?.size.height
}
.subscribe { [unowned self] newBounds in
if var newHeight = newBounds.element??.size.height,
let separatorHeight = self.separatorHeightConstraint?.constant,
let buttonHeight = self.closeButton?.intrinsicContentSize.height,
let bottomSpace = self.answerTextViewBottomConstraint?.constant {
newHeight = newHeight == 0 ? 30 : newHeight
let generalHeight = newHeight + separatorHeight + buttonHeight + abs(bottomSpace)
self.view.frame = CGRect(x: 0, y: 0, width: self.view.superview?.bounds.width ?? 100, height: generalHeight)
// self.generalHeightConstraint?.constant = generalHeight
// UIView.animate(withDuration: 0.2) {
// self.view.setNeedsLayout()
// self.view.layoutIfNeeded()
// }
}
}
.addDisposableTo(junk)
}
}
// MARK: - UITextViewDelegate Protocol Conformance
extension InputViewController: UITextViewDelegate {
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
textView.inputAccessoryView = view
return true
}
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
textView.inputAccessoryView = nil
return true
}
}
View Controller where input accessory VC is used:
import UIKit
class TestViewController: UIViewController {
override var inputAccessoryViewController: UIInputViewController? {
return SDAnswerInputViewController()
}
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
Can you explain how shall I correctly modify height of input accessory view overriding inputAccessoryViewController?
The problem was in these two lines:
view.addConstraint(NSLayoutConstraint(item: answerTextView!, attribute: .top, relatedBy: .equal, toItem: tipLabel, attribute: .bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: answerTextView!, attribute: .top, relatedBy: .equal, toItem: closeButton, attribute: .bottom, multiplier: 1, constant: 0))
The answerTextView couldn't modify it's height because of constraints at the bottom and the top.

Programmatically add full size view in swift

I'm working with a iMessage application and have programmatically added a view. However I can't seem to work out the correct constraints for making it the correct size at all times. For example, the view moves down a few hundred px if I leave the extension for another and come back to it. I think this has something to do with the .isActive. My goal is to make the view automatically resize to always be the right size or take up the full available height and width.
func createBrowser() {
let controller = MSStickerBrowserViewController(stickerSize: .small)
addChildViewController(controller)
view.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.stickerBrowserView.backgroundColor = UIColor.blue
controller.stickerBrowserView.dataSource = self
view.topAnchor.constraint(equalTo: controller.view.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: controller.view.bottomAnchor).isActive = true
view.leftAnchor.constraint(equalTo: controller.view.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: controller.view.rightAnchor).isActive = true
view.centerXAnchor.constraint(equalTo: controller.view.centerXAnchor).isActive = true
view.centerYAnchor.constraint(equalTo: controller.view.centerYAnchor).isActive = true
}
Screenshot: https://d17oy1vhnax1f7.cloudfront.net/items/1F2B0s3v0s1k3E2L0Z07/Screen%20Shot%202016-09-19%20at%2011.42.51%20AM.png
to better explain things I've put together the following. This demonstrates two methods of fixing the layout for subviews. When using constraints, I prefer to create the constraints as an array and activate them all in one go, as you will see in the code for createredSquareWithConstraints. A constraint is simply a linear equation relating the features of one view to that of another. In "pseudocode", for example, the first constraint in my array could be written:
"Set the leading margin of the subview equal to 1 times the leading margin of the container view plus a constant of 0."
(This is why I was getting confused earlier as it looked to me as though you were setting the containing view's constraints based on the characteristics of one of its subviews.)
While it remains perfectly valid to use layout constraints, I think the preferred methodology these days is to override the viewWillTransitionToSize() delegate method, which simply asks you to specify, given a size for the containing view, what the frame of a view controller's subviews should be. As such, I've included an implementation of this too, creating a yellow square with an initial frame that is then modified whenever viewWillTransitionToSize is called. I personally find this a lot less fiddly that using layout constraints.
If you lay around with the buttons and rotate the screen you should see that either method achieves the same thing. [NB I have labelled one square as constrained and one as unconstrained, but in reality they are of course both constrained, just in different ways. I would add that this is clearly not how you would do things in practice - you should choose one methodology and stick to it otherwise your code will be all over the place!].
Hope that helps!
import UIKit
class ViewController: UIViewController {
var constrainedredSquare : UIView!
var unconstrainedRedSquare : UIView!
var methodOneButton : UIButton!
var methodTwoButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor.blue
func getButton(name: String) -> UIButton {
let button : UIButton = UIButton()
button.backgroundColor = UIColor.white
button.layer.cornerRadius = 3
button.clipsToBounds = true
button.setTitle(name, for: UIControlState.normal)
button.setTitleColor(UIColor.black, for: UIControlState.normal)
return button
}
self.methodOneButton = getButton(name: "Red - Constraints")
self.methodTwoButton = getButton(name: "Yellow - viewWillTransitionToSize")
self.methodOneButton.addTarget(self, action: #selector(self.createRedSquareWithConstraints), for: .touchUpInside)
self.methodTwoButton.addTarget(self, action: #selector(self.createYellowSquareWithoutConstraints), for: .touchUpInside)
self.methodOneButton.frame = CGRect(origin: CGPoint(x: 200, y: 100), size: CGSize(width: 300, height: 300))
self.methodTwoButton.frame = CGRect(origin: CGPoint(x: self.view.frame.width - 500, y: 100), size: CGSize(width: 300, height: 300))
self.view.addSubview(self.methodOneButton)
self.view.addSubview(self.methodTwoButton)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if let _ = self.unconstrainedRedSquare {
self.unconstrainedRedSquare.frame = CGRect(origin: CGPoint.zero, size: size)
}
self.methodOneButton.frame = CGRect(origin: CGPoint(x: 200, y: 100), size: CGSize(width: 300, height: 300))
self.methodTwoButton.frame = CGRect(origin: CGPoint(x: size.width - 500, y: 100), size: CGSize(width: 300, height: 300))
}
func createYellowSquareWithoutConstraints() {
if let _ = self.unconstrainedRedSquare {
self.unconstrainedRedSquare.removeFromSuperview()
}
else
{
if let _ = constrainedredSquare {
self.constrainedredSquare.removeFromSuperview()
}
self.unconstrainedRedSquare = UIView()
self.unconstrainedRedSquare.backgroundColor = UIColor.yellow
self.unconstrainedRedSquare.frame = CGRect(origin: CGPoint.zero, size: self.view.frame.size)
self.view.addSubview(self.unconstrainedRedSquare)
self.view.bringSubview(toFront: self.methodOneButton)
self.view.bringSubview(toFront: self.methodTwoButton)
}
}
func createRedSquareWithConstraints() {
if let _ = self.constrainedredSquare {
self.constrainedredSquare.removeFromSuperview()
}
else
{
if let _ = self.unconstrainedRedSquare {
self.unconstrainedRedSquare.removeFromSuperview()
}
let redSquare : UIView = UIView()
redSquare.backgroundColor = UIColor.red
self.view.addSubview(redSquare)
self.view.bringSubview(toFront: self.methodOneButton)
self.view.bringSubview(toFront: self.methodTwoButton)
let rsConstraints : [NSLayoutConstraint] = [NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.leading, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.leading, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.top, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.bottom, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.width, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.height, multiplier: 1.0, constant: 0)]
redSquare.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(rsConstraints)
}
}
}
You can use my extension to UIView. It allows to add extra padding on any side (only if you want to):
public extension UIView {
typealias ConstraintsTupleStretched = (top:NSLayoutConstraint, bottom:NSLayoutConstraint, leading:NSLayoutConstraint, trailing:NSLayoutConstraint)
func addSubviewStretched(subview:UIView?, insets: UIEdgeInsets = UIEdgeInsets() ) -> ConstraintsTupleStretched? {
guard let subview = subview else {
return nil
}
subview.translatesAutoresizingMaskIntoConstraints = false
addSubview(subview)
let constraintLeading = NSLayoutConstraint(item: subview,
attribute: .Left,
relatedBy: .Equal,
toItem: self,
attribute: .Left,
multiplier: 1,
constant: insets.left)
addConstraint(constraintLeading)
let constraintTrailing = NSLayoutConstraint(item: self,
attribute: .Right,
relatedBy: .Equal,
toItem: subview,
attribute: .Right,
multiplier: 1,
constant: insets.right)
addConstraint(constraintTrailing)
let constraintTop = NSLayoutConstraint(item: subview,
attribute: .Top,
relatedBy: .Equal,
toItem: self,
attribute: .Top,
multiplier: 1,
constant: insets.top)
addConstraint(constraintTop)
let constraintBottom = NSLayoutConstraint(item: self,
attribute: .Bottom,
relatedBy: .Equal,
toItem: subview,
attribute: .Bottom,
multiplier: 1,
constant: insets.bottom)
addConstraint(constraintBottom)
return (constraintTop, constraintBottom, constraintLeading, constraintTrailing)
}
}
Usage:
view.addSubviewStretched(tableView)
let BorderedBackgroundInset = UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1)
view?.addSubviewStretched(calendar.view, insets: BorderedBackgroundInset)

Resources