Subclassing UILabel doesn't work as expected - ios

I want to add a UILabel to the view which slides down when an error occurs to send the error message to user and after 3 seconds it will slide up to disappear. The prototype of it is like the one Facebook or Instagram shows. I need errorLabel in many ViewControllers, so I tried to subclass UILabel. Here is my subclass ErrorLabel:
class ErrorLabel: UILabel {
var errorString: String?
func sendErrorMessage() {
self.text = errorString
showErrorLabel()
let timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "hideErrorLabel", userInfo: nil, repeats: false)
}
func animateFrameChange() {
UIView.animateWithDuration(1, animations: { self.layoutIfNeeded() }, completion: nil)
}
func showErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height + 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
func hideErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height - 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
}
Then, I tried to add the errorLabel to one of my ViewController like following:
class ViewController: UIViewController {
var errorLabel = ErrorLabel()
override func viewDidLoad() {
super.viewDidLoad()
let errorLabelFrame = CGRectMake(0, 20, self.view.frame.width, 0)
self.errorLabel.frame = errorLabelFrame
self.errorLabel.backgroundColor = translucentTurquoise
self.errorLabel.font = UIFont.systemFontOfSize(18)
self.errorLabel.textColor = UIColor.whiteColor()
self.errorLabel.textAlignment = NSTextAlignment.Center
self.view.addSubview(errorLabel)
self.view.bringSubviewToFront(errorLabel)
}
func aFunc(errorString: String) {
self.errorLabel.errorString = errorString
self.errorLabel.sendErrorMessage()
}
}
When I run it in iOS Simulator, it doesn't work as expected:
errorLabel shows on the left horizontally and in the middle vertically with only I... which should be Invalid parameters.
After 1 second, it goes to the position as expected but its width is still not self.view.frame.width.
After that, nothing happens but it should slide up after 3 seconds.
Can you tell me what's wrong and how to fix the error?

I might have partial solution to your issues. Hope it helps.
The I... happens when the string is longer than the view. For this you'll need to increase the size of UILabel.
For aligning text inside a UILable refer to this.
To animate away use the same code in the completion block of the UIView.animateWithDuration. Refer to this link
I suggest you to consider using Extensions to accomplish what you are trying to do.

Rather than subclassing UILabel I would subclass UIViewController, which maybe you have aldready done? Let's call out subclass - BaseViewController and let all our UIViewControllers subclass this class.
I would then programatically create an UIView which contains a vertically and horizontally centered UILabel inside this BaseViewController class. The important part here is to create NSLayoutConstraints for it. I would then hide and show it by changing the values of the constraints.
I would use the excellent pod named Cartography to create constraints, which makes it super easy and clean!
With this solution you should be able to show or hide an error message in any of your UIViewControllers
This is untested code but hopefully very near a solution to your problem.
import Cartography /* Requires that you have included Cartography in your Podfile */
class BaseViewController: UIViewController {
private var yPositionForErrorViewWhenVisible: Int { return 0 }
private var yPositionForErrorViewWhenInvisible: Int { return -50 }
private let hideDelay: NSTimeInterval = 3
private var timer: NSTimer!
var yConstraintForErrorView: NSLayoutConstraint!
var errorView: UIView!
var errorLabel: UILabel!
//MARK: - Initialization
required init(aDecoder: NSCoder) {
super.init(aDecoder)
setup()
}
//MARK: - Private Methods
private func setup() {
setupErrorView()
}
private func setupErrorView() {
errorView = UIView()
errorLabel = UILabel()
errorView.addSubview(errorLabel)
view.addSubview(errorView)
/* Set constraints between viewController and errorView and errorLabel */
layout(view, errorView, errorLabel) {
parent, errorView, errorLabel in
errorView.width == parent.width
errorView.centerX == parent.centerX
errorView.height == 50
/* Capture the y constraint, which defaults to be 50 points out of screen, so that it is not visible */
self.yConstraintForErrorView = (errorView.top == parent.top - self.yPositionForErrorViewWhenInvisible)
errorLabel.height = 30
errorLabel.width == errorView.width
errorLabel.centerX == errorView.centerX
errorLabel.centerY = errorView.centerY
}
}
private func hideOrShowErrorMessage(hide: Bool, animated: Bool) {
if hide {
yConstraintForErrorView.constant = yPositionForErrorViewWhenInvisible
} else {
yConstraintForErrorView.constant = yPositionForErrorViewWhenVisible
}
let automaticallyHideErrorViewClosure: () -> Void = {
/* Only scheduling hiding of error message, if we just showed it. */
if show {
automaticallyHideErrorMessage()
}
}
if animated {
view.animateConstraintChange(completion: {
(finished: Bool) -> Void in
automaticallyHideErrorViewClosure()
})
} else {
view.layoutIfNeeded()
automaticallyHideErrorViewClosure()
}
}
private func automaticallyHideErrorMessage() {
if timer != nil {
if timer.valid {
timer.invalidate()
}
timer = nil
}
timer = NSTimer.scheduledTimerWithTimeInterval(hideDelay, target: self, selector: "hideErrorMessage", userInfo: nil, repeats: false)
}
//MARK: - Internal Methods
func showErrorMessage(message: String, animated: Bool = true) {
errorLabel.text = message
hideOrShowErrorMessage(false, animated: animated)
}
//MARK: - Selector Methods
func hideErrorMessage(animated: Bool = true) {
hideOrShowErrorMessage(true, animated: animated)
}
}
extension UIView {
static var standardDuration: NSTimeInterval { return 0.3 }
func animateConstraintChange(duration: NSTimeInterval = standardDuration, completion: ((Bool) -> Void)? = nil) {
UIView.animate(durationUsed: duration, animations: {
() -> Void in
self.layoutIfNeeded()
}, completion: completion)
}
}

