Updating Main.storyboard constraints programatically (Swift) - ios

I want to update the bottom anchor of my textview to a constant equal to the height of the keyboard when it appears so that it doesn't cover the text in the textView. I have a constraint identifier in Main.storyboard set as "bottomTextViewConstraint" for my textView, we well as the following code:
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
print("This prints")
for constraint in self.textView.constraints {
print("This does not print")
if constraint.identifier == "bottomTextViewConstraint" {
constraint.constant = keyboardSize.height
print("This does not print")
}
}
textView.updateConstraints()
}
}
}
self.textView.constraints is nil... It seems that programatically I can't access what I have set up in the storyboard. Any ideas why?

A constraint relating the bottom anchor of a text view and its super view's bottom anchor will be added to the text view's super view, so you cannot find it in textView.constraints.
A much simpler way to do this is to add an IBOutlet from the storyboard:
#IBOutlet var bottomConstraint: NSLayoutConstraint!
Right click on your view controller in the storyboard, then connect bottomConstraint to the constraint shown in the outline view:
Then you can remove your entire for loop and replace it with just:
bottomConstraint.constant = keyboardSize.height

Related

Add/ expand animation will cause unwanted UIScrollView scrolling

I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.

Constrains count is always getting zero ( 0 ) in AutoLayout - iOS

I am getting one weird issue, I am setting constraints on the view using storyboard as below:
But if I am accessing all applied constraint by code then always getting zero (0).
for self.view it's returning the constraint but for yellow view getting zero.
Use this code (Swift 4+ and Xcode 9.4.1)
import UIKit
class ViewController: UIViewController {
#IBOutlet var subView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
print(self.view.getAllConstraints().count)
print(self.subView.getAllConstraints().count)
}
}
extension UIView {
func getAllConstraints() -> [NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { constraint in
return constraint.firstItem as? UIView == self ||
constraint.secondItem as? UIView == self
}
}
}
Just try to print constraints in viewDidAppear. In viewDidLoad method constraints not loaded for view so, just try in viewDidAppear method.
for your information when viewDidLoad is called subviews is not laid out yet thats why you getting count zero put you code into viewDidLayoutSubviews you will get your constraints there

Prevent UITextView from auto-scrolling when when being resized through auto-layout

