Can't change UIView.isHidden property after action is called by UIBarButton - ios

I am using UIView's isHidden property to show/hide controls on a posting interface I'm developing. I use this to change the controls available when the keyboard is up/down, post types are selected, etc. I have written a method triggered by notifications that sets the view properties (mainly .isHidden) and animates changes, which is called when the keyboard appears, or when the user selects a post type which then changes the controls available to them. This usually works perfectly fine, and the properties change as intended, but when the method is triggered by a UIBarButton action, the view being set to visible becomes unresponsive. After hitting bar button I am unable to change the isHidden property of the view (even by explicitly setting isHidden to true or false... it does not change).
Here is the method that changes the view properties:
#objc func animateActionViewChange(_ sender: Notification) {
// DEBUG
print("\n[BEFORE]\nnormalActionView.isHidden: \(normalActionView.isHidden)\nkeyboardActionView.isHidden: \(keyboardActionView.isHidden)\nkeyboardUp: \(keyboardUp)\nactionViewActive: \(actionViewActive)")
// save prev state
let normalTemp: Bool = normalActionView.isHidden
let keyboardTemp: Bool = keyboardActionView.isHidden
// set new state based on env vars
normalActionView.isHidden = keyboardUp ? true : !actionViewActive
keyboardActionView.isHidden = keyboardUp ? !actionViewActive : true
// DEBUG
print("[AFTER]\nnormalActionView.isHidden: \(normalActionView.isHidden)\nkeyboardActionView.isHidden: \(keyboardActionView.isHidden)\nkeyboardUp: \(keyboardUp)\nactionViewActive: \(actionViewActive)")
// animate opacity changes
if normalActionView.isHidden != normalTemp {
let targetAlpha: CGFloat = normalTemp ? CGFloat(1) : CGFloat(0)
UIView.animate(withDuration: actionViewAnimationDuration / 2) {
self.normalActionView.alpha = targetAlpha
}
}
if keyboardActionView.isHidden != keyboardTemp {
let targetAlpha: CGFloat = keyboardTemp ? CGFloat(1) : CGFloat(0)
UIView.animate(withDuration: actionViewAnimationDuration / 2) {
self.keyboardActionView.alpha = targetAlpha
}
}
}
and the action called by the UIBarButton (this issue concerns when the button title is 'back'):
// back/cancel button action
#IBAction func cancel(sender: UIBarButtonItem) {
if sender.title == "Cancel" {
// cancel ongoing request if there is one
if let request = ongoingRequest {
if !request.isFinished {
request.cancel()
}
}
self.performSegue(withIdentifier: "cancel_unwindFromNewPostView", sender: self)
} else {
// reset post type to default (general)
postType = .general
// set actionViewActive bool to set visibility going forwards
actionViewActive = true
// hide datePicker
self.datePickerView.isHidden = true
actionViewAnimationDuration = 0.35
NotificationCenter.default.post(name: NSNotification.Name("animateActionViewChange"), object: nil)
UIView.animate(withDuration: 0.35) {
// layout
self.view.layoutIfNeeded()
}
// revert button text to 'cancel'
cancelButton.title = "Cancel"
}
}
Here is the output of the debugging flag before the 'back' button is hit:
[BEFORE]
normalActionView.isHidden: false
keyboardActionView.isHidden: true
keyboardUp: true
actionViewActive: true
[AFTER]
normalActionView.isHidden: true
keyboardActionView.isHidden: false
keyboardUp: true
actionViewActive: true
after:
[BEFORE]
normalActionView.isHidden: true
keyboardActionView.isHidden: true
keyboardUp: false
actionViewActive: true
[AFTER]
normalActionView.isHidden: true
keyboardActionView.isHidden: true
keyboardUp: false
actionViewActive: true
As you can see above, the isHidden property of the normal action view is not changing, even though it is being set to 'false'. I originally thought this was because a reference was being lost somewhere (though I thought that would result in a nil reference, which I don't have), so I made the references to normalActionView and keyboardActionView strong. This did not fix the problem, obviously, so I put the changes to isHidden in a method called by a notification to ensure it was always on the same thread (which it is, I checked by printing the current thread, it is always main), but that did not help either.

isHidden behaves as though it is cumulative. For example, if you set it to true twice in a row, you would need to set it to false twice.
I usually use a check for the opposite value before setting a value.
if view.isHidden == false {
view.isHidden = true
}

Why did you bind uiview animation to keyboard notifications ? Is that a required part of your application UI ? I think you can animate your view after a button pressed. Because it's highly difficult to catch the exact keyboard animation speed together working with any view. What I mean is that keyboard closes fast while notification center tries to catch and animate view.
Thanks.

Related

VC.isHidden = true not working as expected

I have a Viewcontroller ThirdViewControllerPassenger which has multiple subviews on it, including a UICollectionView called collectionViewwith horizontally scrolling Cards. So far, so good. I have written code to be executed from a tap action from inside the uicollectionviewcells. Tapping the action does work and prints to console. However, by pressing one of these cards I want to hide the whole UICollectionView. I have set up an onTap Function as shown here:
#objc func onTap(_ gesture: UIGestureRecognizer) {
if (gesture.state == .ended) {
/* action */
if favCoordinate.latitude == 1.0 && favCoordinate.longitude == 1.0 {
//There has been an error OR the User has pressed the new Address button
//do
}else{
ThirdViewControllerPassenger().collectionView.isHidden = true
if ThirdViewControllerPassenger().collectionView.isHidden == true {
print("done!")
}
}
}
}
As you can see, I have already been troubleshooting a bit. I have tested ThirdViewControllerPassenger().collectionView.isHidden = true from ThirdViewControllerPassenger directly, which worked. It does not work, however, from a cell. The "done!" print never gets printed to console, so the call never arrives. I wonder why or what I am doing wrong.
Don't mind the first if statement, that function is not written yet. That should not matter. I am guessing that the rest of my code would not lead to any more clues.
Every ThirdViewControllerPassenger() here
ThirdViewControllerPassenger().collectionView.isHidden = true
if ThirdViewControllerPassenger().collectionView.isHidden == true {
print("done!")
}
is a new instance not the real one , you need to get access to the real shown istance
delegate.collectionView.isHidden = true
if delegate.collectionView.isHidden == true {
print("done!")
}

