How to disable main UI when overlay appears? - ios

I've built a streaming audio app that looks like this:
When the stream is lost, I overlay this screen:
My issue is that with Voiceover ON, all the underlying controls are still active: I can swipe to them all, and adjust their value.
Here is a snippet of my code for when the signal is lost and regained:
#objc func lostStream() {
DispatchQueue.main.async {
self.lossOfSignalBlocker.alpha = 0.0
self.lossOfSignalBlocker.frame = self.view.bounds
self.view.addSubview(self.lossOfSignalBlocker)
self.lossOfSignalBlocker.isUserInteractionEnabled = true
//UIView.animate(withDuration: 0.2) { self.lossOfSignalBlocker.alpha = 1.0 }
UIView.animate( withDuration: 0.2, animations: { self.lossOfSignalBlocker.alpha = 1.0 } )
//Announce loss of signal to Voiceover user.
UIAccessibilityPostNotification(
UIAccessibilityAnnouncementNotification,
"Signal Lost" as NSString
)
}
}
#objc func regainedStream() {
DispatchQueue.main.async {
UIView.animate( withDuration: 0.2, animations: { self.lossOfSignalBlocker.alpha = 0.0 } )
{ _ in
self.lossOfSignalBlocker.removeFromSuperview()
}
}
}
How do I disable the main UI so that only the overlay responds to any Voiceover-related actions?

On UIAccessibility elements (eg. a view), there is a property called accessibilityElementsHidden. Setting this value to true should hide views that are covered by the arrival of a the "reconnecting" view.
Considering setting the this value to true on the UITableView when in the background.
For more details check out the documentation: https://developer.apple.com/documentation/objectivec/nsobject/1615080-accessibilityelementshidden

OK! I've made it work. In addition to setting 'accessibilityElementsHidden' to TRUE, you have to tell the app that the screen has changed by calling 'UIAccessibilityPostNotification' with the 'UIAccessibilityScreenChangedNotification' notification.
Here's what that code looks like now:
#objc func lostStream() {
DispatchQueue.main.async {
self.lossOfSignalBlocker.alpha = 0.0
self.lossOfSignalBlocker.frame = self.view.bounds
self.view.addSubview(self.lossOfSignalBlocker)
self.lossOfSignalBlocker.isUserInteractionEnabled = true
UIView.animate( withDuration: 0.1, animations: { self.lossOfSignalBlocker.alpha = 1.0 } )
//Disable Voiceover accessibility controls in main view
self.tableView.accessibilityElementsHidden = true
//Notify app the screen has changed.
UIAccessibilityPostNotification(
UIAccessibilityScreenChangedNotification,
nil
)
//Announce loss of signal to Voiceover user.
UIAccessibilityPostNotification(
UIAccessibilityAnnouncementNotification,
"Signal Lost. Reconnecting." as NSString
)
}
}
#objc func regainedStream() {
DispatchQueue.main.async {
UIView.animate( withDuration: 0.2, animations: { self.lossOfSignalBlocker.alpha = 0.0 } )
{ _ in
self.lossOfSignalBlocker.removeFromSuperview()
}
//Re-enable Voiceover accessibility controls in main view
self.tableView.accessibilityElementsHidden = false
//Notify app the screen has changed.
UIAccessibilityPostNotification(
UIAccessibilityScreenChangedNotification,
nil
)
//Announce signal regained to Voiceover user.
UIAccessibilityPostNotification(
UIAccessibilityAnnouncementNotification,
"Reconnected." as NSString
)
}
}

Related

Tap a UIView to full screen and tap again back to small size