My view has a TextView with 0 as the number of lines, populated with a long text that the user can edit.
When the keyboard shows, to resize the TextView so that it fits in the visible portion of the screen, the height constraint of an empty view at the bottom of the screen is equaled to the keyboard height. The bottom of the textView is constrained to the top of the empty view, so the text view gets resized.
When this happens, the textView automatically scrolls down in the text. I would like the text to avoid this scrolling, so that the first line always stays visible. I have tried a few ways, such as:
Disabling scrolling between keyboardWillShow and keyboardDidShow, not working
Scrolling back to zero, but we can see the text scrolling down and then scrolling back up (textView.scrollRangeToVisible(NSRange(location:0, length:0)))
Here's a drawing to make it much clearer (can't embed it yet, sorry):
UITextView Drawing
Relevant code:
func keyboardWillShow(_ notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let duration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
emptyViewHeightConstraint.constant = keyboardSize.height
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
}
func keyboardWillHide(_ notification: NSNotification) {
let duration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
emptyViewHeightConstraint.constant = 0
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
PS: The TextView is actually inside a StackView, and this StackView has its bottom constrained to the top of the empty view.

Autolayout: How to compress a complex popover

If you like to try the source code (which you are very welcome to do), have a look at my Bitbucket repository.
I have a popover dialogue that shows a list of settings. These settings a listed inside multiple UITableViews. The UITableViews shall not be scrollable, for the overall settings view already is. Furthermore, the popover dialogue shall take as much screen vertically as it needs but shall be horizontally compressed.
Thus, I conceived the following structure:
UIView => MySettingsViewController
- UIScrollView
- UIView (Content View)
- Container View1
- UITableView (embedded) => MyTableViewController
- Container View2
- UITableView (embedded)
The structure is assembled via Interface Builder and Autolayout is used for the sizing.
I have both the Scroll View, the Content View (I started with just one) and the Container View to their respective superviews (or layout guides). I constrained the size of the content view in the following manner:
contentView.width == (topmost) UIView.width
contentView.height == 200 // removed at build time
Additionally, I set the size of the table view to its content size, because otherwise the popover appears to be empty:
class MyTableViewController: UITableViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// this is Cartography syntax - the intention should be clear
layout(view, replace: ConstraintGroup()) { [unowned self] view in
view.width == self.tableView.contentSize.width
view.height == self.tableView.contentSize.height
}
view.setNeedsLayout()
}
}
The settings popover is filled with content, but its size is not quite right:
To fix this, I tried the following approach which does not work:
class MySettingsViewController: UIViewController {
override var preferredContentSize: CGSize {
get {
let compressedSize = view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
// this is always (0, 0) because the subviews are not resized, yet
return compressedSize
}
set {
super.preferredContentSize = newValue
}
}
}
To conclude: The compression does not work.
So I just fixed the problem myself as you can see when looking at the Bitbucket repository.
The layout is now fixed both in MyTableViewController and MySettingsViewController. The former one now looks like this:
class MyTableViewController: UITableViewController {
var heightConstraint: NSLayoutConstraint?
var tableViewEdgesConstraints: [NSLayoutConstraint]?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let container = tableView.superview where tableViewEdgesConstraints == nil {
layout(tableView, container, replace: ConstraintGroup()) { [unowned self] tableView, container in
self.tableViewEdgesConstraints = tableView.edges == inset(container.edges, 0)
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let heightConstraint = heightConstraint {
if Int(heightConstraint.constant) != Int(tableView.contentSize.height) {
heightConstraint.constant = self.tableView.contentSize.height
}
} else {
layout(view, replace: ConstraintGroup()) { [unowned self] view in
if (self.tableView.contentSize.height > 0) {
self.heightConstraint = view.height == self.tableView.contentSize.height
}
}
}
}
}
So basically, I constraint the height of the table to its content's height and change the constraint if the content's height changes. This is done as soon as the table is laid out. Furthermore, the nested table view is pinned by its edges to the edges of the container view. I think that this is mandatory because I could not find out how to constrain two views of different scenes right in Interface Builder.
In MySettingsViewController the scrollview's size is set to the size of the content view's frame (which is accessible via an outlet) as soon as this size is known. Furthermore, to make the popover compress, the preferredContentSize of the settings controller is adapted accordingly, when the height changes (if you omit the condition you might get yourself in a layout endless loop. Furthermore I did 3 things to make it possible to have a navigation controller wrapped around MySettingsViewController:
The width of the popover is set to a fixed value (otherwise it would sometimes expand to the full width).
The presentedViewController's preferredContentSize needs to be set equally.
I had to set the insets of the scrollView to 0 to avoid an ugly vertical offset - this solution is sub-optimal because it breaks the scroll view experience a bit. But it works.
Here is the code:
class MySettingsViewController: UIViewController {
#IBOutlet weak var contentView: UIView!
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = contentView.frame.size
if (preferredContentSize.height != scrollView.contentSize.height) {
let newSize = CGSize(width: 400, height: scrollView.contentSize.height)
preferredContentSize = newSize
presentingViewController?.presentedViewController?.preferredContentSize = newSize
scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
}
}
And this is the result:

Customise Position of UIPageControl

I am using project from github as a reference.
project URL:
https://github.com/lephuocdai/iOSsample/tree/master/PageViewDemo
In this project i want to show the UIPageControl at top left position .
I tried setting the rect property of pageControl using CGRectMake() to some value ;But it shows always at bottom center
here s a very neat and 100% effective way to get to change the position of the pageControl
extension UIPageViewController {
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subV in self.view.subviews {
if type(of: subV).description() == "UIPageControl" {
let pos = CGPoint(x: newX, y: newY)
subV.frame = CGRect(origin: pos, size: subV.frame.size)
}
}
}
}
The project uses a UIPageViewController to handle the display and movement through the content.
You can supply data to that object so it displays a UIPageControl as you say.
BUT you have no control over the display of that item, other than maybe some colour styling.
If you want to position it, you'll need to implement you're own instance of UIPageControl and handle it's content, position and changes manually.
Override the viewDidLayoutSubviews() of the pageviewcontroller and use this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// get pageControl and scroll view from view's subviews
let pageControl = view.subviews.filter{ $0 is UIPageControl }.first! as! UIPageControl
let scrollView = view.subviews.filter{ $0 is UIScrollView }.first! as! UIScrollView
// remove all constraint from view that are tied to pagecontrol
let const = view.constraints.filter { $0.firstItem as? NSObject == pageControl || $0.secondItem as? NSObject == pageControl }
view.removeConstraints(const)
// customize pagecontroll
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.addConstraint(pageControl.heightAnchor.constraintEqualToConstant(35))
pageControl.backgroundColor = view.backgroundColor
// create constraints for pagecontrol
let leading = pageControl.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
let trailing = pageControl.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor)
let bottom = pageControl.bottomAnchor.constraintEqualToAnchor(scrollView.topAnchor, constant:8) // add to scrollview not view
// pagecontrol constraint to view
view.addConstraints([leading, trailing, bottom])
view.bounds.origin.y -= pageControl.bounds.maxY
}
is your page control encompassed within some other View, if so then you may be setting the co-ordinates wrong, try to put log of your page control.frame so as to know where it lies
If using storyboards, place a UIPageControl object using the menu in the bottom right and set constraints.
If using frames, just programmatically add it:
var pageControl = UIPageControl()
pageControl.frame = CGRectMake(0,0,0,0) <- These are the coordinates.
self.view.addSubView(pageControl)
if you set the frame for UIPageControl , it doesnot work.
Other than this you can set the transform.
[_pageControl setTransform:CGAffineTransformMakeTranslation(100, 0.0)];
enjoy coding

Resources