Related

FirstViewController over another SecondViewController where SecondViewController is clickable

I am trying to create universal alert that appears on the top most view controller while the bottom viewcontroller is still clickable.
This alert is just a 20 points height status line that inform user about network reachability. How can I make UIViewController not user interactable?
Please note that I do not use Storyboard or XIB
Also if you are targeting iOS11 and above you would need to use safeAreaLayoutGuide while using autolayout code
The solution is two folds.
First, create a Base View Controller and have all your view controllers that need to show the alert to extend from that Base View Controller.
Then create a new swift file, a subclass of NSObject. Lets say NetworkAlerter.swift and copy paste the code below (as appropriate)
import UIKit
class NetworkAlerter: NSObject {
var window :UIWindow? = UIApplication.shared.keyWindow
var alertShowingConstraint : NSLayoutConstraint?
var alertHidingConstraint : NSLayoutConstraint?
var closeTimer : Timer? = nil
public lazy var networkIndicatorLabel : UILabel = {
let label : UILabel = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = NSTextAlignment.center
return label
}()
override init() {
super.init()
createSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("Time to deinit")
networkIndicatorLabel.removeFromSuperview()
}
func createSubviews() {
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
window.addSubview(networkIndicatorLabel)
addAutolayout()
}
func addAutolayout() {
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
alertShowingConstraint = networkIndicatorLabel.topAnchor.constraint(equalTo: window.topAnchor)
alertHidingConstraint = networkIndicatorLabel.bottomAnchor.constraint(equalTo: window.topAnchor)
alertHidingConstraint?.isActive = true
networkIndicatorLabel.heightAnchor.constraint(equalToConstant: 20).isActive = true
networkIndicatorLabel.leadingAnchor.constraint(equalTo: window.leadingAnchor).isActive = true
networkIndicatorLabel.trailingAnchor.constraint(equalTo: window.trailingAnchor).isActive = true
}
func showNetworkAlerter(networkAvailable: Bool) {
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
invalidateAndKillTimer()
closeTimer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(dismissNetworkAlerter), userInfo: nil, repeats: false)
if networkAvailable {
networkIndicatorLabel.text = "Available"
networkIndicatorLabel.backgroundColor = UIColor.green
} else {
networkIndicatorLabel.text = "Not Available"
networkIndicatorLabel.backgroundColor = UIColor.red
}
window.layoutIfNeeded()
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseOut, animations: {
if (self.alertHidingConstraint?.isActive)! {
self.alertHidingConstraint?.isActive = false
}
if !(self.alertShowingConstraint?.isActive)! {
self.alertShowingConstraint?.isActive = true
}
window.layoutIfNeeded()
}, completion: { _ in
})
}
#objc func dismissNetworkAlerter() {
invalidateAndKillTimer()
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
window.layoutIfNeeded()
UIView.animate(withDuration: 0.5, animations: {
if (self.alertShowingConstraint?.isActive)! {
self.alertShowingConstraint?.isActive = false
}
if !(self.alertHidingConstraint?.isActive)! {
self.alertHidingConstraint?.isActive = true
}
window.layoutIfNeeded()
}) { (done) in
}
}
// MARK:- Timer Related
private func invalidateAndKillTimer() -> Void {
if (closeTimer != nil) {
closeTimer?.invalidate()
closeTimer = nil
}
}
}
Then move back Base View Controller. Right on top copy paste the following
var networkAlertLauncher : NetworkAlerter? = nil
and then find an appropriate place in Base View Controller and paste the following:
func showAlertBar(networkAvailabilityStatus: Bool) -> Void {
if networkAlertLauncher != nil {
networkAlertLauncher = nil
}
networkAlertLauncher = NetworkAlerter()
networkAlertLauncher?.showNetworkAlerter(networkAvailable: networkAvailabilityStatus)
}
Now the function showAlertBar will be accessible from all the view controllers that you have extended from Base View Controller. You can invoke it like so:
self.showAlertBar(networkAvailabilityStatus: false) or self.showAlertBar(networkAvailabilityStatus: true)