I have a view which contains 6 UIView vPreviews and each of them plays real-time stream video.
The target is:
When tap one of each UIView, it should become full screen.
Tap again, back to origin small screen layout
Code as below:
custom tap gesture with value
class FullScreenTapGesture: UITapGestureRecognizer{
var isFullScreen: Bool = false
var index: Int?
}
Add tap gesture recognizer in viewDidLoad()
let gesture = FullScreenTapGesture(target: self, action: #selector(fullScreenTap))
vPreviews[0].addGestureRecognizer(gesture)
gesture.index = 0
fullScreenTap implementation
#objc func fullScreenTap(sender: FullScreenTapGesture){
if sender.isFullScreen{
print("setsmall")
setSmallScreenLayoutConstrains()
// sender.isFullScreen = false
}else{
print("setfull")
setFullScreenLayoutConstrains(index: sender.index!)
// sender.isFullScreen = true
}
// Animate to full screen
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.vc.view.layoutIfNeeded()
}) { (finished) in
// ...
}
}
func setSmallScreenLayoutConstrains(){
vPreviews[0].pin.top().left().width(previewWidth).height(previewHeight).margin(padding,padding,0,0)
vPreviews[1].pin.top().after(of: vPreviews[0]).width(previewWidth).height(previewHeight).margin(padding,padding,0,0)
vPreviews[2].pin.top().after(of: vPreviews[1]).width(previewWidth).height(previewHeight).margin(padding,padding,0,0)
vPreviews[3].pin.below(of: vPreviews[0]).left().width(previewWidth).height(previewHeight).margin(padding,padding,0,0)
vPreviews[4].pin.below(of: vPreviews[0]).after(of: vPreviews[3]).width(previewWidth).height(previewHeight).margin(padding,padding,0,0)
vPreviews[5].pin.below(of: vPreviews[0]).after(of: vPreviews[4]).width(previewWidth).height(previewHeight).margin(padding,padding,0,0)
}
func setFullScreenLayoutConstrains(index: Int){
self.vPreviews[index].pin.top().right().left().bottom().margin(3)
}
However, current result is like this: when tap is detected, full screen but automatically and immediately back to small screen.
Any suggestion would be greatly appreciated!

Repeat Fade In / Fade Out Between Two Different Texts On Same UILabel

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.

Cannot tap UIButton after having set alpha to 0.0, even when reset to 1.0

I have this simple code:
func tappedButton() {
self.button.alpha = 1.0
UIView.animate(withDuration: 1.0, delay: 4.0, options: .curveLinear, animations: {
self.button.alpha = 0.0
}) { _ in }
}
This function aims at showing a button again for 4 seconds before hiding it (with a 1 second animation). However, while the button is completely visible for these 4 seconds, tapping it doesn't work anymore.
Thanks for your help.
As per the documentation in for the method hittest(_:with:) of UIView https://developer.apple.com/documentation/uikit/uiview/1622469-hittest
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.
This means that any view, particularly a button, with alpha 0.0 would not be touched.
However, the problem here is that the button is still visible, at least for you. This odd behavior occurs because the actual alpha value of the button is already setted to 0.0 when the animations starts. Animations work by changing the visual hierachy and transition the difference with the parameters you give to the function. In your case, you have two states: a view with a visible button visible and another view without the button. Only the visual part is animated but the corresponding values are already setted. A solution would be:
func tappedButton() {
self.button.alpha = 1.0
UIView.animate(withDuration: 1.0, delay: 4.0, options: .curveLinear, animations: { [weak self] in
self?.button.alpha = 0.01
}) { [weak self] _ in self?.button.alpha = 0.0 }
}
EDIT: This solution seems like a hack but works. I use this approach because the completion handler is always called with a true value.
func tapped() {
let duration = 1.0
let delay = 2.0
let delayDuration = delay + duration
UIView.animate(withDuration: duration, delay: delay, options: [.curveLinear, .allowUserInteraction], animations: { [weak self] in
self?.saveButton.alpha = 0.1
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayDuration, execute: { [weak self] in
self?.saveButton.alpha = 0.0
})
}
You need to use allUserInteraction in the options and also check for touches. The animation is added immediately and although you see the button to the system it is already hidden. What does this mean? It means you are watching a movie. But at least with userInteraction enabled you can check for touch events. This is great but how do we know the button is really showing or not? Well you have to use two different checks most likely. One that checks the true UIView alpha of the button and one check that checks the opacity on the presentation layer. I have never fully looked at the link between UIView animations and Core Animation except that I think UIView animations are a wrapper for Core Animations. UIView animations definitely update the view model immediately. So an alpha animation is most likely interpreted into an opacity animation on the layer. Armed with this we can check the opacity of the presentation layer on touches and see that the button is being clicked even if the view model thinks the alpha is 0. This check on the presentation layer will work as long as the opacity is above 0. So here you go.
import UIKit
class ViewController: UIViewController {
lazy var testButton : UIButton = {
let v = UIButton(frame: CGRect(x: 20, y: 50, width: self.view.bounds.width - 40, height: 50))
v.backgroundColor = UIColor.blue
v.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(testButton)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 1.0, delay: 4.0, options: .allowUserInteraction, animations: {
self.testButton.alpha = 0
}, completion: nil)
//test the layer
//test the layer for opacity
if let presentation = testButton.layer.presentation()?.animation(forKey: "opacity"){
print("the animation is already added so normal clicks won't work")
}
}
#objc func buttonClicked(){
print("clicked")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first{
let location = touch.location(in: self.view)
if self.testButton.frame.contains(location){
//but what you might not know is the animation is probably already running
//and so the check above misses this
if let buttonPres = testButton.layer.presentation(),
let _ = buttonPres.animation(forKey: "opacity"),
let opacity = buttonPres.value(forKey: "opacity") as? CGFloat{
if opacity > 0{
buttonClicked()
}
}
}
}
}
}