How to update one UISwitch when using another UISwitch

Case: I have 2 UISwitches - Switch1 and Switch2.
Switch1 controls the visbility of a UILabel in my scene.
Switch2 once activated turns Switch1 off as well as its visbility.
Problem: After Switch2 is activated Switch1 turns off as well as disappears however my UILabel is still visible in my scene.
switch1.addTarget(self, action: #selector(switch1Action), for: UIControl.Event.valueChanged)
switch2.addTarget(self, action: #selector(switch2Action), for: UIControl.Event.valueChanged)
#objc func switch1Action(switch1: UISwitch) {
if switch1.isOn {
lockedMessage.isHidden = false
}
if !switch1.isOn {
lockedMessage.isHidden = true
}
}
#objc func switch2Action(switch2: UISwitch) {
if switch2.isOn {
switch1.isOn = false
switch1.isHidden = true
}
Many thanks! :)
If I understand your issue correctly, then it seems you want lockedMessage to be hidden if switch2 is on as well. If this is the case – you can change the visibility of the lockedMessage within your function switch2Action.
#objc func switch2Action(switch2: UISwitch) {
if switch2.isOn {
switch1.isOn = false
switch1.isHidden = true
lockedMessage.isHidden = true
}
This is correct and desired behavior. Since you explicitly changed the value, it is up to you to decide how to handle the changed value.
The reason for this is because it is not uncommon to explicitly change the value of control after being notified of its value being changed through user interaction. If the explicit state change caused the event to fire again, you would end up in an infinite loop. "#rmaddy"
Deprecated (since the original question updated):
You (probably accidentally) set a unrelated condition for hiding the label. (What is moveWall ?).
Try this instead:
#objc func switch1Action(switch1: UISwitch) {
lockedMessage.isHidden = !switch1.isOn
,,,
}
Setting switch1.isOn programmatically will not trigger the switch1Action. You need to hide the message label from switch2Action.
For reference see the documentation:
Setting the switch to either position does not result in an action message being sent.

