Swift - resignFirstResponder resets contentOffset on my UITextView. How come? - swift

So I have a pretty simple app I am trying to build. Its basically a scrollable text field that fills up the entire screen.
When the user taps the screen, the keyboard appears and you can begin to edit on the line where you tapped.
If you tap in the area where the keyboard is going to appear, the size of the textView shrinks so you aren't entering text behind the keyboard.
Everything up until here works. Ill put the code at the bottom.
When the user is done editing, there is a 'Done' button in the upper right hand of the screen, When they press that, the keyboard should disappear, and, if the amount of text is more than what fits on the screen, whichever line they were editing should be at the bottom of the screen.
Right now, no matter what I try, when I resignFirstResponder to hide the keyboard, the textView resets the contentOffset to (0,0)
When I am in the above view, I press done, and this is the result:
what I would like to happen is where I was editing to be at the bottom of the screen, like this:
Class variables so they can be accessed from anywhere in the file
var textField: UITextView = UITextView()
var withKeyboard: NSLayoutConstraint!
var withoutKeyboard: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textField)
textField.scrollEnabled = true
textField.selectable = true
textField.bounces = true
textField.contentMode = UIViewContentMode.Bottom
textField.setTranslatesAutoresizingMaskIntoConstraints(false)
self.withoutKeyboard = NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)
self.view.addConstraint(NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0))
self.view.addConstraint(self.withoutKeyboard)
self.view.addConstraint(NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Left, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Right, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 0))
}
func keyboardWillShow(notification: NSNotification){
doneButton.hidden = false
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
keyboardHeight = keyboardSize.height
self.withKeyboard = NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: -keyboardHeight)
self.view.removeConstraint(withoutKeyboard)
self.view.addConstraint(withKeyboard)
textField.layoutIfNeeded()
}
}
func keyboardWillHide(notification: NSNotification){
self.doneButton.hidden = true
self.textFieldOffset = self.textField.contentOffset.y - self.keyboardHeight
println(self.textFieldOffset)
self.view.removeConstraint(withKeyboard)
self.view.addConstraint(withoutKeyboard)
textField.layoutIfNeeded()
textField.contentOffset.y = self.textFieldOffset
}
func donePressed(){
textField.resignFirstResponder()
createRecord()
}
I made the withKeyboard and withoutKeyboard constraints so that I could take one off and add the other one whenever the keyboard appears/disappears.
Anyway, when I hit the done button, it resets the view to the very top. This isnt all of the code, its just the parts that are giving me trouble.

Related

Constraining Text Field to Button in Swift

I have a button I created in my storyboard, but have a custom UITextField that needs to be implemented in code, I am trying to place the text field so that it's 20 pixels above the button, but I am not getting the desired result for some reason. Am I missing something or is this not how you implement it? here's my implementation and output:
#IBOutlet weak var buyPointsButton: UIButton!
lazy var cardTextField: STPPaymentCardTextField = {
let cardTextField = STPPaymentCardTextField()
return cardTextField
}()
In viewDidLoad
self.view.addSubview(cardTextField)
cardTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
NSLayoutConstraint(item: cardTextField, attribute: .bottom, relatedBy: .equal, toItem: buyPointsButton, attribute: .top, multiplier: 1, constant: 70),
NSLayoutConstraint(item: cardTextField, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: buyPointsButton.frame.width),
NSLayoutConstraint(item: cardTextField, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: cardTextField, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 28)
])
The main problem is that you are setting the text field's .centerY to the view's .centerY and you're constraining the Bottom of the text field to the Top of the button...
You'll find it much easier to work with constraints if you use the more "modern" syntax, and include comments telling yourself what the constraints are - or at least, should be - doing.
For example:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(cardTextField)
cardTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// Bottom of text field 20-points above Top of button
cardTextField.bottomAnchor.constraint(equalTo: buyPointsButton.topAnchor, constant: -20.0),
// text field Width equal to button Width
cardTextField.widthAnchor.constraint(equalTo: buyPointsButton.widthAnchor),
// text field centered horizontally to button
cardTextField.centerXAnchor.constraint(equalTo: buyPointsButton.centerXAnchor),
// text field Height equal to 28
cardTextField.heightAnchor.constraint(equalToConstant: 28.0),
])
}

