UILabel not taking appropriate frame when reloading view - ios

So the main screen of my app has a hamburger button which can be used to navigate to other parts of the app. That being said, there's a chance someone might get notifications in other parts of the app, so I'm trying to add an indicator, which is just a subclass of UILabel which shows up over the hamburger button. When the view first loads, it looks like this, which is fine:
Okay, so when the user opens the navigation drawer I animate the hamburger button and remove the notification by hiding it (self.badge.hidden = true), and it goes away fine, like this:
Now the problem I'm facing is that when the view is animated back in, the notification badge ends up in a really weird place, even though no frames change, and if I print out the frames, it's exactly where it should be programmatically, but it actuality it ends up looking really weird, as just the tiny bubble in the top left corner:
All I'm doing to add it back is in my delegate method for when the navigation drawer closes, I try self.badge.hidden = false. So obviously there's something weird going on here.
But what's even weirder, is that if I navigate to another view, say I press one of the buttons in the navigation drawer, and then go back to the home view, then the hiding works fine, and when I unhide the notification badge then it appears exactly where it should be! As I said, this only happens if I navigate away from the home screen and then back to it, but when the app first loads and I go to the navigation drawer, then the notification badge gets put in the wrong place and is also very tiny. I've tried a lot of things to try to get this to work. Originally I was reinitializing the badge view before I added it back using the same frame I did when the view loaded, but it still ended up the way it looks now. I also tried to set it to nil and remove it from the superView instead of just hiding it, but all of the different things I've tried have resulted in the same thing: only on the home screen before navigating elsewhere, the badge doesn't end up in the right place after closing the navigation drawer. I can post more code or answer any additional questions you might have, but please help me I can't figure this one out!
Here is my initialization code in viewDidAppear:
badge = SwiftBadge(frame: CGRectMake(15, -5, 15, 15))
menuButton.addSubview(badge!)
menuButton.bringSubviewToFront(badge!)
And creating my menuButton (which I do do in viewDidLoad)
menuButton = UIButton(frame: CGRectMake(0, 0, 20, 20))
menuButton.setBackgroundImage(UIImage(named: "Side menu.png"), forState: .Normal)
let addBarItem = UIBarButtonItem(customView: addButton)
let menuButtonItem = UIBarButtonItem(customView: menuButton)
self.navigationItem.setLeftBarButtonItem(menuButtonItem, animated: true)
EDIT 2:
Code for rotating the hamburger button
let animationDuration = self.revealViewController().toggleAnimationDuration
UIView.animateWithDuration(animationDuration, animations: { () -> Void in
if !self.menuButtonRotated {
//self.badge?.removeFromSuperview()
self.badge?.hidden = true
self.menuButton.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
self.menuButtonRotated = true
print("Menu frame after rotation: \(self.menuButton.frame)")
} else {
self.menuButton.transform = CGAffineTransformMakeRotation(CGFloat(0))
self.menuButtonRotated = false
self.badge?.hidden = false
}
}, completion: { (bool) -> Void in
if !self.menuButtonRotated {
//self.badge = SwiftBadge(frame: CGRectMake(250, 250, 100, 100))
print("New menu frame: \(self.menuButton.frame)")
print("New badge frame: \(self.badge!.frame)")
//self.view.addSubview(self.badge!)
//self.badge?.hidden = false
}
})
Commented out code is some other things I've tried.

Well after much debugging and hair pulling, I finally figured out what the fix was. For whatever reason it seems that my menuButton frame wasn't set as soon as the animation ended, so I had to change my animation code to below, hopefully this will help someone who comes looking later:
UIView.animateWithDuration(animationDuration, animations: { () -> Void in
if !self.menuButtonRotated {
self.badge?.hidden = true
self.badge?.removeFromSuperview()
self.badge = nil
self.menuButton.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
self.menuButtonRotated = true
print("Menu frame after rotation: \(self.menuButton.frame)")
} else {
self.menuButton.transform = CGAffineTransformMakeRotation(CGFloat(0))
self.menuButtonRotated = false
}
}, completion: { (bool) -> Void in
if !self.menuButtonRotated {
print("New menu frame: \(self.menuButton.frame)")
print("New badge frame: \(self.badge?.frame)")
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.05 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
self.badge = SwiftBadge(frame: CGRectMake(13, -7, 15, 15))
self.badge?.alpha = 0.95
self.menuButton.addSubview(self.badge!)
self.menuButton.layoutIfNeeded()
}
}
})

Related

When implementing custom view controller presentations, where to apply presented view's constraints?