Swift: Disappearing views from a stackView

I have a fairly simple set up in my main storyboard:
A stack view which includes three views
The first view has a fixed height and contains a segment controller
The other two views have no restrictions, the idea being that only one will be active at a time and thus fill the space available
I have code that will deal with the changing view active views as follows:
import Foundation
import UIKit
class ViewController : UIViewController {
#IBOutlet weak var stackView: UIStackView!
#IBOutlet weak var segmentController: UISegmentedControl!
#IBAction func SegmentClicked(_ sender: AnyObject) {
updateView(segment: sender.titleForSegment(at: sender.selectedSegmentIndex)!)
}
override func viewDidLoad() {
updateView(segment: "First")
}
func updateView(segment: String) {
UIView.animate(withDuration: 1) {
if(segment == "First") {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
self.stackView.arrangedSubviews[2].isHidden = false
}
print("Updating views")
print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
}
}
}
As you can see, when the tab called 'First' is selected, the subview at index 1 should show, whilst 2 is hidden, and when anything else is selected, the subview at index 2 should show, whilst 1 is hidden.
This appears to work at first, if I go slowly changing views, but if I go a bit quicker, the view at index 1 seems to remain permanently hidden after a few clicks, resulting in the view at index 0 covering the whole screen. I've placed an animation showing the issue and a screenshot of the storyboard below. The output shows that when the problem happens, both views remain hidden when clicking on the first segment.
Can anybody tell me why this is happening? Is this a bug, or am I not doing something I should be?
Many thanks in advance!
Update: I seem to be able to reliably reproduce the issue by going to the First > Second > Third > Second > First segments in that order.
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. If you show it three times, you need to hide it three times to actually hide it (assuming it was hidden to start).
This is independent of using animation.
So if you do something like this in your code, only hiding a view if it's visible, you'll avoid this problem:
if !myView.isHidden {
myView.isHidden = true
}
Building on the nice answer by Dave Batton, you can also add a UIView extension to make the call site a bit cleaner, IMO.
extension UIView {
var isHiddenInStackView: Bool {
get {
return isHidden
}
set {
if isHidden != newValue {
isHidden = newValue
}
}
}
}
Then you can call stackView.subviews[someIndex].isHiddenInStackView = false which is helpful if you have multiple views to manage within your stack view versus a bunch of if statements.
In the end, after trying all the suggestions here I still couldn't work out why it was behaving like this so I got in touch with Apple who asked me to file a bug report. I did however find a work around, by unhiding both views first, which solved my problem:
func updateView(segment: String) {
UIView.animate(withDuration: 1) {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = false
if(segment == "First") {
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
}
}
}
Based on what I can see, this weird behavior is caused by the animation duration. As you can see, it takes one second for the animation to complete, but if you start switching the segmentControl faster than that, then I would argue that is what is causing this behavior.
What you should do is deactivate the user interactivity when the method is called, and then re-enable it once the animation is complete.
It should look something like this:
func updateView(segment: String) {
segmentControl.userInteractionEnabled = false
UIView.animateWithDuration(1.0, animations: {
if(segment == "First") {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
self.stackView.arrangedSubviews[2].isHidden = false
}
print("Updating views")
print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
}, completion: {(finished: Bool) in
segmentControl.userInteractionEnabled = true
}
}
While this will prevent from fast switching (which you may see as a downside), the only other way I am aware of that solve this is by removing the animations altogether.
Check the configuration and autolayout constraints on the stack view and the subviews, particularly the segmented control.
The segmented control complicates the setup for the stack view, so I'd take the segmented control out of the stack view and set its constraints relative to the main view.
With the segmented control out of the stack view, it's relatively straightforward to set up the stack view so that your code will work properly.
Reset the constraints on the stack view so that it is positioned below the segmented control and covers the rest of the superview. In the Attributes Inspector, set Alignment to Fill, Distribution to Fill Equally, and Content Mode to Scale to Fill.
Remove the constraints on the subviews and set their Content Mode to Scale to Fill.
Adjust the indexing on arrangedSubviews in your code and it should work automagically.

iOS: Why does hidden button still receive tap events?

According to the Apple docs, hidden UIButtons should not receive tap events.
However, our app has a UIButton receiving tap events despite being hidden.
This function is the IB Action invoked when the button is tapped. When the button is removed from Storyboard, this function doesn't get invoked. When the button is added to Storyboard, the function gets invoked -- even though the button is hidden.
To verify that the button is hidden, we put a breakpoint inside the function and ran expr sender.hidden from the Xcode debugger. The result: true.
The stack trace shows the IB Action is triggered by code in UIApplicationMain, not our code.
Through the Connections Inspector, we confirmed there is no other trigger for the IB Action except the mysterious button.
Thoroughly confused. Suggestions?
#IBAction func buttonTapped(sender: UIButton) {
// If here, handle tap
...
}
Try to set enable = false like this:
button.enabled = false
For Swift 3 would be:
button.isEnabled = false
The problem was an incomplete UIButton extension that didn't account for visibility in determining hit tests.
This function correctly handles the case where UIButtons are hidden.
extension UIButton {
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
// Ignore if button hidden
if self.hidden {
return nil
}
// If here, button visible so expand hit area
let hitSize = CGFloat(56.0)
let buttonSize = self.frame.size
let widthToAdd = (hitSize - buttonSize.width > 0) ? hitSize - buttonSize.width : 0
let heightToAdd = (hitSize - buttonSize.height > 0) ? hitSize - buttonSize.height : 0
let largerFrame = CGRect(x: 0-(widthToAdd/2), y: 0-(heightToAdd/2), width: buttonSize.width+widthToAdd, height: buttonSize.height+heightToAdd)
return (CGRectContainsPoint(largerFrame, point)) ? self : nil
}
}
A situation I encountered was that I was toggling between two different buttons based on application state. When I created the second of these two buttons, I copied and pasted the first. This also copied the first button's outlets.
I thought that the first button was being pressed when only the second button was showing, but in reality the second button was sending events to both the outlet I intended AND the one that was set when I copied the first button.
To determine if this is the case in your situation, go to the interface builder, select your button, and check that the touch events are set up exactly how you want in the connections inspector. If any of the connections are wrong, you can remove them by clicking the little 'x'.

