Related
I have UIScrollView which has a UIView (basically a content view) in it. In contentView, I have UITextFields and a UIButton. At first, elements are inside UIScrollView directly. When keyboard opens, If UITextField is below keyboard, scrollview scrolls to show the content. After, I put them inside a ContentView, It started nothing to work. What am I doing wrong?
I searched below posts but not sure why my code isn't working.
How do I scroll the UIScrollView when the keyboard appears?
P.S: I know there are lots of examples in the community but I'm not sure which will be best approach. When this one was working, It was a simple and light answer but not sure why not It isn't working.
#objc override func keyboardWillShow(notification: NSNotification){
//Need to calculate keyboard exact size due to Apple suggestions
if let frameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
keyboardHeight = frameValue.cgRectValue.size.height
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardHeight, right: 0.0)
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
}
}
How I Give Constraints:
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(scrollView)
contentView.addSubview(TextField1)
contentView.addSubview(TextField2)
contentView.addSubview(button)
scrollView.anchor(header.bottomAnchor, left: self.view.leftAnchor, bottom: self.view.bottomAnchor, right: self.view.rightAnchor, topConstant: 20, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
TextField1.anchor(contentView.topAnchor, left: self.contentView.leftAnchor, bottom: nil, right: self.contentView.rightAnchor, topConstant: 5, leftConstant: 25, bottomConstant: 0, rightConstant: 25, widthConstant: 0, heightConstant: 70)
TextField1.widthAnchor.constraint(equalTo: self.view.widthAnchor, constant: -50).isActive = true
TextField2.anchor(TextField1.bottomAnchor, left: self.contentView.leftAnchor, bottom: nil, right: self.contentView.rightAnchor, topConstant: 5, leftConstant: 25, bottomConstant: 0, rightConstant: 25, widthConstant: 0, heightConstant: 70)
Button.anchor(TextField2.bottomAnchor, left: self.contentView.leftAnchor, bottom: self.contentView.bottomAnchor, right: self.contentView.rightAnchor, topConstant: 30, leftConstant: 25, bottomConstant: 20, rightConstant: 25, widthConstant: 0, heightConstant: 40)
EDIT: Another approach I tried (which Apple recommended). This aRect.Contains always returns true.
if let frameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
keyboardHeight = frameValue.cgRectValue.size.height
var contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardHeight, right: 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
// Your application might not need or want this behavior.
//CGRect(x: 0, y: self.header.frame.maxY, width: self.view.frame.width, height: self.view.frame.height - self.header.frame.maxY)
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardHeight
aRect.size.height -= header.frame.maxY
if !aRect.contains(activeTextField.frame.origin) {
var scrollPoint = CGPoint(x: 0.0, y: activeTextField.frame.origin.y - (keyboardHeight))
scrollView.setContentOffset(scrollPoint, animated: true)
}
}
The simple way to achieve this is by using IQKeyboardManager library.
Checkout the link: https://github.com/hackiftekhar/IQKeyboardManager
After an hour work, I find the solution. I'm sharing the answer instead of deleting the post cuz my problem was because UIScrollView doesn't content whole screen. There is an header and a label above UIScrollView . All of the answers in the community, target If UIScroll content whole screen.
#objc override func keyboardWillShow(notification: NSNotification){
//Need to calculate keyboard exact size due to Apple suggestions
if let frameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
keyboardHeight = frameValue.cgRectValue.size.height
let scrollViewRect: CGRect = view.convert(scrollView.frame, from: scrollView.superview)
let hiddenScrollViewRect: CGRect = scrollViewRect.intersection(frameValue.cgRectValue)
// Figure out where the two frames overlap, and set the content offset of the scrollview appropriately
let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: hiddenScrollViewRect.size.height, right: 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
var visibleRect: CGRect = activeTextField.frame
visibleRect = scrollView.convert(visibleRect, from: activeTextField.superview)
visibleRect = visibleRect.insetBy(dx: 0.0, dy: -5.0)
scrollView.scrollRectToVisible(visibleRect, animated: true)
}
I have a UIViewController? SingleEventController displaying Events. Because it has dynamic information to show, I chose to create a new class UIScrollView EventScrollView. I tried to realize that based on this very well explained
Answer.
I tried to apply the Pure Auto Layout Approach: Create another UIView contentView in it, which contains all the information and is anchored to all four anchors of the scrollView.
The contentSize of the scrollView is determined in the viewDidLayoutSubviews function in the SingleEventController.
My struggle is, that the rightAnchor seems to be faulty. All the information-Elements go over the border of the view, the word-wrapping for the larger UILabels does not work, and every Item layed out at the right anchor is gone. (For example the usernames in the UITableView nopePeopleTV which is a subview of the contentView.)
This looks like this:
Code in the SingleEventController:
view.addSubview(scrollView)
scrollView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: buttonViewDividerView.topAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
override func viewDidLayoutSubviews() {
let heightOfAllObjects = scrollView.calculateHeightOfAllObjects()
scrollView.contentSize = CGSize(width: self.view.frame.width, height: heightOfAllObjects + scrollView.heightOfAllPaddings)
}
Code in EventScrollView:
addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(locationLabel)
...
contentView.addSubview(nopePeopleTV)
contentView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
titleLabel.anchor(top: contentView.topAnchor, left: contentView.leftAnchor, bottom: nil, right: contentView.rightAnchor, paddingTop: 10, paddingLeft: padding, paddingBottom: 0, paddingRight: padding, width: 0, height: 0)
locationLabel.anchor(top: titleLabel.bottomAnchor, left: contentView.leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: padding, paddingBottom: 0, paddingRight: 0, width: 200, height: 0)
nopePeopleTV.anchor(top: maybePeopleTV.bottomAnchor, left: contentView.leftAnchor, bottom: nil, right: contentView.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
layoutIfNeeded()
contentView.layoutIfNeeded()
What am I doing wrong?
My anchor function looks like this:
func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
The problem is that you are letting the content view width be dictated from the inside out by the width of the subviews. You need to give the content view a width constraint matching the width of the scroll view itself.
I'll demonstrate the difference with an artificial but complete code-only example:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let sv = UIScrollView()
sv.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: self.view.topAnchor),
sv.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
sv.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
sv.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
])
let cv = UIView() // content view
cv.translatesAutoresizingMaskIntoConstraints = false
sv.addSubview(cv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: cv.topAnchor),
sv.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
sv.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
sv.bottomAnchor.constraint(equalTo: cv.bottomAnchor),
])
let lab = UILabel()
lab.translatesAutoresizingMaskIntoConstraints = false
lab.numberOfLines = 0
lab.text = Array(repeating: "xxx", count: 100).joined(separator: " ")
cv.addSubview(lab)
NSLayoutConstraint.activate([
lab.topAnchor.constraint(equalTo: cv.topAnchor, constant:30),
lab.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
lab.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
])
}
}
Here's the result:
We have a very long label, but it doesn't wrap: it keeps extending to the right, off the screen. That's because the content view itself has its right edge off the screen. That's because we have done nothing to prevent this from happening. The long label is itself making the content view as wide as the label's entire text.
Now I add one final line of code to viewDidLoad:
cv.widthAnchor.constraint(
equalTo:sv.frameLayoutGuide.widthAnchor).isActive = true
The result is this:
The content view is now the width of the scroll view, so the label wraps within the scroll view.
We do not, however, do the same thing to the height of the content view; we want it to grow vertically in accordance with its subviews. And that way, we will be able to scroll vertically (which is what you want) but not horizontally (which is also what you want).
I have a view (Lets called thirdStackView) and two textfield (Lets called them textfield b and c).
In my viewdidload, I give their anchors and make textfields alpha to 0, also y frame of textfields to -100. With a button tap, I move thirdstachview out of screen with an animation and make textfields visible and make their frame +100.
My problem is that when I change textfield, thirdstackview started to seen on screen and textfields positions move back to ones I gave in viewdidload. Also when I dismiss keyboard, this problem occurs.
I guess It is something about layoutIfneeded() but What can be the problem?
override func viewDidLoad() {
thirdStackView.anchor(self.btnStackView.bottomAnchor, left: self.view.leftAnchor, bottom: self.emergenyBtn.topAnchor, right: self.view.rightAnchor, topConstant: 20, leftConstant: 20, bottomConstant: 13, rightConstant: 20, widthConstant: 0, heightConstant: 0)
self.createTextField(placeHolderText: getLabelText(key: CMSKeys.CMS_TF_TCKNID), titleText: getLabelText(key: CMSKeys.CMS_TF_TCKNID), imageName: "id-icon",leftButtons: [.backDisabled,.forward], rightButtons: [.cancel],textField: idTextField)
idTextField.tag = 1
idTextField.keyboardType = UIKeyboardType.numberPad
idTextField.anchor(self.btnStackView.bottomAnchor, left: self.view.leftAnchor, bottom: nil, right: self.view.rightAnchor, topConstant: 20, leftConstant: 30, bottomConstant: 0, rightConstant: 30, widthConstant: 0, heightConstant: 40)
idTextField.alpha = 0
self.createTextField(placeHolderText: getLabelText(key: CMSKeys.CMS_TF_PHONE), titleText: getLabelText(key: CMSKeys.CMS_TF_PHONE), imageName: "phone-icon",leftButtons: [.back,.forward], rightButtons: [.cancel],textField: phoneTextField)
phoneTextField.keyboardType = UIKeyboardType.numberPad
phoneTextField.tag = 2
phoneTextField.anchor(idTextField.bottomAnchor, left: self.view.leftAnchor, bottom: nil, right: self.view.rightAnchor, topConstant: 20, leftConstant: 30, bottomConstant: 0, rightConstant: 30, widthConstant: 0, heightConstant: 40)
phoneTextField.alpha = 0
}
My animation function;
#objc func adminBtnTapped(_ button: UIButton) {
if(btnTapped){
UIView.transition(with: button, duration: 0.5, options: .transitionCrossDissolve, animations: {
self.emergenyBtn.frame.origin.y -= 20
}, completion: nil)
UIView.animate(withDuration: 1, animations: {
self.thirdStackView.frame.origin.y += self.view.bounds.height
self.bgView.frame.origin.y += self.view.bounds.height
self.backBtn.frame.origin.x += self.view.bounds.width
self.idTextField.frame.origin.y += 100
self.idTextField.alpha = 1.0
self.phoneTextField.frame.origin.y += 100
self.phoneTextField.alpha = 1.0
self.passTextField.frame.origin.y += 100
self.passTextField.alpha = 1.0
}) { (_) in
//self.emergenyBtn.setTitle("Giriş Yap", for: UIControlState.normal)
self.view.layoutIfNeeded()
}
btnTapped = false
}
}
When I make superviews "translatesAutoresizingMaskIntoConstraints" attribute to false, It solved. I think, when It is true, auto layout constraints messed up.
self.view.translatesAutoresizingMaskIntoConstraints = false
For gurus that this answer is not enough, you can check official Apple document
https://developer.apple.com/documentation/uikit/uiview/1622572-translatesautoresizingmaskintoco
From Apple Document
Note that the autoresizing mask constraints fully specify the view’s size and position; therefore, you cannot add additional constraints to modify this size or position without introducing conflicts.
I'm trying to layout a UIView on screen to be at the top at an aspect ratio of 16:9 when device is in portrait mode, and to be full screen when device is in landscape mode (like a video player screen). I decided to use size classes. Here is how i was able to achieve it but i'm not satisfied with the the implementation and there are some bugs. Here is my code:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.verticalSizeClass == .compact {
localPlayerView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
}
override func viewWillLayoutSubviews() {
if traitCollection.verticalSizeClass == .compact {
localPlayerView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
if traitCollection.horizontalSizeClass == .compact {
localPlayerView.removeFromSuperview()
view.addSubview(localPlayerView)
let height = view.frame.width * 9 / 16
localPlayerView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: height)
collectionView.anchor(top: localPlayerView.bottomAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
localPlayerView.layoutIfNeeded()
}
}
I had to remove the localPlayerView from superview and add it back to get the desired result because, it was still constraining to full superview width when i switch back to portrait.
How can I improve this code to achieve the same result?
Everytime you give the localPlayerView new constraints defining its size and position, you have to remove/deactivate the old constraints. In your code it seems that you are always just adding new constraints, which will result in unsatisfiable constraints and then problems in layout - morever, if you remove the player from the superview and then add it again, that will remove those constraints - that only confirms my hypothesis. You have to be careful to always have non-conflicting constraints.
Just a broad guidelines as how you can do it. Define two properties in the viewController:
var portraitConstraints: [NSLayoutConstraint] = []
var landscapeConstraints: [NSLayoutConstraint] = []
Then, once the view is loaded (e.g., in viewDidLoad) populate them accordingly:
portraitConstraints = [
// here create all the constraints that are needed to properly define position and size of the player when in portrait
]
landscapeConstraints = [
// all the constraints for landscape
]
NSLayoutConstraint.activate(portraitConstraints) // activate these as default
Then in the traitCollectionDidChange detect if you are in landscape or in portrait (I omit this code, you can find SO questions about that), and just activate the proper constraints (and deactivate old ones):
let inLandscape = // code to detect current orientation
if inLandscape {
NSLayoutConstraint.deactivate(portraitConstraints)
NSLayoutConstraint.activate(landscapeConstraints)
} else {
NSLayoutConstraint.deactivate(landscapeConstraints)
NSLayoutConstraint.activate(portraitConstraints)
}
I have an App, that doesn't have a UINavigationController and it has a UICollectionView with some cells as its feed.
To avoid the cells appearing behind the status bar, I added a UIView behind it.
Before iPhone X it was pretty simple to do this, as the following:
let v = UIView()
v.backgroundColor = .white
view.addSubview(v)
v.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
By doing so, a tiny white layer would appear behind it as expected.
(this anchor method is from an extension, not language-default).
How can I achieve this for iPhone X ?
So far I tried the following code:
if #available(iOS 11, *) {
v.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, bottom: nil, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
let window = UIWindow(frame: UIScreen.main.bounds)
print("window.safeAreaInsets.top:", window.safeAreaInsets.top)
if window.safeAreaInsets.top > CGFloat(0.0) {
print("iPhone X")
v.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, bottom: nil, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop:-44, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: window.safeAreaInsets.top)
} else {
v.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
}
}
but it doesn't work.
What's the best approach in this case?
Thank you
You have to tell your collectionView to adjust the insets:
if #available(iOS 11, *) {
collectionView.contentInsetAdjustmentBehavior = .scrollableAxes
}
This will make sure your content will be inset enough so it's not overlapped by the iPhone X's status bar.