Custom View in Cocoa can't draw after setting AutoLayout Constraints

On a MacOS Project.
I'm trying to draw a line under a text field (so that by making the text field transparent I can create a "please fill in the blank" style of view that asks the user to input the data). I created a simple subclass of NSView called lineDrawer just to draw the line, then I tried to add an instance of a lineDrawer as a subview to the view where I let the user enter his name. The line should be drawn right below the transparent text field. The text field is an IBOutlet of the view and already in correct position, and I want the line to have the exact same frame as the text field, so that if I draw a path from the lineDrawer's (minX, minY)to (maxX, minY), the path is exactly below the transparent text field.
I can't make it work because after I set
lineView.translatesAutoresizingMaskIntoConstraints = false
and added my own constraints, the custom view wouldn't draw. I think it has something to do with me not giving the program enough information about where exactly to draw the view, but I can't figure out what's lacking here. BTW, if I don't set the constraints, it would just draw the view at its frame's place. If I set the constraints, nothing shows.
The NickName: NSViewController
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
greetingLine.font = .labelFont(ofSize: 50)
nameField.font = .labelFont(ofSize: 50)
nameField.isBezeled = false
let lineView = lineDrawer.init()
lineView.setFrameSize(nameField.frame.size)
self.view.addSubview(lineView)
lineView.translatesAutoresizingMaskIntoConstraints = false
let lineConstraintX = NSLayoutConstraint.init(item: lineView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let lineConstraintY = NSLayoutConstraint.init(item: lineView, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1.3, constant: 0)
self.view.addConstraints([lineConstraintY, lineConstraintX])
}
#IBOutlet weak var nameField: NSTextField!
#IBOutlet weak var greetingLine: NSTextField!
and the lineDrawer: NSView:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
let context = NSGraphicsContext.current?.cgContext
context!.beginPath()
context!.move(to: CGPoint(x: self.bounds.minX, y: self.bounds.minY))
let endPoint = CGPoint.init(x: self.bounds.maxX, y: self.bounds.minY)
context!.addLine(to: endPoint)
context!.move(to: CGPoint(x: 0, y: 0))
context!.addLine(to: CGPoint(x: 10, y: 10))
let loadedColor = ColorGetter.getCurrentThemeColor()
context!.setStrokeColor(loadedColor.cgColor)
context!.setLineWidth(5)
context!.strokePath()
print("drawing")
}
Thanks very much in advance to anyone who would offer help on this!
Can you, please, add behind your layout constraint the variable isActive and set it to be true. Then ist should work.
So, the code you have:
let lineConstraintX = NSLayoutConstraint.init(item: lineView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let lineConstraintY = NSLayoutConstraint.init(item: lineView, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1.3, constant: 0)
Must be changed in:
NSLayoutConstraint.init(item: lineView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint.init(item: lineView, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1.3, constant: 0).isActive = true
Then the auto layout constraint is activated and works. Currently, you did set the auto layout constraint, but you didn't activate it.
Answering my own question:
I added 2 constraints to the custom view's width and height by
NSLayoutConstraint.init(item: lineView, attribute: .width, relatedBy: .equal, toItem: self.nameField, attribute: .width, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint.init(item: lineView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
I think by making
lineView.translatesAutoresizingMaskIntoConstraints = false
I was removing the program's knowledge about both the custom view's position and size, and so I had to re-specify them both manually?
Weird that the program doesn't imply what the size of the custom view should be from its frame's size, which was set.

Changing position of UIProgressView?

I added a progress bar to my screen. I want it to be centered horizontally in my container, but I want to move it to the bottom of my screen. How do I edit the third line to change its position?
func addControls() {
progressView = UIProgressView(progressViewStyle: UIProgressViewStyle.Default)
progressView?.center = self.view.center
view.addSubview(progressView!)
}
You could use NSLayoutConstraints and do something like this. The 3rd line being where you put the progress bar on top or beneath the other object.
var otherObject = UIView()
self.view.addSubview(progressView)
let topConstraint = NSLayoutConstraint(item: progressView, attribute: .topMargin, relatedBy: .equal, toItem: otherObject, attribute: .bottomMargin, multiplier: 1, constant: 0)
let leftConstraint = NSLayoutConstraint(item: progressView, attribute: .leftMargin, relatedBy: .equal, toItem: self.view, attribute: .leftMargin, multiplier: 1, constant: 0)
let rightConstraint = NSLayoutConstraint(item: progressView, attribute: .rightMargin, relatedBy: .equal, toItem: self.view, attribute: .rightMargin, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: progressView, attribute: .bottomMargin, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50)
self.view.addConstraints([topConstraint, leftConstraint, rightConstraint, bottomConstraint])
That's really simple.
If you want to add a subview above another one, use this function:
func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView)
To add a subview below another one, use this function:
func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView)

How to put a uiview back in a stackview after I took it out?

I have a viewcontroller that holds multiple stackviews. There is a button that when pressed, the corresponding uiview wil become fullscreen inside the original view. There is a different button that is supposed to make the uiview go back to its original stackview. The uiview itself contains other views. I am having some problems doing that. The uiview does end up in the stackview, but not near the same size/place it used to be. I am not sure how to solve this, and been going at it for several hours now, looking at multiple sources.
This is the code that makes the uiview go fullscreen:
private func moveToFrontOfCardView(v: UIView) {
originalView = v.superview
if let stack = originalView as? UIStackView {
stack.removeArrangedSubview(v)
}
myCardView.addSubview(v)
let topConstraint = NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: myCardView, attribute: .top, multiplier: 1, constant: 10)
let bottomConstraint = NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: myCardView, attribute: .bottom, multiplier: 1, constant: -10)
let leftConstraint = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: myCardView, attribute: .left, multiplier: 1, constant: 10)
let rightConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: myCardView, attribute: .right, multiplier: 1, constant: -10)
myCardView.addConstraints([topConstraint, bottomConstraint, leftConstraint, rightConstraint])
}
And this is the code I use when I want it to go back:
private func moveToOriginalPosition(v: UIView) {
if let stack = originalView as? UIStackView {
stack.addArrangedSubview(v)
}
}
Does anyone have a clue how I could fix this?
EDIT
I've tried Saqib and Bilals answer, but I get this as a result:
Declare a class variable for tracking view's index
var selectedIndex = 0 // Contains Current Seleceted view's index
overrie func viewDidLoad() { ...
Before removing view from stackview get the view index like this selectedIndex = stack.subviews.index(of: v)
keep reference to all the constraints.
Before adding it back disable all the constraints topConstraint.isActive = false
Now add the view at the same index using stack.insertArrangedSubview(view, at: selectedIndex)
An other option is to create a same new view and just hide/unhide the one in stackview. StackView automatically fills the space accordingly for the hidden views.
You should deActivate the constraints you added to view when removed it from stackView, at the time you want add the view to the stackView again.
For this you should make the constraints instance of your viewController class and next, write your moveToOriginalPosition(v: UIView) method like this:
private func moveToOriginalPosition(v: UIView) {
if let stack = originalView as? UIStackView {
stack.addArrangedSubview(v)
topConstraint.isActive = false
bottomConstraint.isActive = false
leftConstraint.isActive = false
rightConstraint.isActive = false
}
}
Ofcourse, you should remove, these lines of codes from moveToFrontOfCardView(v: UIView) method:
self.topConstraint = NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: myCardView, attribute: .top, multiplier: 1, constant: 10)
self.bottomConstraint = NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: myCardView, attribute: .bottom, multiplier: 1, constant: -10)
self.leftConstraint = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: myCardView, attribute: .left, multiplier: 1, constant: 10)
self.rightConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: myCardView, attribute: .right, multiplier: 1, constant: -10)
myCardView.addConstraints([topConstraint, bottomConstraint, leftConstraint, rightConstraint])
and add them where you make your view initialized. and replace below lines with above lines in moveToFrontOfCardView(v: UIView) method:
topConstraint.isActive = true
bottomConstraint.isActive = true
leftConstraint.isActive = true
rightConstraint.isActive = true
By the looks of things you don't need to remove the original view. You could make a copy of it then display the copy full screen. Then when you dismiss this copy you release the reference to it

