Vertical alignment on editable UITextView - ios

I want to set the vertical alignment on my UITextView. The text view is editable and scroll is disabled:
let textView = UITextView(frame: frame)
textView.backgroundColor = .clear
textView.attributedText = myAttributedString
textView.isUserInteractionEnabled = true
textView.isEditable = true
textView.allowsEditingTextAttributes = true
textView.isScrollEnabled = false
textView.textContainerInset = .zero
self.addSubview(textView)
So, I would like to do something like :
textView.verticalAlignment = .center
I already tried subclassing the text view and adding an attribute like this:
class MyTextView: UITextView {
public var verticalAlignment: UIControl.ContentVerticalAlignment = .center {
didSet {
let topCorrection: CGFloat
switch verticalAlignment {
case .center:
topCorrection = (bounds.size.height - contentSize.height * zoomScale) / 2.0
case .bottom:
topCorrection = (bounds.size.height - contentSize.height * zoomScale)
case .top, .fill:
topCorrection = 0
#unknown default:
topCorrection = 0
}
contentInset.top = max(0, topCorrection)
}
}
}
But it seems not to work with isScrollEnabled set to false.
Every other solution I found on the internet didn't work either and I'm a bit hopeless… Can you help me?
Thanks

Maybe you intended to use a UITextField?
let field = UITextField()
field.contentVerticalAlignment = UIControlContentVerticalAlignment.center
Or set it to .bottom, .top, etc.

