How to remove the blank area after change WKWebview's frame height?
I want change webview's height when keyboard show up, but after the frame height changed, there are an extra space below the HTML body. And the height of this extra blank area is about the height of keyboard.
Here is the code snippet:
NotificationCenter.default.rx
.notification(UIApplication.keyboardDidShowNotification)
.subscribe(onNext: { [weak self] keyboardConfig in
if let userInfo = keyboardConfig.userInfo{
let keyboardBounds = (userInfo["UIKeyboardFrameEndUserInfoKey"] as! NSValue).cgRectValue;
self?.webView.snp.remakeConstraints({ make in
make.left.right.top.equalToSuperview();
/// remake from screen height to below height.
make.height.equalTo(UIScreen.main.bounds.size.height - keyboardBounds.size.height);
});
}
});
Finally I still don't know how to remove this extra blank area, but I have figure out a workaround.
Since WKScrollView is a subclass of UIScrollView, so the UIScrollView Delegate also works for WKScrollView;
Therefore the contentOffset can be force reseted in the UIScrollView scrollViewDidScroll delegate method. Code snippet is provided below.
/// Change webview frame when keyboard appear.
NotificationCenter.default.rx
.notification(UIApplication.keyboardDidShowNotification)
.subscribe(onNext: { [weak self] keyboardConfig in
if let userInfo = keyboardConfig.userInfo, let this = self {
let keyboardBounds = (userInfo["UIKeyboardFrameEndUserInfoKey"] as! NSValue).cgRectValue;
this.webView.snp.remakeConstraints({ make in
make.left.right.top.equalToSuperview();
/// remake from screen height to below height.
make.height.equalTo(UIScreen.main.bounds.size.height - keyboardBounds.size.height);
});
let originalOffset = this.webView.scrollView.contentOffset;
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
this.webView.scrollView.contentOffset = originalOffset;
}
}
});
/// Force reset contentOffset when the contentOffset is out of HTML Body
extension YourWebviewVC: UIScrollViewDelegate{
internal func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.adjustWebviewOffsetFor(scrollView);
}
private func adjustWebviewOffsetFor(_ scrollView: UIScrollView, animated: Bool = false){
let currentY = scrollView.contentOffset.y + scrollView.frame.height;
if currentY > scrollView.contentSize.height{
let maxOffset = scrollView.contentSize.height - scrollView.frame.height;
scrollView.setContentOffset(CGPoint(x: 0, y: maxOffset), animated: animated);
}
}
}
/// Don't forgot set webview scroolView's delegate, for example
self.webView.scrollView.delegate = self;
self.webView.scrollView.showsVerticalScrollIndicator = false;
Related
I have a UIScrollView which scrolls automatically by setting its content offset within via a UIViewPropertyAnimator. The auto-scrolling is working as expected, however I also want to be able to interrupt the animation to scroll manually.
This seems to be one of the selling points of UIViewPropertyAnimator:
...dynamically modify your animations before they finish
However it doesn't seem to play nicely with scroll views (unless I'm doing something wrong here).
For the most part, it is working. When I scroll during animation, it pauses, then resumes once deceleration has ended. However, as I scroll towards the bottom, it rubber bands as if it is already at the end of the content (even if it is nowhere near). This is not an issue while scrolling towards the top.
Having noticed this, I checked the value of scrollView.contentOffset and it seems that it is stuck at the maximum value + the rubber banding offset. I found this question/answer which seems to be indicate this could be a bug with UIViewPropertyAnimator.
My code is as follows:
private var maxYOffset: CGFloat = .zero
private var interruptedFraction: CGFloat = .zero
override func layoutSubviews() {
super.layoutSubviews()
self.maxYOffset = self.scrollView.contentSize.height - self.scrollView.frame.height
}
private func scrollToEnd() {
let maxOffset = CGPoint(x: .zero, y: self.maxYOffset)
let duration = (Double(self.script.wordCount) / Double(self.viewModel.wordsPerMinute)) * 60.0
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
self.scrollView.contentOffset = maxOffset
}
animator.startAnimation()
self.scrollAnimator = animator
}
extension UIAutoScrollView: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// A user initiated pan gesture will begin scrolling.
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
self.interruptedFraction = scrollAnimator.fractionComplete
scrollAnimator.pauseAnimation()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
switch scrollView.panGestureRecognizer.state {
case .changed:
// A user initiated pan gesture triggered scrolling.
if let scrollAnimator = self.scrollAnimator {
let fraction = (scrollView.contentOffset.y - self.maxYOffset) / self.maxYOffset
let boundedFraction = min(max(.zero, fraction), 1)
scrollAnimator.fractionComplete = boundedFraction + self.interruptedFraction
}
default:
break
}
}
}
Is there anywhere obvious I'm going wrong here? Or any workarounds I can employ to make the scroll view stop rubber banding on scroll downwards?
You can add tap Gesture Recognizer and call this function,
extension UIScrollView {
func stopDecelerating() {
let contentOffset = self.contentOffset
self.setContentOffset(contentOffset, animated: false)
}
}
When loading posts, the most recent post is usually at the top of the screen and as the user swipes up, once the scrollView hits the bottom of the screen older posts shown are using this method
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maximumOffset - contentOffset <= 40 {
handlePagination()
}
}
But when loading messages it's the exact opposite. The most recent message is at the bottom of the screen and as the user swipes down once the scrollView hits the top of the screen older messages are shown.
There are 2 situations to take into account:
1- there is usually a textField or a textView with a sendButton at the bottom of the screen. If the user doesn't touch the textField/ or textView, they can just swipe down and once the scrollView hits the top of the screen handlePagination() will get called to show older messages
2- If the user touches the textView/Field, the keyboard is raised, even if they type some text into the textView/Field they can still swipe down to view older messages
The questions are
1. How can I detect when the user scrolls down so I can call handlePaginate() when the scrollView hits the top of the screen
and
2. Do I need to take the keyboard's height into consideration when it is raised while doing that?
I can use the keyboard notifications to detect when the keyboard is or isn't raised and simply toggle a property to the keyboard's height.
var keyboardHeight: CGFloat = 0.0
#objc fileprivate func keyboardWillShow(notification: Notification) {
guard let keyboardDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardRectangle = keyboardFrame.cgRectValue
keyboardHeight = keyboardRectangle.height
}
#objc fileprivate func keyboardWillHide(notification: Notification) {
keyboardHeight = 0.0
}
Seems I only had to check if the contentOffset was less then or equal to 50
if contentOffset <= -40 {
handlePagination()
}
It worked when the keyboard was and wasn't raised.
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
if contentOffset <= -40 {
handlePagination()
}
}
I want to put a tableView inside a pageviewcontroller which is inside a scrollview.
So I have in my storyboard :
All constraints are respected like this
I disabled the tableview scroll but my scroll doesn't scroll, my tableView in TestVC1 not expand the PagerPlace in TestVC
How can I make my scrollview scroll and its content size depends on the tableview height + my red view?
EDIT
I tried your solution, then I got a storyboard like this :
storyboard
Then my scrollview doesn't scroll, I don't know why,
In order to make your effect perfect.
Scroll View -> UIPageViewController's view -> UITableView
Scroll View has a subview of UIPageViewController's view,
UIPageViewController has many page, one page ( a controller's view ) has a subview of UITableView
Yeah. You can change the solution.
mainScrollView ( vertical slide ) -> contentScrollView ( horizontal slide ) -> contentStackView ( has many pages) -> UITableView ( one page )
mainScrollView is UIScrollView, slides in vertical,
contentScrollView is UIScrollView, slides in horizontal
contentScrollView.isPagingEnabled = true
that simulates UIPageViewController
contentStackView has many pages, one page is your UITableView
To make it work like this:
How can I make my scrollview scroll and its content size depends on the tableview height + my red view?
for the part above the UITableView
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == mainScrollView {
// vertical
let offsetY = scrollView.contentOffset.y
if offsetY >= sillValue {
scrollView.contentOffset = CGPoint(x: 0, y: sillValue)
currentChildScrollView?.am_isCanScroll = true
scrollView.am_isCanScroll = false
} else {
let negScroll = (scrollView.am_isCanScroll == false)
if negScroll{
scrollView.contentOffset = CGPoint(x: 0, y: sillValue)
}
}
}
}
for the UITableView part
use KVO to controller the base scroll view's offset Y ,
and UITableView's offset Y is by default.
let keyValueObservation = currentChildScrollView?.observe(\.contentOffset, options: [.new, .old], changeHandler: { [weak self] (scrollView, change) in
guard let self = self, change.newValue != change.oldValue else {
return
}
self.childScrollView(didScroll: scrollView)
})
internal func childScrollView(didScroll scrollView: UIScrollView){
let scrollOffset = scrollView.am_originOffset.val
let offsetY = scrollView.contentOffset.y
if scrollView.am_isCanScroll == false {
scrollView.contentOffset = scrollOffset
}
else if offsetY <= scrollOffset.y {
scrollView.contentOffset = scrollOffset
scrollView.am_isCanScroll = false
mainScrollView.am_isCanScroll = true
}
}
the full code in github
I have a VC as shown in the image
It has a UICollectionView on top, and a UITableView at the bottom.
CollectionView has 1:3 of the screen and TableView has 2:3 of the screen(set using equalHeight constraint).
I want to change the height of the UICollectionView when the tableView is scrolled.
When the tableView is scrolled up,I want to change the multiplier of equalHeights constraint to like 1:5 and 4:5 of collectionView and tableView respectively.This will ensure that height of tableView increases and collectionView decreases
When the tableView is scrolled down, the multiplier of equalHeights constraint should reset to default.
I've tried adding swipe and pan gestures to tableView, but they are unrecognised.
How to achieve this functionality?
P.S: Would love to achieve this functionality by using a pan gesture, so that dragging up and down changes the heights progressively.
This is the view hierarchy
EDIT/UPDATE
This is the code that I'm using.
class MyConstant {
var height:CGFloat = 10
}
let myConstant = MyConstant()
MainScreenVC
override func viewWillAppear(_ animated: Bool) {
myConstant.height = self.view.frame.size.height
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.decreaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = -self.view.frame.height / 6
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
} else if (self.lastContentOffset > scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.increaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
}
self.lastContentOffset = scrollView.contentOffset.y
}
Cell.Swift
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(decreaseHeight), name: Notification.Name("decreaseHeightNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(increaseHeight), name: Notification.Name("increaseHeightNotification"), object: nil)
self.contentView.translatesAutoresizingMaskIntoConstraints = false
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = heightConstraint.constant * 1.5
}
#objc func decreaseHeight() {
heightConstraint.constant = (myConstant.height / 6) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
#objc func increaseHeight() {
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
Now when I scroll both simultaneously, the screen freezes. Also is there a better way of resizing the collectionViewCell size?
I haven't tested it but you can do this. Use Autolayout in this view. it will work better with that.
Set the tableview constraint as Top, Left, Bottom, Right => 0, 0, 0, 0 with the main view and put collectionView under the tableview with constraint as Top,Left,Right, height => 0, 0, 0, x with the main view.
Note: Tableview is on top of the collectionView.
Connect your height constraint outlet of CollectionView and also define your defaultOffset variable
#IBOutlet weak var defaultHeightConstraint: NSLayoutConstraint
var defaultOffSet: CGPoint?
In viewDidLoad,
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset = UIEdgeInsetsMake(collectionView.size.height, 0, 0, 0)
}
In viewDidAppear, write
override func viewDidAppear(_ animated: Bool) {
defaultOffSet = tableView.contentOffset
}
In ViewDidScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = tableView.contentOffset
if let startOffset = self.defaultOffSet {
if offset.y < startOffset.y {
// Scrolling down
// check if your collection view height is less than normal height, do your logic.
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant - deltaY
} else {
// Scrolling up
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant + deltaY
}
self.view.layoutIfNeeded()
}
}
Hope it helps.
Since UITableView is a subclass of UIScrollView, your table view's delegate can receive UIScrollViewDelegate methods.
You don't need to use PanGesture
First implement UITableviewDelegate and in that
var oldContentOffset = CGPoint.zero
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y - oldContentOffset.y
if (self.oldContentOffset < scrollView.contentOffset.y) {
// moved to top
} else if (self.oldContentOffset > scrollView.contentOffset.y) {
// moved to bottom
} else {
// didn't move
}
oldContentOffset = scrollView.contentOffset
}
Here you can write you logic to change constant value of your constraint
UPDATE/EDIT
Another thing is don't use multiplier for tabelview height.with collection view it is fine you can give height constraint with self.view in ratio of 1/3 . for tableview just give leading ,trailing top (UICollectionView) and bottom.
So when you change height constant of UICollectionView Tableview height automatically decrease and vice-versa for decrease.
You can not change the multiplier from the outlet as it is read only. so you have to constant value to self.view.frame.height / 3 will work for you
Hope it is helpful to you
For a much cleaner approach, why don't you use a UIScrollView as the container view and add the UICollectionView and UITableView inside it and give the collectionView a height constraint while disabling tableView's scroll till the height constraint of your collectionView becomes 0.
Here's the snippet:
Extend your VC with UIScrollViewDelegate and use:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y <= yourLimit {
heightConstraint.constant = collectionViewHeight - scrollView.contentOffset.y
tableView.isScrollEnabled = false
} else if scrollView.contentOffset.y > yourLimit {
heightConstraint.constant = collectionViewHeight
tableView.isScrollEnabled = true
}
}
Try this snippet after customizing small things in it. It should work mostly, if it doesn't there is another approach which I'd share once this doesn't work.
Why don't you just add Custom Table Cell with your collection view inside, and when indexPath.row == 0 , return your custom table cell.
I am working on implementing a parallax effect for my UICollectionView. The effect thankfully is working. However, when I scroll the collection vertically for the very first time, there is a choppy effect that occurs. This ONLY happens on the first scroll through of the list. After that, the parallax effect is smooth while scrolling.
Below is my relevant code:
MyViewController.swift:
extension FeedViewController: UIScrollViewDelegate{
func scrollViewDidScroll(scrollView: UIScrollView) {
// Scroll navigation bar animation
let scrolledDown = scrollView.contentOffset.y > -self.topLayoutGuide.length
let newAlpha:CGFloat = scrolledDown ? 0.65 : 0.00
UIView.animateWithDuration(0.25) {
self.navBarBackground.alpha = newAlpha
}
// Parallax effect
for cell in collectionView.visibleCells() {
updateParallaxCell(cell)
}
}
func updateParallaxCell(cell: UICollectionViewCell) {
if let myCell = cell as? MyCustomCell {
myCell.updateParallaxEffect(collectionView, mainView: view)
}
}
}
MyCustomCell.swift
override func awakeFromNib() {
super.awakeFromNib()
backgroundImageOriginalOrigin = backgroundImage.frame.origin
}
func updateParallaxEffect(collectionView: UICollectionView, mainView view: UIView) {
let convertedRect = collectionView.convertRect(self.frame, toView: view) //frame of the cell within the coordinate system of the main view
let distanceFromCenter = CGRectGetHeight(self.frame) / 2 - CGRectGetMinY(convertedRect) //y coordinate distance of the cell from the center of the main view
let difference : CGFloat = 50.0 //maximum relative offset of the image within the cell
let move = -difference / 4 + (distanceFromCenter / self.frame.size.height) * difference // calculated relative offset of the image
let y = self.backgroundImageOriginalOrigin.y + move // newly calculated Y coordinate of the image
self.backgroundImage.frame.origin = CGPointMake(self.backgroundImageOriginalOrigin.x, y) // adjusting the image view frame
}
override func layoutSubviews() {
updateParallaxEffect(collectionView, mainView: mainView)
super.layoutSubviews()
}
Can anyone see what it is I'm doing wrong?
Try adding this to your viewDidLoad():
UICollectionView().setContentOffset(CGPointMake(0, 1), animated: true)
UICollectionView().setContentOffset(CGPointMake(0, 0), animated: false)