Expanding UITextView with UIButton instead of automatically based on content - ios

I have a UITextView which is embedded in a UIView amongst a number of other UIViews which are all in a UIScrollView (a form essentially). Instead of the textView automatically expanding with the content, I have a button underneath which I would like the user to be able to click and expand/colapse the textView.
Here is what I have:
var textViewIsExpanded: Bool = false {
didSet {
if self.textViewIsExpanded {
self.expandTextViewButton.isSelected = true
guard self.myTextView.contentSize.height > 70 else { return }
self.myTextView.isScrollEnabled = false
self.myTextView.translatesAutoresizingMaskIntoConstraints = true
self.myTextView.sizeThatFits(CGSize(width: self.scrollView.width - 24, height: CGFloat.greatestFiniteMagnitude))
} else {
self.expandTextViewButton.isSelected = false
self.myTextView.isScrollEnabled = true
self.myTextView.translatesAutoresizingMaskIntoConstraints = false
}
}
}
#IBAction func expandTextViewButtonTapped(_ sender: UIButton) {
textViewIsExpanded.toggle()
}
I have tried .sizeToFit() in place of .sizeThatFits(...) which sort of worked but it resized the width along with the height and I am only looking to expand/colapse the height. I'm guessing it is a matter of correctly implementing the CGSize and/or IB constraints but I am not able to land on a solution that works how I want.

First, it's a bad idea to toggle .translatesAutoresizingMaskIntoConstraints between true and false.
What you probably want is to give your text view a Height constraint of 70... connect it to an #IBOutlet... and then toggle .isActive on that constraint.
Second, if you have only one line of text, so the content size height is, maybe, 30, and then you call
textViewIsExpanded = true
your code as-is will set textViewIsExpanded to true but will leave .isScrollEnabled true --- so it won't really be "expanded".
Third, you need to let auto-layout know that you're changing the sizing behavior of the text view by calling:
self.myTextView.invalidateIntrinsicContentSize()
after toggling .isScrollEnabled.
So, add and connect a property for your text view's height constraint:
#IBOutlet var textViewHeightConstraint: NSLayoutConstraint!
and try changing your code to this:
var textViewIsExpanded: Bool = false {
didSet {
if self.textViewIsExpanded {
// if contentSize.height is less-than 71
// reset to false
if self.myTextView.contentSize.height < 71 {
self.textViewIsExpanded = false
return
} else {
self.expandTextViewButton.isSelected = true
self.myTextView.isScrollEnabled = false
self.textViewHeightConstraint.isActive = false
}
} else {
self.expandTextViewButton.isSelected = false
self.myTextView.isScrollEnabled = true
self.textViewHeightConstraint.isActive = true
}
self.myTextView.invalidateIntrinsicContentSize()
}
}

Related

StackView isHidden attribute not updating as expected

