I have a custom keyboard for a UITextField using the inputView. I can switch over to the native keyboard again, at which point the height of the keyboard can change (e.g. swiping up/down on autosuggest, changing keyboards). This is why my custom keyboard inputView needs to be able to change its height.
However, when the frame height of the inputView is change, it magically resets itself again!
(I'm doing this on iOS 8.4 simulator.)
Simplifying the problem to the most basic example:
func createInputView(#height:CGFloat) -> UIView {
let view = ViewSubclass(frame: CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), height))
view.backgroundColor = UIColor.redColor()
let label = UILabel(frame: CGRectMake(0, 0, 100, 30))
label.backgroundColor = UIColor.yellowColor()
label.text = "Hello!"
view.addSubview(label)
return view
}
// halves the view's frame height
func changeFrameHeightForView(view: UIView) {
view.frame = {
var f = view.frame
f.size.height = f.size.height / 2.0
return f
}()
}
override func viewDidLoad() {
super.viewDidLoad()
view1 = createInputView(height: 200)
view2 = createInputView(height: 150)
myTextField.inputView = view1
}
// to reproduce this scenario, you would first focus on the text field to open the keyboard, and then tap on the controller's view, which triggers the below action:
// example 1
#IBAction func viewTapped(sender: AnyObject) {
let currentInputView = self.myTextField.inputView!
println("starting height: \(currentInputView.frame.height)")
// starting height: 200.0
changeFrameHeightForView(currentInputView)
println("height after frame changed: \(currentInputView.frame.height)")
// height after frame changed: 100.0
self.myTextField.reloadInputViews()
println("height after reloadInputViews: \(currentInputView.frame.height)")
// height after reloadInputViews: 100.0
// if I stopped here, the view will update and be shorter, but it shows a grey area underneath - the keyboard itself hasn't changed height! So try to dismiss the keyboard and reload it again...
myTextField.resignFirstResponder()
println("height after resign first responder: \(currentInputView.frame.height)")
// height after resign first responder: 200.0 // --> HOW DID IT GET REVERTED???
myTextField.becomeFirstResponder()
println("height after become first responder: \(currentInputView.frame.height)")
// height after become first responder: 200.0
}
So the question for example 1 is:
Why did the frames get reverted as soon as resignFirstResponder gets called?
I start thinking, maybe the text field is doing something to the inputView. So what if I nil it out. Then it shouldn't be able to affect the original view right? This is where it gets weird...
// example 2
#IBAction func viewTapped(sender: AnyObject) {
let currentInputView = self.myTextField.inputView!
println("starting height: \(currentInputView.frame.height)")
// starting height: 200.0
// remove the inputView and close the keyboard.
self.myTextField.inputView = nil
self.myTextField.reloadInputViews()
myTextField.resignFirstResponder()
// change the frame height
changeFrameHeightForView(currentInputView)
println("height after frame changed: \(currentInputView.frame.height)")
// height after frame changed: 100.0
// set it as the inputView again
self.myTextField.inputView = currentInputView
self.myTextField.reloadInputViews()
println("height after reloadInputViews: \(currentInputView.frame.height)")
// height after reloadInputViews: 100.0
// open the keyboard
myTextField.becomeFirstResponder()
println("height after become first responder: \(currentInputView.frame.height)")
// height after become first responder: 200.0 // --> HOW DID IT GET REVERTED??? Shouldn't the keyboard use the new frame height?
}
Moving onto example 3...
Those with a keen eye will notice i have 2 input views set up. Here I will toggle the inputView between them. Interestingly, if a different view object gets assigned, it all works as expected:
// example 3
#IBAction func viewTapped(sender: AnyObject) {
let currentInputView = self.myTextField.inputView!
let otherView = (currentInputView == self.view1 ? self.view2 : self.view1)
self.myTextField.inputView = otherView
self.myTextField.reloadInputViews()
}
Apologies for the long question, but I've documented my findings in this investigation. I don't really want to create a brand new view every time the keyboard changes height.
Facebook messenger's custom keyboard is able to do something similar to this, so I'm hoping someone can shed some light on this.
thanks!
Related
I'm trying to use a custom view as an accessory view over the keyboard, for various reasons, in this case, it is much preferred over manual keyboard aligning because of some other features.
Unfortunately, this is a dynamic view that defines its own height. The constraints all work fine outside of the context of an accessoryView without errors, and properly resizing
When added as a keyboardAccessoryView it seems to impose a height of whatever the frame is at the time and break other height constraints
It appears as:
"<NSLayoutConstraint:0x600003e682d0 '_UIKBAutolayoutHeightConstraint' Turntable.ChatInput:0x7fb629c15050.height == 0 (active)>"
(where 0 would correspond to whatever height had been used at initialization
It is also labeled accessoryHeight which should make it easy to remove, but unfortunately, before I can do this, I'm getting unsatisfiable constraints and the system is tossing my height constraints
Tried:
in the inputAccessoryView override, I tried to check for the constraints and remove it, but it doesn't exist at this time
setting translatesAutoresizing...Constraints = false
tl;dr
Using a view as a KeyboardAccessoryView is adding its own height constraint after the fact, can I remove this?
Looks like keyboard doesn't like inputAccessoryView with height constraint. However you still can have inputAccessoryView with dynamic height by using frame (it is still possible to use constraints inside your custom inputAccessoryView).
Please check this example:
import UIKit
final class ViewController: UIViewController {
private let textField: UITextField = {
let view = UITextField()
view.frame = .init(x: 100, y: 100, width: 200, height: 40)
view.borderStyle = .line
return view
}()
private let customView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.frame.size.height = 100
view.autoresizingMask = .flexibleHeight // without this line height won't change
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(textField)
textField.inputAccessoryView = customView
textField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.customView.frame.size.height = 50
self.textField.reloadInputViews()
}
}
}
I have a UIView .xib, and am subclassing UIView. When I load the Nib, the frame of the view is set and awakeFromNib() is called.
I have 3 buttons. When I load the view, I pass in callbacks for each button. If one or more of the callbacks is nil, then I hide the buttons and resize the view:
let viewSize = 50
var adjustedSize = 0
if (self.option2String == nil){
self.option2View.isHidden = true
adjustedSize -= viewSize
}
if (self.option3String == nil){
self.option3View.isHidden = true
adjustedSize -= viewSize
}
let _size = self.innerView.frame.size
let size = CGSize(width: _size.width, height: _size.height + CGFloat(adjustedSize))
self.innerView.frame = CGRect(origin: self.innerView.frame.origin, size: size)
I have tried putting this code in awakeFromNib(), and didMoveToSuperview(), but the frame does not change size.
If I enclose the last line in DispatchQueue.main.async, then it works. But I'm concerned that this is just luck due to timing.
Is this best practice? Where can I resize a view from within a UIView subclass?
EDIT: Confirmed, the DispatchQueue.main.async is just luck. It only works 50% of the time.
Having the view inside a UIViewController, call a function that resizes your view inside the controller's viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
myView.resize()
}
I have a "classic app" with 3 ViewController and a tabBar that I use to change ViewController.
On my first ViewController, I have a button that display a UIView on all the screen, so I hide tabBar with this setTabBarVisible func :
extension UIViewController
{
func setTabBarVisible(visible: Bool, animated: Bool)
{
//* This cannot be called before viewDidLayoutSubviews(), because the frame is not set before this time
// bail if the current state matches the desired state
if (isTabBarVisible == visible) { return }
// get a frame calculation ready
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
let offsetY = (visible ? -height! : height)
// zero duration means no animation
let duration: TimeInterval = (animated ? 0.3 : 0.0)
// animate the tabBar
if frame != nil
{
UIView.animate(withDuration: duration)
{
self.tabBarController?.tabBar.frame = frame!.offsetBy(dx: 0, dy: offsetY!)
return
}
}
}
var isTabBarVisible: Bool
{
return (self.tabBarController?.tabBar.frame.origin.y ?? 0) < self.view.frame.maxY
}
}
That's working, the tabBar is hidden and I see all my UIVIew.
The problem is, I have a UILabel at bottom of the UIView (at the place I usually display the tabBar), and I can't use my TapGesture on my UILabel, nothing is happening.
(if I display the label somewhere else the UITapGesture works good.)
I tried to set zPosition of my tabBar to 0 and zPosition of my UIView to 1 but that's doesn't work either.
How can I get my label clickable at bottom of my view?
Check UILabel.isUserInterration = enable
Make sure that when you hide tabbar, perticular view controller Under bottom bar property is unselect . See atteh imnage.
You can try with programatically also like
ViewController.edgesForExtendedLayout = UIRectEdge.top
make sure is true
label.isUserInteractionEnabled = true
Please refer this link may it help you.
Programmatically i have created a View and added a label and buttons to it. its fine in vertical it aligns to centre but as i rotate the screen it does not aligns to centre rather it seems as left aligned.
This is my code:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 150))
headerView.layer.borderWidth = 1
headerView.layer.borderColor = UIColor.whiteColor().CGColor
headerView.backgroundColor = ClientConfiguration.primaryUIColor()
let myLabel = UILabel()
myLabel.frame = CGRectMake(-1, -1, tableView.frame.width, 30)
myLabel.font = UIFont.boldSystemFontOfSize(14)
myLabel.backgroundColor = ClientConfiguration.primaryUIColor()
myLabel.textColor = UIColor.whiteColor()
myLabel.text = "Select Time Zone"
myLabel.textAlignment = .Center
let frame = CGRectMake(1, 1,headerView.frame.width , 70)
self.btnTimeZone.frame = frame
headerView.addSubview(myLabel)
headerView.addSubview(self.btnTimeZone)
self.buttonTitileString = self.selectedZone.value
self.btnTimeZone.setTitle(buttonTitileString, forState: .Normal)
return headerView
}
In horizontal mode the button text and label are aligned to left
and when i set self.btnTimeZone.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Center
its fine in horizontal mode but in vertical mode they all are right aligned as;
enter image description here
How can i solve this issue i need both of them in centre aligned in both horizontal and vertical mode.
You just need to reload the table when rotate the phone. Implement following method and inside of this method reload the tableView
override func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
tableView.reloadData()
}
In case of the application should support both portrait and landscape modes, the good approach would be to work with NSLayoutConstraint.
Of course you have both options to create them programmatically or from the Interface Builder, I'd like to note that you are able to create the header view as a cell, without the need of doing it programmatically; That's -obviously- will leads to the ease of setup the desired constrains for the header subviews.
Try this also
change
tableView.frame.width
to
tableView.frame.size.width
You could try to set autoresizing for parent view.
headerView.autoresizesSubviews = true
myLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.btnTimeZone.autoresizingMask = [.flexibleWidth, .flexibleHeight]
In Swift 5 (and 3, 4 too) you got to override viewDidLayoutSubviews() method of life cycle of view controller, it calls after viewWillTransition() method when rotating your device.
In viewDidLayoutSubviews() method you just need to reload the data of your tableView in order to redraw your section header for portrait to landscape or landscape to portrait, and it works perfectly.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView.reloadData()
}
Thanks to GayashanK for give me this idea with his answer.
I have a UIView that is will slide down on button tap and then slide back up on another button tap. It slides down perfectly, but when I try to hit the contract button, I can't click it, but it does click a collection view cell that is underneath the view. I can't figure out why it slides down properly but it doesn't function how I expect. Thanks a lot!
#IBAction func onExpandButtonPressed(sender: UIButton) {
let xPosition = expandingView.frame.origin.x
//View will slide to 40 px higher bottom of the screen
let yPosition = expandingView.frame.maxY - 40
let height = expandingView.frame.size.height
let width = expandingView.frame.size.width
UIView.animateWithDuration(0.5, animations: {
self.expandingView.frame = CGRectMake(xPosition, yPosition, height, width)
self.expandButton.hidden = true
self.contractButton.hidden = false
})
}
#IBAction func onContractButtonPressed(sender: UIButton) {
let xPosition = expandingView.frame.origin.x
let yPosition = expandingView.frame.origin.y
let height = expandingView.frame.size.height
let width = expandingView.frame.size.width
UIView.animateWithDuration(1.0, animations: {
self.expandingView.frame = CGRectMake(xPosition, yPosition, height, width)
self.expandButton.hidden = false
self.contractButton.hidden = true
})
}
Perhaps it's because the view has slid over the button. So try to use the following inside the button action that will slide over the non-working button:
self.view.bringSubViewToFront(buttonName)
If you put an NSLog inside your button Action, it can help determine if the function is being called.