How do you really hide and show a tab bar when tapping on part of your view? (no buttons but any location of the screen)

override func viewDidLoad() {
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
tabBarController?.hideTabBarAnimated(hide: true)
}
extension UITabBarController {
func hideTabBarAnimated(hide:Bool) {
UIView.animate(withDuration: 2, animations: {
if hide {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: 100)
} else {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: -100)
}
})
}
}
I can only hide the tab bar but I can't make it show when you tap again. I tried to look for answers on stack overflow but the answers seems to only work if you're using a button or a storyboard.
Have a variable isTabBarHidden in class which stores if the tabBar has been animated to hide. (You could have used tabBar.isHidden, but that would complicate the logic a little bit when animate hiding and showing)
class ViewController {
var isTabBarHidden = false // set the default value as required
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
guard let tabBarControllerFound = tabBarController else {
return
}
tabBarController?.hideTabBarAnimated(hide: !isTabBarHidden)
isTabBarHidden = !isTabBarHidden
}
}
Generalised solution with protocol which will work in all the screens
Create UIViewController named BaseViewController and make it base class of all of your view controllers
Now Define protocol
protocol ProtocolHideTabbar:class {
func hideTabbar ()
}
protocol ProtocolShowTabbar:class {
func showTabbar ()
}
extension ProtocolHideTabbar where Self : UIViewController {
func hideTabbar () {
self.tabBarController?.tabBar.isHidden = true
}
}
extension ProtocolShowTabbar where Self : UIViewController {
func showTabbar () {
self.tabBarController?.tabBar.isHidden = false
}
}
By default we want show tabbar in every view controller so
extension UIViewController : ProtocolShowTabbar {}
In your BaseView Controller
in view will appear method add following code to show hide based on protocol
if self is ProtocolHideTabbar {
( self as! ProtocolHideTabbar).hideTabbar()
} else if self is ProtocolShowTabbar{
( self as ProtocolShowTabbar).showTabbar()
}
How to use
Simply
class YourViewControllerWithTabBarHidden:BaseViewController,ProtocolHideTabbar {
}
Hope it is helpful
Tested 100% working
Please try below code for that in UITabBarController subclass
var isTabBarHidden:Bool = false
func setTabBarHidden(_ tabBarHidden: Bool, animated: Bool,completion:((Void) -> Void)? = nil) {
if tabBarHidden == isTabBarHidden {
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
//check tab bar is visible and view and window height is same then it should be 49 + window Heigth
if (tabBarHidden == true && UIScreen.main.bounds.height == self.view.frame.height) {
let offset = self.tabBar.frame.size.height
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset)
}
if let block = completion {
block()
}
return
}
let offset: CGFloat? = tabBarHidden ? self.tabBar.frame.size.height : -self.tabBar.frame.size.height
UIView.animate(withDuration: animated ? 0.250 : 0.0, delay: 0.1, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [.curveEaseIn, .layoutSubviews], animations: {() -> Void in
self.tabBar.center = CGPoint(x: CGFloat(self.tabBar.center.x), y: CGFloat(self.tabBar.center.y + offset!))
//Check if View is already at bottom so we don't want to move view more up (it will show black screen on bottom ) Scnario : When present mail app
if (Int(offset!) <= 0 && UIScreen.main.bounds.height == self.view.frame.height) == false {
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset!)
}
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}, completion: { _ in
if let block = completion {
block()
}
})
isTabBarHidden = tabBarHidden
}
Hope it is helpful