UIView Animate only animates if it's in a IBAction. How do I get it to animate in my situation?

func pickWash() {
bottomChange = self.mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -350.0)
bottomChange.isActive = true
UIView.animate(withDuration: 10.0, animations: {
self.view.layoutIfNeeded()
self.waterlessLabel.isHidden = false
self.exteriorInterior.isHidden = false
self.exteriorOnly.isHidden = false
self.info.isHidden = false
self.blackLine.isHidden = false
self.extIntPrice.isHidden = false
self.extPrice.isHidden = false
self.confirmWash.isHidden = false
self.when.isHidden = false
self.timeChoice.isHidden = false
}, completion: nil)
}
func tester(){
self.pickWash()
}
In actuality, my tester method in my code is using Google's Place autocomplete iOS, but I did not want to flood my code with useless autocomplete code. Thus, when the user is done entering their location in Google's autocomplete, the function pickwash() is called and the animate does not work. It only worked for me when I had it in an IBAction with a button. Any idea?
pickWash() is being on a thread other than the main one (as confirmed in OP comments) and since the main thread is the only one allowed to work on the UI the behaviour is undefined (here nothing happens). You have to move your code execution to the main thread using
func pickWash() {
// Code here is on a non-main thread
DispatchQueue.main.async {
// Code here is executed on the main thread
bottomChange = self.mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -350.0)
bottomChange.isActive = true
UIView.animate(withDuration: 10.0, animations: {
self.view.layoutIfNeeded()
self.waterlessLabel.isHidden = false
self.exteriorInterior.isHidden = false
self.exteriorOnly.isHidden = false
self.info.isHidden = false
self.blackLine.isHidden = false
self.extIntPrice.isHidden = false
self.extPrice.isHidden = false
self.confirmWash.isHidden = false
self.when.isHidden = false
self.timeChoice.isHidden = false
}, completion: nil)
}
}
func tester(){
self.pickWash()
}
isHidden can't be animated use alpha, see the below code.
and I noticed that you are setting the time to 10.0 and 10 is so long.
func pickWash() {
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
self.waterlessLabel.alpha = 1
self.exteriorInterior.alpha = 1
self.exteriorOnly.alpha = 1
self.info.alpha = 1
self.blackLine.alpha = 1
self.extIntPrice.alpha = 1
self.extPrice.alpha = 1
self.confirmWash.alpha = 1
self.when.alpha = 1
self.timeChoice.alpha = 1
}, completion: nil)
}
func tester(){
self.pickWash()
}
If you want to animate a constraint, all you have to do is update the constant property. Here's some sample code:
#IBOutlet private weak var mapViewBottomConstraint; // If you are not using InterfaceBuilder, then hold a reference to the bottom constraint when you add it to your view.
func pickWash() -> Void {
self.mapViewBottomConstraint.constant = -350.0; // or whatever is appropriate here.
UIView.animate(withDuration: 1.0, animations: { [weak self] in
self?.view.layoutIfNeeded()
// do your stuff
}, completion: nil)
}

UIView.animateWithDuration Not Animating Swift (again)