I'm trying to update a UIStackView so that a field displays, should the value of a UITextField equal "Other". Here is my code:
#IBOutlet var stackView: UIStackView!
func updateView() {
print("UPDATING")
UIView.animate(withDuration: 0.25, animations: { () -> Void in
if(self.myTextField.text! == "Other") {
print("SHOWING")
self.stackView.arrangedSubviews[3].isHidden = false
} else {
print("HIDING")
self.stackView.arrangedSubviews[3].isHidden = true
}
print("Is hidden: \(self.stackView.arrangedSubviews[3].isHidden )")
})
An example output looks like this:
> UPDATING
> HIDING
> Is hidden: true
> UPDATING
> SHOWING
> Is hidden: true
As you can see, the isHidden attribute is reported as true, regardless of what the code above has set it to. I can't really figure out why that might be, but perhaps someone on here can? Is there something obvious to check? Is there any reason why isHidden can't be updated? (note there are no errors appearing in the output).
It's known UIStackView bug (http://www.openradar.me/25087688). There is a thread on SO about it: (Swift: Disappearing views from a stackView). Long story short:
The bug is that hiding and showing views in a stack view is
cumulative. Weird Apple bug. If you hide a view in a stack view twice,
you need to show it twice to get it back.
To fix this issue you can use following extension:
extension UIView {
var isHiddenInStackView: Bool {
get {
return isHidden
}
set {
if isHidden != newValue {
isHidden = newValue
}
}
}
}
Updates on the user interface always have to be done on the main thread (THE LAW).
So wrap you UI updates on the main thead:
#IBOutlet var stackView: UIStackView!
func updateView() {
print("UPDATING")
UIView.animate(withDuration: 0.25, animations: { () -> Void in
DispatchQueue.main.async { // UI updates on the main thread
if(self.myTextField.text! == "Other") {
print("SHOWING")
self.stackView.arrangedSubviews[3].isHidden = false
} else {
print("HIDING")
self.stackView.arrangedSubviews[3].isHidden = true
}
print("Is hidden: \(self.stackView.arrangedSubviews[3].isHidden )")
}
})
try to manipulate alpha along with isHidden property:
self.stackView.arrangedSubviews[3].isHidden = true
self.stackView.arrangedSubviews[3].alpha = 0
self.stackView.arrangedSubviews[3].isHidden = false
self.stackView.arrangedSubviews[3].alpha = 1

UIButton exceeding view controller without clipping to its bounds

In my app I'm using KYDrawerController library from here: https://github.com/ykyouhei/KYDrawerController
It works as expected, but I want to add an UIButton on the menu view controller (which is on top when opened), however it's clipped by view bounds. Best way I can explain this is by showing you a screenshot:
And here's how it should look like:
Button now has negative right constraint margin so it's position is correct, but how can I disable clipping?
In the menu view controller, which can you see on the foreground, I've added this code:
self.navigationController?.navigationBar.clipsToBounds = false
self.navigationController?.view.clipsToBounds = false
self.view.clipsToBounds = false
let elDrawer = self.navigationController?.parent as! KYDrawerController
elDrawer.view.clipsToBounds = false
elDrawer.displayingViewController?.view.clipsToBounds = false
elDrawer.drawerViewController?.view.clipsToBounds = false
elDrawer.displayingViewController?.view.clipsToBounds = false
elDrawer.mainViewController.view.clipsToBounds = false
elDrawer.inputViewController?.view.clipsToBounds = false
elDrawer.splitViewController?.view.clipsToBounds = false
As you can see I've tried all possible ways to disable clipping, yet it's still clipped. How can I achieve this?
edit:
Added hierachy view:
I've also tried to run following test:
var view = arrowButton as UIView?
repeat {
view = view?.superview
if let sview = view {
if(sview.clipsToBounds){
print("View \(view) clips to bounds")
break
}
else{
print("View \(view) does not clip to bounds")
}
}
} while (view != nil)
And it prints:
View Optional(>) does not clip to
bounds
So looks like nothing is clipping yet it's clipped.
edit2:
debug view hierarchy:
Yay, I've found the solution:
self.navigationController?.view.subviews[0].clipsToBounds = false
old code is not needed.
UINavigationTransitionView (it's Apple private class) was the one responsible with clipToBounds turned on.
We can't really tell what's going on because you don't show us the view hierarchy.
I suggest you write test code that starts at the button, walking up the superview hierarchy looking for a superview who's' clipsToBounds is true. Something like this:
var view = button as UIView?
repeat {
view = view.superview
if view?.superview.clipsToBounds ?? false == true {
print("View \(view) clips to bounds")
break
} while view != nil
Then you'll know which view is clipping, and you can fix it witout the "shotgun" approach you're using.
EDIT:
I'm not sure why you are getting such strange debug messages. I suggest adding unique tag numbers to all the button's superviews, and then using code like this:
override func viewDidAppear(_ animated: Bool) {
var view = button as UIView?
repeat {
view = view?.superview
if let view = view {
let tag = view.tag
let description = (tag == 0) ? view.debugDescription : "view w tag \(tag)"
if(view.clipsToBounds){
print("View \(description) clips to bounds")
break
}
else{
print("View \(description) does not clip to bounds")
}
}
} while (view != nil)
}
Post ALL the output from that debug code so we can see the entire view hierarchy you're dealing with.

Swift how to move UIButton programmatically?

I have an infoButton UIButton outlet in my swift file. This button is originally placed in the storyboard with the following constraints:
I want to move this button down by 50 pixels depending if NoAds boolean is true or false.
I have tried doing this, but I can't seem to move it.
if NoAds{
print("No Ads")
infoButton.center.y = infoButton.center.y - 50
}else{
print("Ads")
loadBanner()
}
I assume this should be an easy fix?
EDIT: The ad is a standard google ad banner which is 320 wide and 50 high.
have an outlet for the top constraint
change the topConstraint.constant
like this:
if NoAds{
print("No Ads")
//topConstraint.constant -= 50
topConstraint.constant = 50 // Avoid using -= or += if this func will call a lot of time
}else{
print("Ads")
// set the default constant you want
topConstraint.constant = 100
loadBanner()
}
Use an IBOutlet to connect to the constraint as #wilson-xj describes.
Then use the following code to animate the change to the constraint. This will add some polish and allow the ad to slide in/out nicely instead of jarring the user by instantly shifting content they may be interacting with.
view.layoutIfNeeded()
UIView.animateWithDuration(0.30) { () -> Void in
self.topConstraint.constant = constraintConstant // 50 or 100 etc
self.view.layoutIfNeeded()
}
The best way would probably be to add the constraints and dynamically change the button, but if you didn't want to add another outlet and wanted to do this dynamically you could change the position of the button frame like so:
var X_Position:CGFloat? = startButton.frame.origin.x + 50 //add 50 to move button down page
var Y_Position:CGFloat? = startButton.frame.origin.y
startButton.frame = CGRectMake(X_Position, Y_Position, startButton.frame.width, startButton.frame.height)
This will redraw the button in the new position.
Try this i hope it would be helpful This is mine code i am using!!
and i Assume that you all set the constraints like Top,Bottom,leading,Trailing space
if NoAds
{
print("No Ads")
for item in self.view.constraints
{
if item.firstItem .isKindOfClass(UIButton)
{
let newField = item.firstItem as! UIButton
if newField == buttonName && item.firstAttribute == NSLayoutAttribute.Top
{
item.constant = -50
self.view .layoutIfNeeded()
}
}
}
}else{
print("Ads")
for item in self.view.constraints
{
if item.firstItem .isKindOfClass(UIButton)
{
let newField = item.firstItem as! UIButton
if newField == buttonName && item.firstAttribute == NSLayoutAttribute.Top
{
item.constant = 0
self.view .layoutIfNeeded()
}
}
}
loadBanner()
}

How to add a "More" and "Less" text indicator in a expandable UILabel?

I am a IOS learner. I was doing a sample project, in which i require "More" and "Less" text indicators in a UILabel for which i have added a SingleTapGestureRecognizer to expand and contract based on the text content.The text content comes from API and is dynamic.
Here i have attached the screen shot of my required output.(The screen shots are from android).
Also i resize the UILabel programmatically except the constraints(No Auto layout). the code use for tap gesture is
In viewDidLoad()
let singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: "singleTapped")
singleTapGestureRecognizer.numberOfTapsRequired = 1
singleTapGestureRecognizer.enabled = true
singleTapGestureRecognizer.cancelsTouchesInView = false
lblAmneties.addGestureRecognizer(singleTapGestureRecognizer)
the method is
var amnetiesClicked = false
func singleTapped()
{
if(amnetiesClicked == false) { amnetiesClicked = true }
else { amnetiesClicked = false }
if(amnetiesClicked == true)
{
Amneties.sizeToFit()
self.display()
}
else
{
if(self.Amneties.frame.size.height > 70 )
{
self.Amneties.frame.size.height = 70
self.display()
}
}
}
Here display() is the method where i align all the contents of the view programmatically.
I have no idea on how to do it.
I use xcode 7.1.1 Swift 2.0
I used TTTAttributed Label to do that https://github.com/TTTAttributedLabel/TTTAttributedLabel
It has a predefined method to do that.

Auto align vertically when width decreases

I have three buttons aligned like below when the width of the container is 640px:
UIButton 1 <30px> UIButton 2 <30px> UIButton 3
When the width decreases to 320px, I would like to bring the third button to the next line like this:
UIButton 1 <30px> UIButton 2
UIButton 3
Let me know if this can be done using autolayout without creating outlets of the constraints.
You are must overide layoutSubviews and recalculate manualy setFrame's properties for each views.
I found this blog post with example on github
Ended up using a collection view with a left aligned vertical flow layout. It may seem a bit of an overkill but works nice.
Because there wasn't any specification about how/where to create such constraints I'd go this way.
Here is a little NSLayoutConstraint Extension of mine to create constraints fast (for iOS 9 and later).
I'm not using StoryBoards/IB so here is a fast code solution in Swift (2.0):
class SomeViewController: UIViewController {
private let button1 = UIButton(type: UIButtonType.System)
private let button2 = UIButton(type: UIButtonType.System)
private let button3 = UIButton(type: UIButtonType.System)
private var buttonArray: [UIButton] { return [self.button1, self.button2, self.button3] }
private var constraintsForWideWidth: [NSLayoutConstraint] = []
private var constraintsForShortWidth: [NSLayoutConstraint] = []
override func viewDidLoad() {
super.viewDidLoad()
self.button1.translatesAutoresizingMaskIntoConstraints = false
self.button2.translatesAutoresizingMaskIntoConstraints = false
self.button2.translatesAutoresizingMaskIntoConstraints = false
// set your buttons somewhere !!!
self.view.addSubview(self.button1)
self.view.addSubview(self.button2)
self.view.addSubview(self.button3)
self.setupConstraints()
}
private func setupConstraints() {
// points are doubled px on retina display
// setup the width and/or height as you would like
// for this example I'll set them with some static numbers
for button in self.buttonArray {
NSLayoutConstraint.with(button.widthAnchor == 80)
NSLayoutConstraint.with(button.heightAnchor == 20)
}
NSLayoutConstraint.with(self.button2.leftAnchor == self.button1.rightAnchor, constant: 15)
NSLayoutConstraint.with(self.button2.centerYAnchor == self.button1.centerYAnchor)
self.constraintsForWideWidth.append(NSLayoutConstraint.with(self.button3.leftAnchor == self.button2.rightAnchor, constant: 15))
self.constraintsForWideWidth.append(NSLayoutConstraint.with(self.button3.centerYAnchor == self.button2.centerYAnchor))
self.constraintsForShortWidth.append(NSLayoutConstraint.with(self.button3.topAnchor == self.button1.bottomAnchor, active: false))
self.constraintsForShortWidth.append(NSLayoutConstraint.with(self.button3.centerXAnchor == self.button1.centerXAnchor, active: false))
}
func someFunctionThatExecutesWhenTheSizeChanges() {
// deactivate both, because we want less code later
NSLayoutConstraint.deactivateConstraints(self.constraintsForShortWidth)
NSLayoutConstraint.deactivateConstraints(self.constraintsForWideWidth)
// 640 px are 320 pt
if self.view.frame.width == 320 {
NSLayoutConstraint.activateConstraints(self.constraintsForWideWidth)
} else if self.view.frame.width == 160 {
NSLayoutConstraint.activateConstraints(self.constraintsForShortWidth)
}
self.view.layoutIfNeeded()
}
}
Don't be afraid of the code, it is not that much and really not that hard to understand. :) Hope that will help you somehow.

Resources