Replacing a View with a UIActivityIndicator Programatically - ios

I have been trying to create a quick method that will allow me to replace any UIView with a spinner right before a process starts and do the re-show the view once my process is done. For some reason, the UIView does disappear but the spinner never shows. These are the methods in question:
func showLoader(view: UIView, controller: UIViewController) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.color = AC_BLUE
spinner.center = view.center
spinner.startAnimating()
view.alpha = 0
controller.view.addSubview(spinner)
return spinner
}
func hideLoader(view: UIView, spinner: UIActivityIndicatorView) {
view.alpha = 1
spinner.removeFromSuperview()
}
..which I call with something like this:
let spinner = Extensions().showLoader(view: signInBtn, controller: self)
APICalls().getUser(email: usernameTextField.text!, pass: passwordTextField.text!) { success, user in
//..CODE..//
Extensions().hideLoader(view: self.signInBtn, spinner: spinner)
}
Also, I tried centering on the main VC view, and that does work. So I'm guessing it must be related to the view's position.
Thanks!

Try setting this before adding the spinner to the controllers view (instead of the old spinner.center = view.center):
spinner.center = view.superview!.convert(view.center, to: controller.view)
You need to convert the view's center to the coordinates of the controller.view.

Related

After running a view controller custom transition back and forward the view controller stops responding (but the app doesn't crash entirely)

I have two view controllers and from the first one I show the second one by a modal segue, which presentation style is set to over current context. I also use blur effect which appears during the transition and disappears after it.
I've created a demo app to show how the transition looks like:
Here, the second view controller contains a UIScrollView, and on top of that, a yellow rectangle, which is another UIView with a UIButton on it. That UIButton also closes the view controller. Also, as you can see, I've set the background color to clear, so the blur effect is visible.
Now, in my project the transition is basically the same, except, my view controllers are "heavier".
The first view controller is embedded inside a UINavigationController" and a UITabBarController and on it I have a custom segmented control which
is made from UIViews and UIButtons, and a UITableView with custom cells.
The second view controller consists of a UIScrollView and a UIView (like on the image above, just bigger a little). That view contains a UIImageView, UILabels and a smaller UIView which is used as button to close the view by tapping on it.
Here is the code that I use to close the second view controller by dragging it down (UIScrollViewDelegate's methods).
extension AuthorInfoViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard isDragging else {
return
}
if !isDismissingAuthorInfo && scrollView.contentOffset.y < -30.0 && dismissAnimator != nil {
authoInfoViewControllerDelegate?.authorInfoViewControllerWillDismiss()
isDismissingAuthorInfo = true
dismissAnimator?.wantsInteractiveStart = true
dismiss(animated: true, completion: nil)
return
}
if isDismissingAuthorInfo {
let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
dismissAnimator?.update(progress)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
if progress > 0.5 {
dismissAnimator?.finish()
} else {
dismissAnimator?.cancel()
print("Canceled")
}
isDismissingAuthorInfo = false
isDragging = false
}
}
Here, isDismissingAuthorInfo and isDragging are booleans which keep
track whether the view is dismissing and is dragged at all. authorInfoViewControllerWillDismiss is a method implemented in a protocol to which conforms the first view controller. That methods calls another method which adds the blur animation to the custom transitions animator.
EDITED
The animator code is the following:
class DismissAnimator: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {
var auxAnimationsForBlur: (()->Void)?
var auxAnimationsForTabBar: (()->Void)?
var auxAnimationsCancelForBlur: (()->Void)?
var auxAnimationsCancelForTabBar: (()->Void)?
var tabBar: UITabBar?
var blurView: UIView?
let transitionDuration = 0.75
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return transitionDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
transitionAnimator(using: transitionContext).startAnimation()
}
func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
let from = transitionContext.view(forKey: .from)!
container.addSubview(from)
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)
animator.addAnimations({
from.transform = CGAffineTransform(translationX: 0.0, y: container.frame.size.height + 30)
}, delayFactor: 0.15)
animator.addCompletion { (position) in
switch position {
case .end:
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.tabBar?.isHidden = false
self.blurView?.removeFromSuperview()
default:
transitionContext.completeTransition(false)
self.auxAnimationsCancelForBlur?()
self.auxAnimationsCancelForTabBar?()
}
}
if let auxAnimationsForBlur = auxAnimationsForBlur {
animator.addAnimations(auxAnimationsForBlur)
}
if let auxAnimationsForTabBar = auxAnimationsForTabBar {
animator.addAnimations(auxAnimationsForTabBar)
}
return animator
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
return transitionAnimator(using:transitionContext)
}
}
In the code above, the auxAnimationsForBlur is for adding blur animation to the animator, the auxAnimationsCancelForBlur is for canceling it, auxAnimationsForTabBar is for adding the animation of alpha value of tabBar, auxAnimationsCancelForTabBar is for canceling it.
Now, the problem is the following: the animation works fine, but after running it by dragging the second view controller for several times (5-9, approximately), after the transition is over and the first view controller is shown, it just stops responding. However, on the bottom I have a tab bar, and it works. So, when I change to another tab and return back, I see the second view controller and a black screen behind it (where the first view controller should have been). When this happens the cancel method on UIPercentDrivenInteractiveTransition gets called, from scrollViewWillEndDragging method above (on the console I see the word cancel printed). Is it possible that cancel method on UIPercentDrivenInteractiveTransition doesn't causes this problem, because when I comment out the call to that method, it seems everything works ok (in that case, I also comment out the call to interruptibleAnimator(using:) method on my animator, so I lose the interactive behaviour of the transition)? I couldn't reproduce this behaviour while closing the second view controller by tapping on a close button, so I think it has something to do with dragging.
What could cause this problem, and what could you suggest for solving it? I would appreciate all your help.