Create a custom textView
class CustomTextView: UITextView {
override var canBecomeFirstResponder: Bool {
return false
}
override var selectedTextRange: UITextRange? {
get {
return nil
} set {
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
tapGestureRecognizer.numberOfTapsRequired == 1 {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
if let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
longPressGestureRecognizer.minimumPressDuration < 0.325 {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
gestureRecognizer.isEnabled = false
return false
}
}
Use this textview as a baseclass for the textview

Related

My UIView doesn't show up as I try to set constraints relative to safe area programmatically

I have a StackContainerView inside my main view controller called TodayPicksViewController. I am trying to programmatically set the StackContainerView to fill up the whole view controller side to side, with around 50 from top and bottom (just like a Tinder card).
However, as I try to implement constraints relative to safe area as follows(as other answers on StackOverflow suggest), turned out the StackContainerView doesn't show up at all. I don't know where the problem is.
Please advice.
Code of my main view controller, TodayPicksViewController:
class TodayPicksViewController: UIViewController {
//MARK: - Properties
var viewModelData = [CardsDataModel(bgColor: UIColor(red:0.96, green:0.81, blue:0.46, alpha:1.0), text: "Hamburger", image: "hamburger"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.64, blue:0.96, alpha:1.0), text: "Puppy", image: "puppy"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.63, blue:0.49, alpha:1.0), text: "Poop", image: "poop"),
CardsDataModel(bgColor: UIColor(red:0.69, green:0.52, blue:0.38, alpha:1.0), text: "Panda", image: "panda"),
CardsDataModel(bgColor: UIColor(red:0.90, green:0.99, blue:0.97, alpha:1.0), text: "Subway", image: "subway"),
CardsDataModel(bgColor: UIColor(red:0.83, green:0.82, blue:0.69, alpha:1.0), text: "Robot", image: "robot")]
var stackContainer : StackContainerView!
private let spinner = JGProgressHUD(style: .dark)
private var users = [[String: String]]()
private var results = [SearchResult]()
private var hasFetched = false
var divisor: CGFloat!
private let noResultsLabel: UILabel = {
let label = UILabel()
label.isHidden = true
label.text = "No Results"
label.textAlignment = .center
label.textColor = .green
label.font = .systemFont(ofSize: 21, weight: .medium)
return label
}()
override func loadView() {
view = UIView()
stackContainer = StackContainerView()
view.addSubview(stackContainer)
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(noResultsLabel)
configureStackContainer()
stackContainer.dataSource = self
}
#IBAction func panMatch(_ sender: UIPanGestureRecognizer) {
let match = sender.view!
let point = sender.translation(in: view)
let xFromCenter = match.center.x - view.center.x
print(xFromCenter)
match.center = CGPoint(x: view.center.x + point.x, y: view.center.y + point.y)
match.transform = CGAffineTransform(rotationAngle: xFromCenter/divisor)
if sender.state == UIGestureRecognizer.State.ended {
if match.center.x < 75 {
// Move off to the left side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x - 200, y: match.center.y + 75)
match.alpha = 0
})
return
} else if match.center.x > (view.frame.width - 75) {
// Move off to the right side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x + 200, y: match.center.y + 75)
match.alpha = 0
})
return
}
// resetCard()
}
}
private var loginObserver: NSObjectProtocol?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
validateAuth()
}
private func validateAuth() {
if FirebaseAuth.Auth.auth().currentUser == nil {
let vc = SignInViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: false)
}
}
#objc private func pageControlDidChange(_ sender: UIPageControl) {
let current = sender.currentPage
// scrollView.setContentOffset(CGPoint(x: CGFloat(current) * view.frame.size.width,
// y: 70), animated: true)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
//MARK: - Configurations
func configureStackContainer() {
stackContainer.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -60).isActive = true
// stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
// stackContainer.heightAnchor.constraint(equalToConstant: 400).isActive = true
stackContainer.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
stackContainer.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
stackContainer.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
stackContainer.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
func updateUI() {
if results.isEmpty {
noResultsLabel.isHidden = false
}
else {
noResultsLabel.isHidden = true
}
}
func calcAge(birthday: Date) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
// let birthdayDate = dateFormater.date(from: birthday)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthday, to: now, options: [])
let age = calcAge.year
return age!
}
extension TodayPicksViewController : SwipeCardsDataSource {
func numberOfCardsToShow() -> Int {
return viewModelData.count
}
func card(at index: Int) -> SwipeCardView {
let card = SwipeCardView()
card.dataSource = viewModelData[index]
return card
}
func emptyView() -> UIView? {
return nil
}
}
Probably doesn't matter, but here is my code for the StackContainerView:
class StackContainerView: UIView, SwipeCardsDelegate {
//MARK: - Properties
var numberOfCardsToShow: Int = 0
var cardsToBeVisible: Int = 3
var cardViews : [SwipeCardView] = []
var remainingcards: Int = 0
let horizontalInset: CGFloat = 10.0
let verticalInset: CGFloat = 10.0
var visibleCards: [SwipeCardView] {
return subviews as? [SwipeCardView] ?? []
}
var dataSource: SwipeCardsDataSource? {
didSet {
reloadData()
}
}
//MARK: - Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reloadData() {
removeAllCardViews()
guard let datasource = dataSource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCardsToShow = datasource.numberOfCardsToShow()
remainingcards = numberOfCardsToShow
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) {
addCardView(cardView: datasource.card(at: i), atIndex: i )
}
}
//MARK: - Configurations
private func addCardView(cardView: SwipeCardView, atIndex index: Int) {
cardView.delegate = self
addCardFrame(index: index, cardView: cardView)
cardViews.append(cardView)
insertSubview(cardView, at: 0)
remainingcards -= 1
}
func addCardFrame(index: Int, cardView: SwipeCardView) {
var cardViewFrame = bounds
let horizontalInset = (CGFloat(index) * self.horizontalInset)
let verticalInset = CGFloat(index) * self.verticalInset
cardViewFrame.size.width -= 2 * horizontalInset
cardViewFrame.origin.x += horizontalInset
cardViewFrame.origin.y += verticalInset
cardView.frame = cardViewFrame
}
private func removeAllCardViews() {
for cardView in visibleCards {
cardView.removeFromSuperview()
}
cardViews = []
}
func swipeDidEnd(on view: SwipeCardView) {
guard let datasource = dataSource else { return }
view.removeFromSuperview()
if remainingcards > 0 {
let newIndex = datasource.numberOfCardsToShow() - remainingcards
addCardView(cardView: datasource.card(at: newIndex), atIndex: 2)
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}else {
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}
}
}
According to the apple developer doc for loadView(), they said "The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property." This might be the cause of the problem. I would recommend you to perform the view set up operations in viewDidLoad or other proper lifecycle methods. Based on my understanding, this line view = UIView() isn't necessary. In your configureStackContainer() func, you set the centerX and centerY anchor and then set the top, leading, trailing, bottom anchor again. This may also raise the constraint conflicts. I think you don't need to specify centerX and centerY anchor if you want to constraint with top, leading, trailing and bottom and vice versa. I hope this will be helpful.