Touch Up Inside not working properly

I have an app with some buttons, when those buttons are pressed the image on them should change. I assume that the TouchUpInside runs when you tap and remove the finger while still holding inside the area of the element, however it only works rarely and I'm not sure why.
The reason I use TouchUpInside instead of TouchDown is because I want the user to be able to cancel the action.
I'm sorry if I've misunderstood anything about those events and if this has already been asked. I couldn't find an answer to my problem searching the web.
//The IBAction is set to trigger on TouchUpInside
#IBAction func action11(sender: UIButton) {
setTile(sender)
}
func setTile(sender: UIButton) {
if turn {
print("O's turn")
sender.setImage(xTile, forState: .Normal)
turn = false
}
}
EDIT: Added the necessary code
There are some properties of UIButtons which you can use to achieve what you want.
You can use Default and selected state of uibutton to set two different images.
In XIB select state "Default" and assign default image to that state again select state to "Selected" and assign image which you want after button section.
and add following line in button selection method.
-(IBAction)buttonTapped:(UIButton *)sender{
sender.selected = !sender.selected;
}
Your understanding is correct, you need to use touchUpInside.
I assume you are trying to create a button that has a toggle function. On one touch you want the button to have the value Say "X" and when touched again the button has a value "O".
Take a look at this code below, this should do the job.
class ViewController: UIViewController {
var isButtonPressed = false{
// Adding a Property Observer, that reacts to changes in button state
didSet{
if isButtonPressed{
// Set the Value to X.
}else{
// Set the Value to O.
}
}
}
#IBAction func changeButtonValue(sender: UIButton) {
// Toggle the button value.
isButtonPressed = !isButtonPressed
}
}
If you don't set turn=true after the first time, this code is executed it will be executed only one.
if turn {
print("O's turn")
sender.setImage(xTile, forState: .Normal)
turn = false
}
Check if the button frame is large enough to get finger touch.
Apple says at least 35x35 pixel.

Resources