UITextView takes a while to load the text into it, how to show loading icon?

I have a textview with text in it. It takes a while to load the text, so how can I show a loading icon while it loads the text to it, and when it's done placing the text in the textview then delete the loading icon?
Hope to hear from you guys
Tested code:-
//show progress activity indicator inside UITextView
extension UITextView {
func loadingIndicator(show: Bool) {
if show {
let indicator = UIActivityIndicatorView()
self.isEditable = false
indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.whiteLarge
indicator.color = UIColor.red
let viewHeight = self.bounds.size.height
let viewWidth = self.bounds.size.width
indicator.center = CGPoint(x: viewWidth/2, y: viewHeight/2)
self.addSubview(indicator)
indicator.startAnimating()
} else {
for view in self.subviews {
if let indicator = view as? UIActivityIndicatorView {
indicator.stopAnimating()
indicator.removeFromSuperview()
self.isEditable = true
}
}
}
}
}
Usage:-
tvTextView.loadingIndicator(show: true/false)
Use UIActivityIndicator :
Add UIActivityIndicator on View
Set its activityIndicator.isHidden = true
Then use activityIndeicator.hidesWhenStopped = true
Set its activityIndicator.isHidden = false
And when you assigning text to TextView then use activityIndicator.startAnimating()
And when text is loaded to TextView then use
activityIndicator.stopAnimating()
Hope it helps you
This is one of those simple sounding things that is actually a little tricky.
First the simple part is to add a UIActivityIndicatorView and set it up appropriately (add to the view, set to hidden initially, the hidesWhenStopped property to true, etc).
Now you would think you could just do something like this:
self.activityIndictor.startAnimating()
self.textView.text = self.data
self.activityIndicator.stopAnimating()
but that won't work because execution waits when setting the text view and doesn't update the UI so you don't see the activity indicator.
What does work is this:
self.activityIndicator.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.textView.text = self.data
self.activityIndicator.stopAnimating()
}
What that does is to start the activity indicator animating and then dispatch the actual setting of the text view back onto the main thread which gives the UI a chance to update and you see the activity indicator.
Note you have to use asyncAfter with a small time delay instead of just a straight async call because that doesn't give the UI time to update.

UIActivityIndicatorView not disappearing when back page in Swift

My activity indicator seems not disappear after I go back page. For the first time, it's disappear. Then I go to second view controller then go back to the first view controller. The activity indicator supposedly not to showing up since all UIViews already appeared.
My code
var activityView : UIActivityIndicatorView = UIActivityIndicatorView(frame: CGRectMake(0,0, 50, 50)) as UIActivityIndicatorView
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(.POST , "192.168.0.1/test", parameters: [])
.responseJSON { response in
if response.result.isSuccess {
var jsonObj = JSON(response.result.value!)
print(jsonObj)
self.activityView.stopAnimating()
}
}
}
override func viewDidAppear(animated: Bool) {
activityView.center = self.view.center
activityView.hidesWhenStopped = true
activityView.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray
view.addSubview(activityView)
activityView.startAnimating()
}
You are showing your activityIndicator in viewDidAppear. That is why it appears when you go back from second to first. Also have a look at view life cycle. viewDidLoad is only called once whenever view controller is initialized, but viewDidAppear will be called when ever the view controller is presented. (either back navigation or presented again)
So its better to add views in viewDidLoad and activity indicator should always be associated with its network call. Before starting the call show the activity indicator and hide or remove once its done.
Also, you have missed super calls. Be sure to always call super methods when overriding.
Ex:
override func viewDidLoad() {
super.viewDidLoad()
addActivityIndicator()
fetchRemoteData()
}
func addActivityIndicator() {
activityView.center = self.view.center
activityView.hidesWhenStopped = true
activityView.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray
view.addSubview(activityView)
}
func fetchRemoteData() {
activityView.startAnimating()
Alamofire.request(.POST , "192.168.0.1/test", parameters: [])
.responseJSON { response in
if response.result.isSuccess {
var jsonObj = JSON(response.result.value!)
print(jsonObj)
self.activityView.stopAnimating()
}
}
}
U want to hide and show your activity indicator when you want. And also make sure to set as bringSubviewToFront