Not able to remove custom UIView from SuperView

This is extremely odd. I am trying to remove the view from superview when I drag the view to either left or right. If the view doesn't contain any subviews then I am easily able to remove the view from the superView by using this card.removeFromSuperview() - however, what I have noticed is that if add two views as subviews inside the card view then I am not able to remove it from superView and the entire thing goes bezerk.
Here is the card view class:
class MainSwipeCardView: UIView {
//MARK: - Properties
var swipeView = UIView()
var shadowView = UIView()
var text: String?
var label = UILabel()
var bgColor : UIColor? {
didSet {
swipeView.backgroundColor = bgColor
}
}
var cardsarraydata : CardDataModel? {
didSet {
bgColor = cardsarraydata?.backgroundColor
label.text = cardsarraydata?.title
}
}
var delegate : CardDelegate?
//MARK :- Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
configureShadowView()
configureSwipeView()
configureLabelView()
addPanGestureOnCards()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Configurations
func configureShadowView() {
shadowView.backgroundColor = .clear
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
shadowView.layer.shadowOpacity = 0.8
shadowView.layer.shadowRadius = 4.0
addSubview(shadowView)
shadowView.translatesAutoresizingMaskIntoConstraints = false
shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
func configureSwipeView() {
swipeView.layer.cornerRadius = 15
swipeView.clipsToBounds = true
shadowView.addSubview(swipeView)
swipeView.translatesAutoresizingMaskIntoConstraints = false
swipeView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
swipeView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
swipeView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
swipeView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
func configureLabelView() {
swipeView.addSubview(label)
label.backgroundColor = .white
label.textColor = .black
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18)
label.translatesAutoresizingMaskIntoConstraints = false
label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.heightAnchor.constraint(equalToConstant: 85).isActive = true
}
func addPanGestureOnCards() {
self.isUserInteractionEnabled = true
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)))
}
//MARK: - Handlers
#objc func handlePanGesture(sender: UIPanGestureRecognizer){
let card = sender.view as! MainSwipeCardView
let point = sender.translation(in: self)
let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
switch sender.state {
case .ended:
if (card.center.x) > 400 {
delegate?.swipeDidEnd(on: card)
UIView.animate(withDuration: 0.2) {
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
card.alpha = 0
self.layoutIfNeeded()
}
return
}else if card.center.x < -115 {
delegate?.swipeDidEnd(on: card)
UIView.animate(withDuration: 0.2) {
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
card.alpha = 0
self.layoutIfNeeded()
}
return
}
UIView.animate(withDuration: 0.2) {
card.transform = .identity
card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
self.layoutIfNeeded()
}
default:
break
}
}
In this subclass I have two UIViews, I am adding the views sequentially. Then on swipeView I am adding the text and label and background color. This is how the cards look like:
I am also using a UIPanInteraction on it and so if I drag it to left or right, then I call the delegate which removes the entire MainSwipeCardView from the container view. In doing so this is what happens:
It keeps adding more and more in the background even though this is what I am calling in the delegate function:
func swipeDidEnd(on card: MainSwipeCardView) {
card.removeFromSuperview()
print(visibleCards.count)
}
The visibleCards is essentially an array of subviews in the container view. It should decrease so for example from 3 -> 2 -> 1; but it increases in non linear way ( not able to really get a relationship out of it)
The most confusing thing is that I am actually able to run this whole code just fine if I donot add the SwipeView and shadowView properties inside the custom view and just use the customView itself to house the label and the backgroundColor. When I add these two properties, then this whole thing seem to go haywire.
Please any kind of help will be extremely appreciated. Thanks!
ContainerView code is as follows:
class SwipeCardContainerView: UIView, CardDelegate {
//MARK: - Properties
var numberOfCards: Int = 0
var remainingCards: Int = 0
var cardsView : [MainSwipeCardView] = []
var numberOfAllowedCard: Int = 3
let horizontalInset: CGFloat = 8.0
let verticalInset: CGFloat = 8.0
var visibleCards : [MainSwipeCardView] {
return subviews as? [MainSwipeCardView] ?? []
}
var datasource : CardDataSource? {
didSet {
loadData()
}
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Configuration
func loadData() {
guard let datasource = datasource else { return }
numberOfCards = datasource.numberOfCards()
remainingCards = numberOfCards
for i in 0..<min(numberOfCards,numberOfAllowedCard) {
addCardView(at: i, card: datasource.createCard(at: i))
}
setNeedsLayout()
}
func addCardView(at index: Int, card: MainSwipeCardView) {
card.delegate = self
addCardFrame(index: index, cardView: card)
cardsView.append(card)
insertSubview(card, at: 0)
remainingCards -= 1
}
func addCardFrame(index: Int, cardView: MainSwipeCardView){
cardsView.append(cardView)
var cardViewFrame = bounds
let horizontalInset = (CGFloat(index) * self.horizontalInset)
let verticalInset = CGFloat(index) * self.verticalInset
cardViewFrame.size.width -= 2 * horizontalInset
cardViewFrame.origin.x += horizontalInset
cardViewFrame.origin.y += verticalInset
cardView.frame = cardViewFrame
}
// Delegate Method
func swipeDidEnd(on card: MainSwipeCardView) {
card.removeFromSuperview()
print(visibleCards.count)
}
Main ViewController Code:
class ViewController: UIViewController {
//MARK: - Properties
var stackContainer : SwipeCardContainerView!
var cardDataArray : [CardDataModel] = [CardDataModel(backgroundColor: .orange, title: "Hello"),
CardDataModel(backgroundColor: .red, title: "Red"),
CardDataModel(backgroundColor: .blue, title: "Blue"),
CardDataModel(backgroundColor: .orange, title: "Orange")]
//MARK: - Init
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
stackContainer = SwipeCardContainerView()
view.addSubview(stackContainer)
configureSwipeContainerView()
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
//MARK : - Configurations
func configureSwipeContainerView() {
stackContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true
stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
stackContainer.heightAnchor.constraint(equalToConstant: 350).isActive = true
}
override func viewDidLayoutSubviews() {
stackContainer.datasource = self
}
//MARK : - Handlers
}
extension ViewController : CardDataSource {
func numberOfCards() -> Int {
return cardDataArray.count
}
func createCard(at index: Int) -> MainSwipeCardView {
let card = MainSwipeCardView()
card.cardsarraydata = cardDataArray[index]
return card
}
func emptyCard() -> UIView? {
return nil
}
}
I've investigated the problem.
First issue is in the ViewController:
override func viewDidLayoutSubviews() {
stackContainer.datasource = self
}
Just remove this code. In each layout you set datasource... and loadData... this is incorrect approach, also super.viewDidLayoutSubviews() is missing...
And also stackContainer.datasource = self:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
stackContainer = SwipeCardContainerView()
view.addSubview(stackContainer)
configureSwipeContainerView()
stackContainer.translatesAutoresizingMaskIntoConstraints = false
stackContainer.datasource = self
Second issue is in func loadData(), just replace it with
func loadData() {
guard let datasource = datasource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCards = datasource.numberOfCards()
remainingCards = numberOfCards
for i in 0..<min(numberOfCards,numberOfAllowedCard) {
addCardView(at: i, card: datasource.createCard(at: i))
}
}
or find better solution with layout of SwipeCardContainerView

Text Field cuts off first emoji when typing

I have a text field that sizes to the amount of text it has. It looks fine when you are not typing, but when you begin typing it shifts the emojis to the left and causes the first emoji to be cut off (it works fine with text).
I am not doing any manual resizing, just using sizeToFit().
Code:
override func viewDidLoad() {
super.viewDidLoad()
let emojiTextField = UITextField.makeEmojiTextField()
emojiTextField.delegate = self
view.addSubview(emojiTextField)
emojiTextField.anchorCenterSuperview()
}
class EmojiTextField: UITextField {
override var textInputMode: UITextInputMode? {
for mode in UITextInputMode.activeInputModes {
if mode.primaryLanguage == "emoji" {
return mode
}
}
return nil
}
}
extension UITextField {
static func makeEmojiTextField() -> EmojiTextField {
let tf = EmojiTextField()
tf.placeholder = ""
tf.font = UIFont.systemFont(ofSize: 64)
tf.borderStyle = UITextField.BorderStyle.none
tf.autocorrectionType = UITextAutocorrectionType.no
tf.returnKeyType = UIReturnKeyType.done
tf.contentVerticalAlignment = UIControl.ContentVerticalAlignment.center
tf.backgroundColor = .green
return tf
}
}
extension UIView {
public func anchorCenterXToSuperview(constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
if let anchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
}
}
public func anchorCenterYToSuperview(constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
if let anchor = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
}
}
public func anchorCenterSuperview() {
anchorCenterXToSuperview()
anchorCenterYToSuperview()
}
}
Emojis fit fine when not typing:
First emoji is cut off when typing:
Have you set any constraints to your text field?
if not try adding it to stack view and I believe it should work.