When presenting a view controller using a custom animation, none of Apple's documentation or example code mentions or includes constraints, beyond the following:
// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
[containerView addSubview:toView];
toView.frame = toViewStartFrame;
The problem is that the double-height status bar is not recognized by custom-presented view controllers (view controllers that use non-custom presentations don't have this problem). The presented view controller is owned by the transition's container view, which is a temporary view provided by UIKit that we have next to no dominion over. If we anchor the presented view to that transient container, it only works on certain OS versions; not to mention, Apple has never suggested doing this.
UPDATE 1: There is no way to consistently handle a double-height status bar with custom modal presentations. I think Apple botched it here and I suspect they will eventually phase it out.
UPDATE 2: The double-height status bar has been phased out and no longer exists on non-edge-to-edge devices.
My answer is: You should not use constraints in case of custom modal presentations
Therefore I know your pain, so I will try to help you to save time and effort by providing some hints which I suddenly revealed.
Example case:
Card UI animation like follows:
Terms for further use:
Parent - UIViewController with "Detail" bar button item
Child - UIViewController with "Another"
Troubles you mentioned began, when my animation involved size change along with the movement. It causes different kinds of effects including:
Parent's under-status-bar area appeared and disappeared
Parent's subviews were animated poorly - jumps, duplication and other glitches.
After few days of debugging and searching I came up with the following solution (sorry for some magic numbers ;)):
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0.4,
options: .curveEaseIn, animations: {
toVC.view.transform = CGAffineTransform(translationX: 0, y: self.finalFrame.minY)
toVC.view.frame = self.finalFrame
toVC.view.layer.cornerRadius = self.cornerRadius
fromVC.view.layer.cornerRadius = self.cornerRadius
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, scale, scale, 1.0)
transform = CATransform3DTranslate(transform, 0, wdiff, 0)
fromVC.view.layer.transform = transform
fromVC.view.alpha = 0.6
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
Main point here is, that You have to use CGAffineTransform3D to avoid animation problems and problems with subviews animation (2D Transforms are not working for unknown reasons).
This approach fixes, I hope, all your problems without using constraints.
Feel free to ask questions.
UPD: According to In-Call status bar
After hours of all possible experiments and examining similar projects like this and this and stackoverflow questions like this, this (it's actually fun, OPs answer is there) and similar I am totally confused. Seems like my solution handles Double status bar on UIKit level (it adjusts properly), but the same movement is ignoring previous transformations. The reason is unknown.
Code samples:
You can see the working solution here on Github
P.S. I'm not sure if it's ok to post a GitHub link in the answer. I'd appreciate for an advice how to post 100-300 lines code In the answer.
I've been struggling with double-height statusBar in my current project and I was able to solve almost every issue (the last remaining one is a very strange transformation issue when the presentingViewController is embedded inside a UITabBarController).
When the height of the status bar changes, a notification is posted.
Your UIPresentationController subclass should subscribe to that particular notification and adjust the frame of the containerView and its subviews:
UIApplication.willChangeStatusBarFrameNotification
Here is an example of code I'm using:
final class MyCustomPresentationController: UIPresentationController {
// MARK: - StatusBar
private func subscribeToStatusBarNotifications() {
let notificationName = UIApplication.willChangeStatusBarFrameNotification
NotificationCenter.default.addObserver(self, selector: #selector(statusBarWillChangeFrame(notification:)), name: notificationName, object: nil)
}
#objc private func statusBarWillChangeFrame(notification: Notification?) {
if let newFrame = notification?.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? CGRect {
statusBarWillChangeFrame(to: newFrame)
} else {
statusBarWillChangeFrame(to: .zero)
}
}
func statusBarWillChangeFrame(to newFrame: CGRect) {
layoutContainerView(animated: true)
}
// MARK: - Object Lifecycle
deinit {
// Unsubscribe from all notifications
NotificationCenter.default.removeObserver(self)
}
// MARK: - Layout
/// Called when the status-bar is about to change its frame.
/// Relayout the containerView and its subviews
private func layoutContainerView(animated: Bool) {
guard let containerView = self.containerView else { return }
// Retrieve informations about status-bar
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let normalStatusBarHeight = Constants.Number.statusBarNormalHeight // 20
let isStatusBarNormal = statusBarHeight ==~ normalStatusBarHeight
if animated {
containerView.frame = …
updatePresentedViewFrame(animated: true)
} else {
// Update containerView frame
containerView.frame = …
updatePresentedViewFrame(animated: false)
}
}
func updatePresentedViewFrame(animated: Bool) {
self.presentedView?.frame = …
}
}

UIview animation misbehaves when image of an unrelated button is changed

On my viewcontroller, I have a button to enable/disable audio using the following action:
func audioControl() {
playMusic = playMusic ? false : true // toggle
if (playMusic) {
startMusic()
audioBtn.setImage(UIImage(named: "musicOn"),for: .normal)
}
else {
stopMusic()
audioBtn.setImage(UIImage(named: "musicOff"),for: .normal)
}
}
The button toggles its image, as expected.
There is also an unrelated UIImageView object on the same VC which is being translated as follows:
UIView.animate(withDuration: 10.0, delay: 0.0, options: [ .curveLinear, .repeat ], animations: {
self.movingImg.center = CGPoint(x: screenSize.minX, y: screenSize.maxY)
})
This simple animation works fine by itself. However, as soon as the button is tapped, the movingImg object goes outside the boundary of screen and it is no longer visible.
I am not sure why a change of image on a button causes another object animation to misbehave. Any thoughts? I am using Swift 3.
Thank you.
After reading through discussions in a number of SO threads, I found that with the image change (along with any other layout changes), the viewcontroller's viewDidLayoutSubviews() gets called. As such, the followings are two things I did to resolve this issue:
Within the audioControl() method, I removed the animation of movingImg (i.e. movingImg.layer.removeAllAnimations()) before updating the button image.
I added viewDidLayoutSubviews() to my viewcontroller and started the animation of movingImg there.
Hopefully, this approach helps others too.

Error when trying to click in a view created programmatically swift 3

I created a view programmatically, like a popup coming from the top with some text and images in there!
alert = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.width , height: 0))
alert?.frame.origin.y = (textLable?.frame.height)!
alert?.frame.size.height = (textLable?.frame.height)!
alert?.backgroundColor = self.arrayOptions.colorBackground
and then I'm trying to add a UITapGestureRecognizer to that view like this inside a setup func that is called in the init.
let tap = UITapGestureRecognizer(target: self, action: #selector(self.teste))
tap.numberOfTapsRequired = 1
alert?.addGestureRecognizer(tap)
im adding that view like this in a UitableViewController:
self.popView = PopupView(frame: CGRect(x: 0 , y: 0 , width:self.view.frame.width, height: 0), with: PopUpOptions.error, originY: 0,description:"blablabla")
self.view.addSubview(self.popView!)
But when I tap on the view nothing happens, but when I tap repeatedly over and over this error occurs:
<_UISystemGestureGateGestureRecognizer: 0x174186f50>: Gesture: Failed
to receive system gesture state notification before next touch
But I cant seem to find an answer for this, could anyone help me pls!
Thank you!
here is the GitHub link https://github.com/Coimbraa/AlertsPopup_framework for my framework
Took a look at your GitHub repo - Couple notes...
I see a lot of "!" in your code. Each one of those is a potential crash. For example, I had to make a number of edits just to get it to run at all (I don't have the image assets the code is expecting).
In PopUpView.swift change the init() func to this (just added the clipsToBounds line):
override public init(frame: CGRect) {
super.init(frame: frame)
setup()
self.clipsToBounds = true
}
Now, when you call popView?.toggleStatus() you probably won't see anything.
Your PopupView "container" has a height of Zero. Without clipping its contents, you see the content, but you can't interact with it.
I changed your animation block in toggleStatus() to this:
UIView.animate(withDuration: 0.5, animations: {
if let sz = self.alert?.frame.size {
self.frame.size.height = sz.height
}
self.alert?.frame.origin.y = (self.startY)! + (self.status ? 0 : -((self.textLable?.frame.height)! + self.startY))
}) { (finished:Bool) in
// ...
and I could now tap into and edit the TextView, and tap elsewhere and get print("pressed") output to the debug console.
I did not dig further, so I don't know if/where you need to put additional code to reset the frame-height back to Zero (or maybe it gets hidden or removed, whatever).
Try this:
alert.isUserInteractionEnabled = true
Set this property to true for those views you want to tap on (where you add the tap gesture recognizer).
You are adding that Custom Alert View in Table View Controller.
First try what Tung Fam has suggested, if it doesn't work then,
Trying adding it on window like:
self.view.window.addSubview(self.popView!)
Also set the frame of the window to the popupView.
As I'm seeing you are setting target for TapGesture as self in
let tap = UITapGestureRecognizer(target: self, action: #selector(self.teste))
Here self is object of UIView (your alert view).
So according to this line your tapGesture listener must be implemented in alert? class. And you need to use custom protocol or custom delegate to get the action of tapGesture in other classes.

Animate an imageView with changing constraints (progress bar) Swift

So, I am creating a sort of progress bar, mostly just an animation to go from 0 to 100 percent. I have two images, one that is red on bottom, one on top that is black. I am trying to take away from the top image to create the idea that it is loading from 0 to 100%, slowly revealing the image below. I have all of that working, my problem is that I do not see the in between. I see 0% and 100%, I can debug it and view the controller and see my view controller (self) updating and slowly taking away the top image so I know that it is working but I do not see it go through each animate. I have edited one moved things around a ton but have not been able to see the changes. Thanks for everyones help in advance!
override func viewDidAppear(_ animated: Bool) {
if intCounter <= 100{
UIView.animate(withDuration: 0.05, animations: catScanE.layoutIfNeeded)
repeat {
dblImageH = dblImageH - 3.80
self.catScanE.frame = CGRect(x: 27 , y: 197, width: 320, height: dblImageH)
intCounter = intCounter + 1
} while (intCounter <= 100)
} else {
performSegue(withIdentifier: "lastSegue", sender: nil)
}
}
I have gotten the animate to basically work, it scrolls down the screen but shows a type of animation. I want it to slowly restrict the constraints but this is not the issue now. Since the animation "works", I am not able to get it to segue after the animation, this is what I have. Adding the if intCounter part makes it skip the animation and segue, without it the screen performs the animation and sits there...
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 5.0, animations: {
self.dblImageH = self.dblImageH - 3.80
self.catScanE.frame = CGRect(x: 27 , y: 197, width: 320, height: self.dblImageH)
self.intCounter = self.intCounter + 1
if self.intCounter > 100{
self.performSegue(withIdentifier: "lastSegue", sender: nil)}
})
}
I figured it out with some help from Ravron and more StackOverflow searching. I set the content mode to top in the attributes and used this code to make it act like a progress bar (this is a black image with a red one underneath to look like it is going from 0 to 100). I ran into an issue with the image basically moving up or downwards, this was fixed by changing the y coordinate to match the change in size so it would basically stay in the same position. I just need to figure out how to make a counter to go from 0 to 100 while it is animating. Thanks! Hopefully this can help someone else making a custom progress bar.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 5.0, animations: {
self.catScanE.frame = CGRect(x: 27 , y: 200, width: 320, height: 0)
}, completion: {finished in self.performSegue(withIdentifier: "lastSegue", sender: nil)})
}
Your animate block runs only once, and the final settings of the view's frame is all that counts. The view is actually being animated from 0% to 100% as you wish, but it happens in 50 milliseconds, or about three frames.
Just set the frame to the desired final position in the animate block and increase the duration to something reasonable, like 100 * 0.05s = 5s.
To expand slightly: the animate method says, informally, "run the block below, which changes one or more traits of various views. Then, implicitly animate those traits from their current values to the values set when the block was run." So it's only called once, and in it you should set traits to their desired final values.
Consider reading through the animations section of the View Programming Guide for iOS which gives a comprehensive overview of animation basics on the platform, and also covers UIView block-based animations, the type you're trying to use here.