Adding View Programmatically from ViewController super class doesn't show in front

I have a BaseViewController that my UIViewControllers extend so i can have explicit functions that i dont need to rewrite. Something i would like would be a functions such as self.showSpinner() and the viewController would show the spinner
My Code looks like this
class BaseViewController: UIViewController {
var actvIndicator : UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
self.actvIndicator = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
self.actvIndicator.color = UIColor.blackColor()
self.actvIndicator.backgroundColor = UIColor.blackColor()
self.actvIndicator.frame = CGRectMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2, 100, 100);
self.actvIndicator.center = self.view.center
self.actvIndicator .startAnimating()
self.view.addSubview(self.actvIndicator)
self.actvIndicator.bringSubviewToFront(self.view)
self.edgesForExtendedLayout = UIRectEdge.None
self.navigationController?.navigationBar.translucent = false
}
func showSpinner(){
self.actvIndicator.startAnimating()
}
func hideSpinner(){
self.actvIndicator.stopAnimating()
}
}
And my viewcontrollers looks like this
class MyProjectViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.showSpinner()
}
}
MyProjectViewController have UITableView that fills the entire screen. When i set tblProjects.alpha = 0 i can see the spinner. But i want it in the front.
i also tried self.view.bringSubviewToFront(self.actvIndicator)
What am i missing?
A couple quick notes before I get into what I think your problem is:
When you add a subview it is automatically added to the top layer, no need for the bringSubviewToFront: in viewDidLoad: (which is being used wrong anyway).
You should not set view frames in viewDidLoad: (e.g. centering a view). Frames are not setup yet, so you should move that to viewWillAppear: or some other variant.
Now your issue is most likely a view hierarchy problem (further confirmed by your comment) and thus can probably be fixed by pushing the spinner to the front every time you want it to be shown, like:
func showSpinner() {
self.view.bringSubviewToFront(self.actvIndicator)
self.actvIndicator.startAnimating()
}
The problem here stands on the fact that table view is draw after you are calling self.view.bringSubviewToFront(self.actvIndicator). A possible workaround for this is to call bringSubviewToFront when showing the spinner
func showSpinner(){
self.view.bringSubviewToFront(self.actvIndicator)
self.actvIndicator.startAnimating()
}

How to dim a parent UIView (50% transparent) for login?

I have a view with a Login button. When the button is clicked, I add a view with fields for login. When this happens, I need to dim the parent view. How I do that?
UIViews have a property named mask.
mask will always be on top of the UIView who owns it.
So, your approach should be something like this:
(This is for Swift, but it's easily converted to Obj-c)
self.view.mask = UIView(frame: self.frame)
self.view.mask?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
//Do your work here, block UI, anything you need to, and then...
self.view.mask = nil
Update
Removed Swift 2 refernce as it's not relevant anymore. Just as a curiosity, then the property was called maskView
Add a UIView over the parent view that is initially transparent with a background color of black. When you need to dim it, change the view's alpha to 0.5. This will be 50% transparent.
I would go for a view with white background:
whiteView=[[UIView alloc]initWithFrame:viewToDim.frame];
[whiteView setBackgroundColor:[UIColor whiteColor]];
[whiteView setAlpha:0.5f];
[self.view insertSubview:whiteView aboveSubview:viewToDim];
class UIDecorator: NSObject {
static let sharedInstance = UIDecorator()
private let dimView = UIView()
private let loadingView = MOOverWatchLoadingView(frame: CGRectMake(0, 0, 100, 100),
autoStartAnimation: true)
func showLoadingView() {
if let currentPage = UIApplication.topViewController(){
dimView.frame = currentPage.view.frame
dimView.backgroundColor = UIColor.blackColor()
dimView.alpha = 0.5
currentPage.view.addSubview(dimView)
currentPage.view.userInteractionEnabled = false
loadingView.center = currentPage.view.center
loadingView.backgroundColor = UIColor.clearColor()
currentPage.view.addSubview(loadingView)
}
}
func dismissLocadingView() {
if let currentPage = UIApplication.topViewController(){
currentPage.view.userInteractionEnabled = true
dimView.removeFromSuperview()
loadingView.removeFromSuperview()
}
}
}

Resources