How do I animate the height of an input accessory view?

I am experiencing strange behavior when animating the height of an input accessory view. What am I doing wrong?
I create a UIInputView subclass (InputView) with a single subview. The height of InputView and its intrinsicContentSize are controlled by the subview. InputView is 50 pixels tall when isVisible is true and 0 pixels tall when isVisible is false.
import UIKit
class InputView: UIInputView {
private let someHeight: CGFloat = 50.0, zeroHeight: CGFloat = 0.0
private let subView = UIView()
private var hide: NSLayoutConstraint?, show: NSLayoutConstraint?
var isVisible: Bool {
get {
return show!.isActive
}
set {
// Always deactivate constraints before activating conflicting ones
if newValue == true {
hide?.isActive = false
show?.isActive = true
} else {
show?.isActive = false
hide?.isActive = true
}
}
}
// MARK: Sizing
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: size.width, height: someHeight)
}
override var intrinsicContentSize: CGSize {
return CGSize.init(width: bounds.size.width, height: subView.bounds.size.height)
}
// MARK: Initializers
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
addSubview(subView)
subView.backgroundColor = UIColor.purple
translatesAutoresizingMaskIntoConstraints = false
subView.translatesAutoresizingMaskIntoConstraints = false
subView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
show = subView.heightAnchor.constraint(equalToConstant: someHeight)
hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
hide?.isActive = true
}
}
The host view controller toggles isVisible in a one-second animation block when a button is pressed.
import UIKit
class MainViewController: UIViewController {
let testInputView = InputView.init(frame: .zero, inputViewStyle: .default)
#IBAction func button(_ sender: AnyObject) {
UIView.animate(withDuration: 1.0) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
self.testInputView.layoutIfNeeded()
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
return testInputView
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
I expect the input accessory view to smoothly grow from the botton of the screen when isVisible is set to true, and smoothly shrink to the button of the screen when isVisible is set to false. Instead, the keyboard background overlay appears at full 50-pixel height as soon as isVisible is true and the input accessory view grows from the center of its frame.
When shrinking, the input accessory view instantly loses some of its height before continuing the animation smoothly.
I created an input accessory view demonstration project that displays this unexpected behavior.
This will give you the correct animation:
UIView.animate(withDuration: 1.0) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
self.testInputView.superview?.superview?.layoutIfNeeded()
}
However, it's never a good practice to call superview if Apple changes the design. So there may be a better answer.
This is what the superviews represent:
print(testInputView.superview) // UIInputSetHostView
print(testInputView.superview?.superview) // UIInputSetContainerView
EDIT: ADDED A SAFER SOLUTION
I'm not too familiar with the UIInputView. But one way of solving it without calling the superview would be to only animate the height change of the subview:
Step 1:
Move the isVisible outside the animation block.
#IBAction func button(_ sender: AnyObject) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
UIView.animate(withDuration: 1.0) {
self.testInputView.layoutIfNeeded()
}
}
Step 2:
Create a new method in your InputView which changes the height constraint of the InputView instead of the intrinsicContentSize.
private func updateHeightConstraint(height: CGFloat) {
for constraint in constraints {
if constraint.firstAttribute == .height {
constraint.constant = height
}
}
self.layoutIfNeeded()
}
Step 3:
And call that method inside the setter.
if newValue == true {
updateHeightConstraint(height: someHeight)
hide?.isActive = false
show?.isActive = true
} else {
updateHeightConstraint(height: zeroHeight)
show?.isActive = false
hide?.isActive = true
}
Step 4:
Lastly some changes in the init.
override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
addSubview(subView)
backgroundColor = .clear
subView.backgroundColor = UIColor.purple
subView.translatesAutoresizingMaskIntoConstraints = false
subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
show = subView.heightAnchor.constraint(equalToConstant: someHeight)
hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
hide?.isActive = true
}
Conclusion:
This result in the InputView changes it's height before animating the height of the purple subview. The only downside is the UIInputView, which has some kind of gray background as default and cannot be changed to Clear. However, you can use the same backgroundColor as the VC.
But if you instead should go with a regular UIView as InputAccessoryView it will be UIColor.clear as default. Than the first "jump" will not be noticed.