UIScrollView stops scrolling on keyboard dismiss

I'm currently working on an app for iPhone using Swift 3 however I have ran into an issue with the scrollview.
Prior to selecting a text field and having the keyboard appear, the scrollview work normally (ie.: I can scroll up and down), however after dismissing the keyboard, the scrollview does not allow me to scroll anymore which is my problem.
Note: if I select a text field again and make the keyboard appear it works fine and stops working once it is dismissed again.
I have checked the isScrollEnabled property of the scrollview and it appears to be enabled. Unfortunately I am still not too familiar with all the details of the scrollview and cannot seem to figure out why it has stopped working.
Any help or pointers as to where I could look would be greatly appreciated.
Edit: there is quite a bit of code but here is the narrowed down portion related to scroll view and keyboard:
class ScrollViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate {
//Scroll view
#IBOutlet weak var scrollView: UIScrollView!
//UIView inside the scroll view
#IBOutlet weak var contentView: UIView!
//Save button on the top right corner
#IBOutlet weak var saveButton: UIBarButtonItem!
//Text field being editted
var activeTextField:UITextField?
fileprivate var contentInset:CGFloat?
fileprivate var indicatorInset:CGFloat?
override func viewDidLoad() {
contentInset = scrollView.contentInset.bottom
indicatorInset = scrollView.scrollIndicatorInsets.bottom
NotificationCenter.default.addObserver(self,
selector: #selector(ScrollViewController.keyboardWillShow(_:)),
name: NSNotification.Name.UIKeyboardWillShow,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(ScrollViewController(_:)),
name: NSNotification.Name.UIKeyboardWillHide,
object: nil)
}
func adjustInsetForKeyboardShow(_ show: Bool, notification: Notification) {
let userInfo = notification.userInfo ?? [:]
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let adjustmentHeight = (keyboardFrame.height + 20) * (show ? 1 : -1)
scrollView.contentInset.bottom = (contentInset! + adjustmentHeight)
scrollView.scrollIndicatorInsets.bottom = (indicatorInset! + adjustmentHeight)
}
func keyboardWillShow(_ notification: Notification) {
adjustInsetForKeyboardShow(true, notification: notification)
}
func keyboardWillHide(_ notification: Notification) {
adjustInsetForKeyboardShow(false, notification: notification)
}
//Tap gesture to dismiss the keyboard
#IBAction func hideKeyboard(_ sender: AnyObject) {
self.view.endEditing(false)
}
deinit {
NotificationCenter.default.removeObserver(self);
}
}
I have create extension of UIViewController and create method for scrollView. Just need to call from viewWillAppear() and viewDidDisappear()
extension UIViewController {
func registerForKeyboradDidShowWithBlock (scrollview:UIScrollView ,block: ((CGSize?) -> Void)? = nil ){
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardDidShow, object: nil, queue: nil) { (notification) in
if let userInfo = (notification as NSNotification).userInfo {
if let keyboarRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.findFirstResponder() != nil {
let keyboarRectNew = self.view .convert(keyboarRect, to: self.view)
let scrollViewSpace = scrollview.frame.origin.y + scrollview.contentOffset.y
let textFieldRect:CGRect = self.view.findFirstResponder()!.convert(self.view.findFirstResponder()!.bounds, to: self.view)
let textFieldSpace = textFieldRect.origin.y + textFieldRect.size.height
let remainingSpace = self.view.frame.size.height - keyboarRectNew.size.height
if scrollViewSpace + textFieldSpace > remainingSpace {
let gap = scrollViewSpace + textFieldSpace - remainingSpace
scrollview .setContentOffset(CGPoint(x: scrollview.contentOffset.x, y: gap), animated: true)
}
}
}
}
block?(CGSize.zero)
}
}
func registerForKeyboardWillHideNotificationWithBlock ( scrollview:UIScrollView ,block: ((Void) -> Void)? = nil) {
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillHide, object: nil, queue: nil, using: { (notification) -> Void in
scrollview.scrollRectToVisible(CGRect(x: 0, y: 0, width: 0, height: 0), animated: true)
scrollview.contentOffset = CGPoint(x: 0, y: 0)
scrollview.contentInset = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0)
scrollview.scrollIndicatorInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0);
block?()
})
}
func deregisterKeyboardShowAndHideNotification (){
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
self.view.findFirstResponder()?.resignFirstResponder()
}
}
I have created extension of UIView to create find first responder method in that.
extension UIView {
func findFirstResponder() -> UIView? {
if self.isFirstResponder {
return self
}
for subview: UIView in self.subviews {
let firstResponder = subview.findFirstResponder()
if nil != firstResponder {
return firstResponder
}
}
return nil
}
}
Create Extension of UIView and write down this method.
extension UIView {
func findFirstResponder() -> UIView? {
if self.isFirstResponder {
return self
}
for subview: UIView in self.subviews {
let firstResponder = subview.findFirstResponder()
if nil != firstResponder {
return firstResponder
}
}
return nil
}
}
Their is a third party library in Objective C to handle the keyboard with scroll view, collection view and table view. The name of library is tpkeyboardavoidingscrollview. Try to embed this if possible.