iOS custom UIView as input keyboard [duplicate]

This question already has answers here:
How to create a custom keyboard
(5 answers)
Closed 6 years ago.
I have huge issues when trying to use a custom view as a input keyboard for a text field.
The setup is pretty simple (for testing) : I have a UIView with one button, half the size of the screen and a text field somewhere above it.
I set up a dummy view to be the input view of the text field like so let dummyView : UIView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
Also I added the following setting on viewDidAppear testView.frame.origin.y = view.bounds.height. This is of course ignored as the view appears on screen (putting it on viewDidLoad leads to same behaviour). However, putting it in viewdidLayoutSubviews leads to a very strange behaviour, the view does not appear on screen, but trying to animate to, let's say a 380 origin.y does nothing.
The best part is yet to come: if I hide the view, then the slide up animation (code bellow)
UIView.animateWithDuration(0.6, animations: {
self.testView.frame = self.whereToBe!
}, completion: {
finished in
if finished {
}
})
works just fine. However, trying to slide it down again and resigningFirstResponder on the text field on button press (code bellow)
UIView.animateWithDuration(0.6, animations: {
//self.testView.frame = self.whereToBe!
self.testView.frame.origin.y = self.view.frame.height
}, completion: {
finished in
if finished {
self.view.endEditing(true)
//self.inputField.resignFirstResponder()
self.testView.hidden = true
}
})
shows a fascinating wrong behaviour: the view slides from top to its current position. However, on second press on the button, it works just fine, it slides down BUT it reappears on screen after the animation has completed.
Can you please help me?
Use the view as the inputView of the textfield by simply setting
self.textField.inputView = self.pickerView;
and avoid all these custom animations.

Resources