Animate UIView on a navigationBar in Swift

I´m trying to make an status banner animate on top of a navigationBar.
The animation works fine if I add the banner on the viewControllers view, but will end up behind the navigation bar. If I add the banner to the navigation bar, the banner "pop" on the navigationBar, but will not preform animation. Same problem if I add the banner view to the keyWindow.
I also tried to manipulate the banner view and the navigationBars layers zPosition without any luck.
Anyone hav an idea?
Here is my code...
import UIKit
class BellaBannerView : UIView {
var view : UIView!
var style : BannerStyle!
var position : BannerTopAnchor!
var bannerView : UIView!
var messageLabel : UILabel!
var dissmissButton : UIButton!
var offsetConstraintConstant : CGFloat!
var testconst : NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
}
convenience init(view: ViewController, style : BannerStyle, pos : BannerTopAnchor) {
self.init(frame: CGRect.zero)
self.view = view.view
self.style = style
self.position = pos
self.offsetConstraintConstant = style.bannerSize()
self.translatesAutoresizingMaskIntoConstraints = false
self.clipsToBounds = true
if let isNavigation = view.navigationController?.view {
isNavigation.addSubview(self)
} else {
self.view.addSubview(self)
}
initViews(style: style, pos: pos)
addSubview(bannerView)
bannerView.addSubview(dissmissButton)
bannerView.addSubview(messageLabel)
setConstraints(view: view, pos: pos)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func initViews(style : BannerStyle, pos : BannerTopAnchor) {
bannerView = {
let view = UIView()
view.backgroundColor = style.backgroundColor()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
dissmissButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.backgroundColor = style.backgroundColor()
btn.setImage(UIImage(named: "dissmiss"), for: .normal)
btn.addTarget(self, action: #selector(hide), for: .allTouchEvents)
return btn
}()
messageLabel = {
let label = UILabel()
label.text = "BESKJED beskjed Beskjed"
label.textColor = .lightGray
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
}
func show() {
UIView.animate(withDuration: 0.5, animations: { () -> Void in
self.testconst.constant = 0
self.view.layoutIfNeeded()
})
}
func hide() {
UIView.animate(withDuration: 0.5, animations: { () -> Void in
self.testconst.constant = -self.offsetConstraintConstant
self.view.layoutIfNeeded()
})
}
func setConstraints(view : ViewController, pos : BannerTopAnchor) {
self.heightAnchor.constraint(equalToConstant: style.bannerSize()).isActive = true
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
self.topAnchor.constraint(equalTo: self.view.topAnchor, constant: pos.topPosition()).isActive = true
self.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
bannerView.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
bannerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
bannerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
self.testconst = bannerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -self.offsetConstraintConstant)
self.testconst.isActive = true
dissmissButton.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor).isActive = true
dissmissButton.trailingAnchor.constraint(equalTo: bannerView.trailingAnchor).isActive = true
dissmissButton.widthAnchor.constraint(equalToConstant: style.buttonSize()).isActive = true
dissmissButton.heightAnchor.constraint(equalToConstant: style.buttonSize()).isActive = true
messageLabel.leadingAnchor.constraint(equalTo: bannerView.leadingAnchor, constant: style.buttonSize()).isActive = true
messageLabel.trailingAnchor.constraint(equalTo: bannerView.trailingAnchor, constant: -style.buttonSize()).isActive = true
messageLabel.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor).isActive = true
messageLabel.heightAnchor.constraint(equalToConstant: style.bannerSize()).isActive = true
}
enum BannerTopAnchor {
case top
case statusBar
case navbar
func topPosition() -> CGFloat {
switch self {
case .top:
return 0
case .statusBar:
return UIApplication.shared.statusBarFrame.height
case .navbar:
return 64
}
}
}
enum BannerStyle {
case success
case info
case offline
case online
func backgroundColor() -> UIColor {
switch self {
case .success, .online:
return UIColor.green//.bellaSuccessBannerBackground()
case .info:
return UIColor.blue//.bellaDarkishBlueColor()
case .offline:
return UIColor.red//.bellaLipstickColor()
}
}
func bannerSize() -> CGFloat {
switch self {
case .info:
return 50
case .online, .offline, .success:
return 25
}
}
func buttonSize() -> CGFloat {
switch self {
case .info:
return 50
default:
return 0
}
}
func textColor() -> UIColor {
switch self {
case .success, .online:
return UIColor.green//bellaDarkishGreenColor()
case .info:
return UIColor.brown//bellaBeigeColor()
case .offline:
return UIColor.white
}
}
func dismissable() -> Bool {
switch self {
case .success, .offline, .online:
return false
case .info:
return true
}
}
}
}
Problem fixed. Updated layout before starting the animation
func show() {
self.layoutIfNeeded()
self.testconst.constant = 0
UIView.animate(withDuration: 0.5, animations: { () -> Void in
self.layoutIfNeeded()
})
}
func hide() {
self.layoutIfNeeded()
self.testconst.constant = -self.offsetConstraintConstant
UIView.animate(withDuration: 0.5, animations: { () -> Void in
self.layoutIfNeeded()
})
}
So the problem here is that the navigation controller take your view controller's view and adds it to it's content view, which is behind the navigation bar. You need to add your banner to something that is above the navigation bar.
You have a few options here, you can add it to the navigation controllers view directly:
self.navigationController?.view.addSubview(bannerView)
this should work fine, though I am always hesitant to play with a UINavigationController view hierarchy manually, as I always see it as a manager hierarchy.
In my opinion a better approach would be to add it straight to the window, which will have the added benefit of making sure it is on top of everything:
self.view.window?.addSubview(bannerView)
this accesses the root window that self's view is in, and adds your banner to that, on top of everything.

Resources