UIView disappears after user interaction

whenever I click a textfield inside the view, then click the other text field, the view disappears. Strange... Can anyone help?
I animate the view using facebook pop. Here is my animation engine code:
import UIKit
import pop
class AnimationEngine {
class var offScreenRightPosition: CGPoint {
return CGPoint(x: UIScreen.main.bounds.width + 250,y: UIScreen.main.bounds.midY - 75)
}
class var offScreenLeftPosition: CGPoint{
return CGPoint(x: -UIScreen.main.bounds.width,y: UIScreen.main.bounds.midY - 75)
}
class var offScreenTopPosition: CGPoint{
return CGPoint(x: UIScreen.main.bounds.midX,y: -UIScreen.main.bounds.midY)
}
class var screenCenterPosition: CGPoint {
return CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY - 75)
}
let ANIM_DELAY : Int = 1
var originalConstants = [CGFloat]()
var constraints: [NSLayoutConstraint]!
init(constraints: [NSLayoutConstraint]) {
for con in constraints {
originalConstants.append(con.constant)
con.constant = AnimationEngine.offScreenRightPosition.x
}
self.constraints = constraints
}
func animateOnScreen(_ delay: Int) {
let time = DispatchTime.now() + Double(Int64(Double(delay) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time) {
var index = 0
repeat {
let moveAnim = POPSpringAnimation(propertyNamed: kPOPLayoutConstraintConstant)
moveAnim?.toValue = self.originalConstants[index]
moveAnim?.springBounciness = 8
moveAnim?.springSpeed = 8
if (index < 0) {
moveAnim?.dynamicsFriction += 10 + CGFloat(index)
}
let con = self.constraints[index]
con.pop_add(moveAnim, forKey: "moveOnScreen")
index += 1
} while (index < self.constraints.count)
}
}
class func animateToPosisition(_ view: UIView, position: CGPoint, completion: ((POPAnimation?, Bool) -> Void)!) {
let moveAnim = POPSpringAnimation(propertyNamed: kPOPLayerPosition)
moveAnim?.toValue = NSValue(cgPoint: position)
moveAnim?.springBounciness = 8
moveAnim?.springSpeed = 8
moveAnim?.completionBlock = completion
view.pop_add(moveAnim, forKey: "moveToPosition")
}
}
Then here is my viewcontroller code where the view is inside in:
import UIKit
import pop
class LoginVC: UIViewController, UITextFieldDelegate {
override var prefersStatusBarHidden: Bool {
return true
}
#IBOutlet weak var emailLoginVCViewConstraint: NSLayoutConstraint!
#IBOutlet weak var emailLoginVCView: MaterialView!
#IBOutlet weak var emailAddressTextField: TextFieldExtension!
#IBOutlet weak var passwordTextField: TextFieldExtension!
var animEngine : AnimationEngine!
override func viewDidAppear(_ animated: Bool) {
self.emailLoginVCView.isUserInteractionEnabled = true
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.bringSubview(toFront: emailAddressTextField)
self.animEngine = AnimationEngine(constraints: [emailLoginVCViewConstraint])
self.emailAddressTextField.delegate = self
self.passwordTextField.delegate = self
emailAddressTextField.allowsEditingTextAttributes = false
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if (textField === emailAddressTextField) {
passwordTextField.becomeFirstResponder()
} else if (textField === passwordTextField) {
passwordTextField.resignFirstResponder()
} else {
// etc
}
return true
}
#IBAction func emailTapped(_ sender: AnyObject) {
AnimationEngine.animateToPosisition(emailLoginVCView, position: AnimationEngine.screenCenterPosition, completion: { (POPAnimation, Bool)
in
})
}
#IBAction func exitTapped(_ sender: AnyObject) {
AnimationEngine.animateToPosisition(emailLoginVCView, position: AnimationEngine.offScreenRightPosition, completion: { (POPAnimation, Bool)
in
})
}
}
Last here is my hierchy and options: (my view's name is emailLoginVCView). Also when I was debugging when I clicked another textfield I set a breakpoint so I got this info: enter image description here
I have a constraint that binds the center of the login view with the center of the main screen
when I create the AnimationEngine,I pass it that constraint, and it sets its constant to be the offScreenRightPosition.x
when I bring up the email login sheet, I'm not changing the constant of the constraint; I'm just changing the position of the view
which means that autolayout thinks it’s supposed to still be offscreen
when the second textfield becomes active, that’s somehow triggering auto-layout to re-evaluate the constraints, and it sees that the login view’s position doesn’t match what the constraint says it should be so....
Autolayout moves it offscreen
So if I add this in emailTapped(_:), the problem goes away :)
#IBAction func emailTapped(_ sender: AnyObject) {
AnimationEngine.animateToPosisition(emailLoginVCView, position: AnimationEngine.screenCenterPosition, completion: { (POPAnimation, Bool)
in
self.emailLoginVCViewConstraint.constant = 0
})
}

