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
Related
How do I change the button background color when the button is highlighted with swift,then I also want to know . How can I make this background color last for a period of time instead of clicking it to disappear immediately? For example, I want it to disappear 2 seconds after clicking. I checked some information and couldn't find a way. All I can think of is to write a click event, then change the background color of the button and set a time to change it back
I found the method. As a novice, I can't fully understand it, but it can be used. Friends with similar problems can refer to it
class MyButton : UIButton {
#IBInspectable var normalBackgroundColor: UIColor? {
didSet {
backgroundColor = normalBackgroundColor
}
}
#IBInspectable var highlightedBackgroundColor: UIColor?
override var isHighlighted: Bool {
didSet {
if oldValue == false && isHighlighted {
highlight()
} else if oldValue == true && !isHighlighted {
unHighlight()
}
}
}
func highlight() {
animateBackground(to: highlightedBackgroundColor, duration: highlightDuration)
}
func unHighlight() {
animateBackground(to: normalBackgroundColor, duration: highlightDuration)
}
var highlightDuration: TimeInterval = 1
private func animateBackground(to color: UIColor?, duration: TimeInterval) {
guard let color = color else { return }
UIView.animate(withDuration: highlightDuration) {
self.backgroundColor = color
}
} }
You can create a new file and write the above code
Then bind the button class as shown in the figure
Finally, your button has the content you want in the right property panel
You can modify the animation time by changing the value of highlightduration
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()
}
}
I've been creating my own UIControl subclass for use in my tweak iDunnoU. I've finished the UIControl, with the exception of the expand/collapse animation. The issue with this animation is that it 'jumps' down/up when expanding/collapsing, instead of fanning out smoothly like my original mockup (see below).
I have uploaded the code to a GitHub repository, found here. The code for adding the control to the superview can be found here, the code for setting up the height constraint can be found here, and the code for animating the height constraint can be found here.
UIView.animate() can be a little tricky -- you need to call .layoutIfNeeded() on the correct view.
Replace your isExpanded / didSet in iDUMenuButton class with this:
var isExpanded = false {
didSet {
if isExpanded != oldValue {
if isExpanded {
becomeFirstResponder()
let haptics = UIImpactFeedbackGenerator(style: .rigid)
haptics.impactOccurred()
}
guard let sv = self.superview else {
// shouldn't happen, but let's be thorough
fatalError("Self must have a superview!!!")
}
// not needed
//self.layoutIfNeeded()
UIView.animate(withDuration: 0.3) {
self.heightConstraint.isActive = !self.isExpanded
// call .layoutIfNeeded() on self's superview
//self.layoutIfNeeded()
sv.layoutIfNeeded()
self.layer.shadowOpacity = self.isExpanded ? 1 : 0
self.buttons.forEach { $0.setBadgeHidden(hidden: !self.isExpanded, animated: true) }
}
delegate?.menuButton(self, isExpandedDidUpdate: isExpanded)
}
}
}
I've been searching online for an answer to my question but there doesn't seem to be any solutions. I have a UILabel which contains two interchangeable icons (from FontAwesome). I would like to create an animation where it changes the UILabels text from one to the other repeatedly.
What I have so far is an animation which calls itself again and again. It seemed to look fine on the simulator but when I ran it on the phone it didn't work how I wanted it to. I seem to almost be there but my code creates some sort of flash when it fades out
func animateLabel() {
self.runningPersonLabel.text = self.runningPersonLabel.text == "ICON 1" ? "ICON 2" : "ICON 1"
self.runningPersonLabel.sizeToFit()
self.runningPersonLabel.center = modalContainer.boundsCenter
self.runningPersonLabel.frameTop = titleLabel.frameBottom + 40
UIView.animate(withDuration: 2, delay: 0, options: [.autoreverse], animations: {
self.runningPersonLabel.alpha = 1.0
}, completion: { finished in
self.runningPersonLabel.alpha = 0.0
self.animateLabel()
})
}
Try with this class:
import UIKit
#IBDesignable class FadingLabel: UILabel
{
// The secondary text
#IBInspectable var secondaryText:String?
var primaryText:String?
// Animation time, is divided by 2
#IBInspectable var animationTime:TimeInterval = 1
// Set this flag to true to stop animation
var stop = true
// Start the animation
func startAnimating()
{
stop = false
if primaryText == nil
{
primaryText = self.text
}
fadeAnim()
}
// Stop the animation
func stopAnimating(_ sender: UIButton)
{
stop = true
}
#objc private func fadeAnim()
{
if stop
{
return
}
// Fade out
UIView.animate(withDuration: animationTime / 2, animations: {
self.alpha = 0
}) { (complete) in
UIView.animate(withDuration: self.animationTime / 2, animations: {
if self.text == self.primaryText
{
self.text = self.secondaryText
}
else
{
self.text = self.primaryText
}
self.alpha = 1
}, completion: { (complete2) in
self.fadeAnim()
})
}
}
}
Usage:
put a label in your view controller;
set the label class to FadingLabel;
set the secondary text and the animation time in the storyboard inspector
call the methods startAnimating or stopAnimating as needed.
I have multiple controls on my screen. A collection view on right-top, then a button at left-center and besides the button, i have another collection view. Please refer the attached image for it
I am able to move the focus from button to bottom collection view and vice versa. I have created a focus guide for the same as below:
focusGuide.preferredFocusedView = self.btn
self.view.addLayoutGuide(self.focusGuide)
self.focusGuide.topAnchor.constraintEqualToAnchor(collectionViewHeader.topAnchor).active = true
self.focusGuide.bottomAnchor.constraintEqualToAnchor(collectionViewBottom.topAnchor).active = true
self.focusGuide.leadingAnchor.constraintEqualToAnchor(collectionViewBottom.leadingAnchor).active = true
self.focusGuide.widthAnchor.constraintEqualToAnchor(collectionViewBottom.widthAnchor).active = true
and in didUpdateFocusInContext: , I have write :
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
guard let nextFocusedView = context.nextFocusedView else { return }
if(nextFocusedView .isKindOfClass(bottomCell)) {
self.focusGuide.preferredFocusedView = self.btn
} else {
self.focusGuide.preferredFocusedView = self.collectionViewBottom
}
}
But, I am not able to move focus from button to top collection view. I may need multiple focus guide for this, but I do not know what should come there. Can anyone help me on this?
Thank you
I had similar problems in many parts of my UI so ended up defining a custom view to create larger areas of the screen that represent groups of focusable controls. Because I setup these larger areas to cover empty regions that don't contain controls, they intercept the focus movement and transfer to focus to the controls within the area independently of vertical or horizontal alignment with the original control where focus started from.
This is the custom view. You use it by placing one or more controls inside it in IB.
Note that it has additional features such as forcing focus to a specific control without having to override preferredFocusView but you can drop those if you don't need them).
class FocusGroup : UIView
{
weak private var nextFocusView:UIView? = nil
weak var initialView:UIView? = nil
var captureFocus:Bool = false
func focusOnView(view:UIView, now:Bool=false)
{
if not(view.isDescendantOfView(self))
|| view === self
{ return }
nextFocusView = view
setNeedsFocusUpdate()
if now { updateFocusIfNeeded() }
}
func resetFocus(now now:Bool=false)
{
nextFocusView = nil
setNeedsFocusUpdate()
if now { updateFocusIfNeeded() }
}
override func canBecomeFocused() -> Bool
{
if nextFocusView != nil { return true }
if containsFocus { return false }
return firstFocusableSubView() != nil
}
func firstFocusableSubView() -> UIView?
{
return findSubview({
$0.canBecomeFocused()
&& $0.userInteractionEnabled
&& $0.visible
&& ( not($0 is UIButton)
|| ($0 as! UIButton).enabled )
})
}
override var preferredFocusedView: UIView?
{
if let viewToFocus = ( nextFocusView ?? initialView ) ?? firstFocusableSubView()
{
return viewToFocus
}
return nil
}
override func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool
{
// when capturing focus, prevent navigation outside of grouped subviews
if captureFocus
&& containsFocus
&& context.previouslyFocusedView!.isDescendantOfView(self)
&& (
context.nextFocusedView == nil
|| context.nextFocusedView === self
|| not(context.nextFocusedView!.isDescendantOfView(self))
)
{ return false }
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
{
// give focus to specific view as requested
if nextFocusView != nil
{
if context.nextFocusedView === nextFocusView
|| not(nextFocusView!.canBecomeFocused())
{ nextFocusView = nil }
return
}
}
}
Just insert a focus guide above the button and to the left of the top collection view, redirecting focus to the top collection view:
focusGuide.preferredFocusedView = self.topView
self.view.addLayoutGuide(focusGuide)
self.focusGuide.topAnchor.constraintEqualToAnchor(topView.topAnchor).active = true
self.focusGuide.bottomAnchor.constraintEqualToAnchor(topView.bottomAnchor).active = true
self.focusGuide.leadingAnchor.constraintEqualToAnchor(btn.leadingAnchor).active = true
self.focusGuide.trailingAnchor.constraintEqualToAnchor(btn.trailingAnchor).active = true
You may also want to insert a focus guide to the right of the button and below the top collection view so that navigating down from the top row redirects focus to the button instead of to the bottom row.