Swift auto layout programmatically and dynamic

I have a View where I load buttons dynamically. So I have a for loop to loop through all buttons. Since this is dynamically I want to create the auto layout programmatically. Right now I have the following code:
for var i = 0; i < data.count; i++ {
let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
button.backgroundColor = UIColor.greenColor()
button.setTitle("Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(button)
let centerXConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0)
let centerYConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant:15)
let widthConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 200)
let heightConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant:100)
scrollView.addConstraints([centerXConstraint, centerYConstraint, widthConstraint, heightConstraint])
}
This creates the first button and places it 15px under the top bar. The problem I have is how to place the next button 15px under the first one, the third button 15px under the second one etc. Anyone got any ideas?
Thats definitely possible, but firstly, I should mention it's not required that you add constraints for the buttons' width and height since they have an intrinsic content size (like UILabel) which depends on attributes such their text and font.
Back to your problem!
Here's the code, the explanation for each step is below:
override func viewDidLoad() {
super.viewDidLoad()
// 1.
var upperView: UIView = scrollView
for i in 0..<data.count {
let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
button.backgroundColor = UIColor.greenColor()
button.setTitle("Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
// 2.
scrollView.addSubview(button)
// 3.
let attribute: NSLayoutAttribute = i == 0 ? .Top : .Bottom
// 4.
let topEdgeConstraint = NSLayoutConstraint(item: button,
attribute: .Top,
relatedBy: .Equal,
toItem: upperView,
attribute: attribute,
multiplier: 1.0,
constant: 15.0)
let centerXConstraint = NSLayoutConstraint(item: button,
attribute: .CenterX,
relatedBy: .Equal,
toItem: scrollView,
attribute: .CenterX,
multiplier: 1.0,
constant: 0.0)
scrollView.addConstraint(topEdgeConstraint)
scrollView.addConstraint(centerXConstraint)
// 5.
if i == data.count - 1 {
let bottomConstraint = NSLayoutConstraint(item: button,
attribute: .Bottom,
relatedBy: .Equal,
toItem: scrollView,
attribute: .Bottom,
multiplier: 1.0,
constant: -15.0)
scrollView.addConstraint(bottomConstraint)
}
upperView = button
}
}
1. upperView is used to keep track of the view 'above' the current button. For example, when the first button is created, the upperView is the UIScrollView For the second button, upperView is the first button; for the third button, upperView is the second button and so on...
2. Your buttons should be added to the UIScrollView, not the self.view. Otherwise you'll get the error:
The view hierarchy is not prepared for the constraint...
3. This line selects the attribute on the upperView that will relate the button to the upperView. Here's a picture to demonstrate what I mean:
a) The .Top of the button is related to the .Top of the UIScrollView.
b) The .Top of the button is related to the .Bottom of the previous UIButton.
4. Making the top and centre X constraints - that's all pretty self explanatory.
5. For the UIScrollView to correctly calculate its contentSize it must have constraints in an unbroken chain from it top to bottom (the top to the bottom, in this case, because it needs to scroll vertically). Therefore, if it's the last UIButton a constraint from its bottom edge is added to the UIScrollView's bottom edge.
Hope that helps!