UILabel slides down to send error message

I want to add a UILabel to the view which slides down when an error occurs to send error message to user. The prototype of it is like the one Facebook or Instagram shows. Here is the codes I have worked out so far:
func sendErrorMessage(errorString: String) {
self.errorLabel.text = errorString
UIView.animateWithDuration(1, animations: {
self.errorLabel.frame.height = 30 //Cannot assign to the result of this expression
self.topView.layoutIfNeeded()
}, completion: {
(finished: Bool) in var timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector(), userInfo: nil, repeats: false)
})
}
errorLabel is now already in storyboard but with the height of 0 and topView is the superview of errorLabel. I am quite new to these methods so I am stuck here. I don't understand why that error occurred and what the selector should do here. There is also a step that I haven't done here that the errorLabel should slide up to disappear after three seconds and that's why I need a timer here.
Plus: Is there any difference if I create a new errorLabel whenever it is needed instead of make it ready before in storyboard? I mean in app performance of memory management.
UPDATE
I need errorLabel in many ViewControllers, so following the idea of #Sajjon, I tried to subclass UILabel. Here is my subclass ErrorLabel:
class ErrorLabel: UILabel {
var errorString: String?
func sendErrorMessage() {
self.text = errorString
showErrorLabel()
let timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "hideErrorLabel", userInfo: nil, repeats: false)
}
func animateFrameChange() {
UIView.animateWithDuration(1, animations: { self.layoutIfNeeded() }, completion: nil)
}
func showErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height + 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
func hideErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height - 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
}
Generally speaking, CGRect objects used by views for their frame or bounds properties are immutable. Instead of trying to directly modify the view's frame, create a new CGRect which contains the desired final size and position and assign that to the view's frame in the animation block.
//Don't do this.
myView.frame.size.height = 30;
// Do this instead.
CGRect oldRect = myView.frame;
CGRect newFrame = CGRectMake(oldRect.origin.x, oldRect.origin.y, oldRect.size.width, 30);
[UIView animateWithDuration:0.4 animations:^{
myView.frame = newFrame;
};
Also, I would strongly suggest making the label show and hide my changing its y position on and off the top of the screen, rather than changing the height to 0, which can have some unintended layout consequences
You ought to use autolayout for this, when showing the errorLabel (or some container view that it is inside), I would give the height constraint your wished value (30). And hiding it again by giving the constraint a value of 0.
I would create an UIView extension and put code for animating this height change.
This is untested code, but will give you an idea of how to achieve it.
class MyViewController: UIViewController {
#IBOutlet weak var errorLabelHeightConstraint: NSLayoutConstraint!
private let errorLabelHeightVisible: CGFloat = 30
private let hideDelay: NSTimeInterval = 3
private var timer: NSTimer!
func sendErrorMessage(errorString: String) {
errorLabel.text = errorString
showOrHideErrorView(false)
}
func showOrHideErrorView(hide: Bool = true, animated: Bool = true) {
if hide {
errorLabelHeightConstraint.constant = 0
} else {
errorLabelHeightConstraint.constant = errorLabelHeightVisible
}
let automaticallyHideErrorViewClosure: () -> Void = {
/* Only scheduling hiding of error message, if we just showed it. */
if !hide {
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
automaticallyHideErrorMessage()
})
}
}
if animated {
view.animateConstraintChange(completion: {
(finished: Bool) -> Void in
automaticallyHideErrorViewClosure()
})
} else {
view.layoutIfNeeded()
automaticallyHideErrorViewClosure()
}
}
/* Selector method */
func hideError() {
showOrHideErrorView()
}
func automaticallyHideErrorMessage() {
if timer != nil {
if timer.valid {
timer.invalidate()
}
timer = nil
}
timer = NSTimer.scheduledTimerWithTimeInterval(hideDelay, target: self, selector: "hideError", userInfo: nil, repeats: false)
}
}
extension UIView {
static var standardDuration: NSTimeInterval { get { return 0.3 } }
func animateConstraintChange(duration: NSTimeInterval = standardDuration, completion: ((Bool) -> Void)? = nil) {
UIView.animate(durationUsed: duration, animations: {
() -> Void in
self.layoutIfNeeded()
}, completion: completion)
}
}
you need to set Frame of UILable instead of its height only.
func sendErrorMessage(errorString: String) {
self.errorLabel.text = errorString
UIView.animateWithDuration(1, animations: {
self.errorLabel.setFrame = CGRectmake(self.errorLabel.frame.origin.x,self.errorLabel.frame.origin.y,self.errorLabel.frame.size.width,30)
self.topView.layoutIfNeeded()
}, completion: {
(finished: Bool) in var timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector(), userInfo: nil, repeats: false)
})
}
For Change height Try This:
self.errorLabel.frame.size.height = 30

Resources