Note: I’ve already checked the following stack overflow issues:
27907570, 32229252, 26118141, 31604300
All I am trying to do is fade animate in a view (by alpha) when called by an IBAction attached to a button. Then reverse when a button on the view is hit.
My wrinkle may be that I'm using a secondary view that is on the ViewDock in the storyboard View. The view is added to the subview at the time of viewDidLoad where the frame/bounds are set to the same as the superview (for a full layover)
The reason this is done as an overlay view since it is a tutorial indicator.
The result (like many others who've listed this problem) is that the view (and contained controls) simply appears instantly and disappears as instantly. No fade.
I have tried animationWithDuration with delay, with and without completion, with transition, and even started with the old UIView.beginAnimations.
Nothing is working. Suggestions warmly welcomed.
The code is about as straight forward as I can make it:
Edit: Expanded the code to everything relevant
Edit2: TL;DR Everything works with the exception of UIViewAnimateWithDuration which seems to ignore the block and duration and just run the code inline as an immediate UI change. Solving this gets the bounty
#IBOutlet var infoDetailView: UIView! // Connected to the view in the SceneDock
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
view.addSubview(infoDetailView)
updateInfoViewRect(infoDetailView.superview!.bounds.size)
}
func updateInfoViewRect(size:CGSize) {
let viewRect = CGRect(origin: CGPointZero, size: size)
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
updateInfoViewRect(size)
}
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.0
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.75
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
// MARK: - IBActions
#IBAction func openInfoView(sender: UIButton) {
showInfoView()
}
#IBAction func closeInfoView(sender: UIButton) {
hideInfoView()
}
Please note, I started with the following:
func showInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.75
})
}
func hideInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.00
})
}
If you infoDetailView is under auto layout constraints you need to call layoutIfNeeded on the parent view inside animateWithDuration:
func showInfoView() {
self.view.layoutIfNeeded() // call it also here to finish pending layout operations
UIView.animate(withDuration: 2.0, animations: {
self.infoDetailView.alpha = 0.75
self.view.layoutIfNeeded()
})
}
Theoretically this should not be needed if you just change the .alpha value, but maybe this could be the problem in this case.
There are several strange things I can see,
first, remove:
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
Usually you don't need to call those methods manually unless you know exactly what you are doing.
Also, when you are changing the size:
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
You never need to set both bounds and frame. Just set frame.
Also, you should probably make sure that the view actually doesn't ignore the frame by setting:
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
Instead of resetting the frame, just set autoresize mask:
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
Resulting in:
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
infoDetailView.frame = view.bounds
view.addSubview(infoDetailView)
}
func hideInfoView() {
...
}
I think this should actually help because immediate animations are often connected to size problems.
If the problem persists, you should check whether the infoDetailView in your animation is the same object as the infoDetailView you are adding to the controller.
For others looking to start an animation immediately when a view loads...
The animation won't work if you call UIView.animate(...) inside viewDidLoad. Instead it must be called from the viewDidAppear function.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3) {
self.otherView.frame.origin.x += 500
}
}
If the animation does not seem to execute then consider examining the state of each of your views, before you enter the animation block. For example, if the alpha is already set to 0.4 then the animation that adjusts your view alpha, will complete almost instantly, with no apparent effect.
Consider using a keyframe animation instead. This is what a shake animation in objective c looks like.
+(CAKeyframeAnimation*)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-10.0, 0.0, 0.0)],
[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(10.0, 0.0, 0.0)]];
animation.autoreverses = YES;
animation.repeatCount = 2;
animation.duration = 0.07;
return animation;
}
Here is a post that shows you how to adjust alpha with keyframes https://stackoverflow.com/a/18658081/1951992
Make sure infoDetailView's opaque is false.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/opaque
This property provides a hint to the drawing system as to how it should treat the view. If set to true, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to false, the drawing system composites the view normally with other content. The default value of this property is true.
Try Below code. Just play with alpha and duration time to perfect it.
Hide func
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.8
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.4
},
completion:
{ (finished) in
self.infoDetailView.alpha = 0.0
}
)
}
)
AFLog.exit(thisClass)
}
Show func
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.3
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.7
},
completion:
{ (finished) in
self.infoDetailView.alpha = 1.0
}
)
}
)
AFLog.exit(thisClass)
}
I've replicated your code and it work well, it's all ok.
Probably you must control constraints, IBOutlet and IBActions connections. Try to isolate this code into a new project if it's necessary.
Update: my code
and my storyboard and project folder photo:
Every object (view and buttons) are with default settings.
I've commented all AFLog lines (probably it's only any more "verbose mode" to help you) , the rest of your code is ok and it do what do you aspected from it, if you press open button the view fade in, and when you tap close button the view fade out.
PS Not relevant but i'm using xCode 7.3 , a new swift 2.2 project.
Use this code:
Swift 2
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Have you tried changing your showInfoView() to something more like toggleInfoView?
func toggleInfoView() {
let alpha = CGFloat(infoDetailView.alpha == 0 ? 1 : 0)
infoDetailView.alpha = alpha //this is where the toggle happens
}
It says that if your view's alpha is 0, then change it to 1. Else, make it 0.
If you need that to happen in an animation, try
#IBAction func openInfoView(sender: UIButton) {
UIView.animate(withDuration: 2.0, animations: {
self.toggleInfoView() //fade in/out infoDetailView when animating
})
}
You'll still want to keep that infoDetailView.alpha = 0.0 where you have it, coming from the viewDidLoad.
For UILabel component try to changes layer's background color instead.
Try this (Tested on Swift 4):
UIView.animate(withDuration: 0.2, animations: {
self.dateLabel.layer.backgroundColor = UIColor.red.cgColor;
})
Had a similar issue with animation not being performed.
Changed the function call use perform(aSelector: Selector, with: Any?, afterDelay: TimeInterval) in the form of perform(#selector(functionThatDoesAnimationOfAlphaValue), with: nil, afterDelay: 0) and it worked. Even with a TimeInterval set to 0.
In case someone else comes here wondering for a solution.

Resources