UIScrollView - change content offset when dragging ends - uiscrollview

I have a scroll view that is programmed so that if you pull down more than 100px (i.e into the negative y content offset and with bounces enabled) then let go, a view at the top of the scroll view gets larger 100px larger (pushing everything else down). To make the transition smooth I'm trying to adjust the content offset of the scrollview by 100px at this point, like this:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
if scrollView.contentOffset < -100
{
scrollView.contentOffset.y += 100 //
makeTopViewTaller()
}
}
However, the change in the contentOffset doesn't stick. By logging the content offset in the scrollViewDidScroll() method I can see that the y value changes for a moment, but then goes back to where it was when the dragging ended.
Is there any way to force the content offset to change, then let the UIScrollView's natural bounce decelaration to apply to the new value?

I tried a whole bunch of different approaches to this problem, but the one that eventually worked involved creating a subclass of the UIScrollView (in my case, actually a UICollectionView) and overriding the setter for the contentOffset property. Here's my subclass:
class MyCollectionView : UICollectionView
{
var isTransitioning:Bool = false
var initialTransitioningY:CGFloat = 0
override var contentOffset:CGPoint
{
get
{
return super.contentOffset
}
// This custom setter is to make the transition to the large header state smooth
set
{
if(isTransitioning && initialTransitioningY < 0)
{
var oy = newValue.y
if(oy == 0 && contentOffset.y < -10) // This is to avoid a flicker when you first call 'reloadData' on the collection view, which sets the contentOffset.y to 0, then returns to it's previous state
{
oy = contentOffset.y
}
super.contentOffset = CGPointMake(newValue.x, (initialTransitioningY + 100) * (oy / initialTransitioningY))
}
else
{
super.contentOffset = newValue
}
}
}
}
When the user has pulled passed the transition point (100px in my case) and let go the scrollViewWillEndDragging method is called and the isTransitioning property is set to true and initialTransitioningY set to the collection view's current content offset. When scrollViewDidEndDecelerating is called the isTransitioning property is set back to false.
Works a charm.

Related

iOS Autolayout: Oddly expand-animation of UITextView inside an UIScrollView

I'm trying to animate the height constraint of a UITextView inside a UIScrollView. When the user taps the "toggle" button, the text should appear in animation from top to bottom. But somehow UIKit fades in the complete view.
To ensure the "dynamic" height depending on the intrinsic content-size, I deactivate the height constraint set to zero.
#IBAction func toggle() {
layoutIfNeeded()
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let self = self else {
return
}
if self.expanded {
NSLayoutConstraint.activate([self.height].compactMap { $0 })
} else {
NSLayoutConstraint.deactivate([self.height].compactMap { $0 })
}
self.layoutIfNeeded()
})
expanded.toggle()
}
The full code of this example is available on my GitHub Repo: ScrollAnimationExample
Reviewing your GitHub repo...
The issue is due to the view that is animated. You want to run .animate() on the "top-most" view in the hierarchy.
To do this, you can either create a new property of your ExpandableView, such as:
var topMostView: UIView?
and then set that property from your view controller, or...
To keep your class encapsulated, let it find the top-most view. Replace your toggle() func with:
#IBAction func toggle() {
// we need to run .animate() on the "top" superview
// make sure we have a superview
guard self.superview != nil else {
return
}
// find the top-most superview
var mv: UIView = self
while let s = mv.superview {
mv = s
}
// UITextView has subviews, one of which is a _UITextContainerView,
// which also has a _UITextCanvasView subview.
// If scrolling is disabled, and the TextView's height is animated to Zero,
// the CanvasView's height is instantly set to Zero -- so it disappears instead of animating.
// So, when the view is "expanded" we need to first enable scrolling,
// and then animate the height (to Zero)
// When the view is NOT expanded, we first disable scrolling
// and then animate the height (to its intrinsic content height)
if expanded {
textView.isScrollEnabled = true
NSLayoutConstraint.activate([height].compactMap { $0 })
} else {
textView.isScrollEnabled = false
NSLayoutConstraint.deactivate([height].compactMap { $0 })
}
UIView.animate(withDuration: 0.6, animations: {
mv.layoutIfNeeded()
})
expanded.toggle()
}

UIPageViewController with Scroll Transition Style automatically adds Safe Area insets for iPhone X

I'm developing image gallery like slider using UIPageViewController and I'm having troubles with UIPageViewController automatic insets in Scroll transition style mode.
Here is my layout:
UIViewController with UIContainerView (magenta background)
UIPageViewController linked to the container (from #1)
List of dynamically created view UIViewController(s) within the page controller (from #2), full width-height views (1. orange, 2. red, 3. green)
It used to work fine for a long time and continue to work with iOS 11 unless it's rendered on iPhone X device with safe area:
I've checked a lot of various options and was able to confirm that it's related specifically to the Scroll mode of the Page Controller. If I switch to PageCurl transition style - it works as expected (full height):
The Page Controller doesn't expose a lot of options to control this behavior for the scroll mode and I wasn't able to "hack" it as well by searching the controls tree and modifying various insets and frame and contentSize related properties. What I can clearly see is that once view controller is created, my scroll view contentSize and frame is 34px smaller then the container frame
> view.frame
{{X=0,Y=0,Width=375,Height=732}}
Bottom: 732
Height: 732
IsEmpty: false
Left: 0
Location: {{X=0, Y=0}}
Right: 375
Size: {{Width=375, Height=732}}
Top: 0
Width: 375
X: 0
Y: 0
> scroll.frame
{{X=-5,Y=0,Width=385,Height=698}}
Bottom: 698
Height: 698
IsEmpty: false
Left: -5
Location: {{X=-5, Y=0}}
Right: 380
Size: {{Width=385, Height=698}}
Top: 0
Width: 385
X: -5
Y: 0
> scroll.contentSize
{{Width=1155, Height=698}}
Height: 698
IsEmpty: false
Width: 1155
I've also set up my autolayout constraints to be linked to superview rather than safe area:
Here is my code for the Home Controller and all the rest is set in a storyboard (alert: C# Xamarin syntax)
private List<UIViewController> viewControllers;
public HomePageViewController (IntPtr handle) : base ( handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var child1 = new UIViewController();
child1.View.BackgroundColor = UIColor.Orange;
var child2 = new UIViewController();
child2.View.BackgroundColor = UIColor.Red;
var child3 = new UIViewController();
child3.View.BackgroundColor = UIColor.Green;
this.viewControllers = new List<UIViewController>
{
child1,
child2,
child3,
};
this.SetViewControllers(new UIViewController[] { child1 }, UIPageViewControllerNavigationDirection.Forward, false, null);
this.GetNextViewController = (c, r) =>
{
var current = this.viewControllers.IndexOf(this.ViewControllers[0]);
if (current >= this.viewControllers.Count - 1)
return null;
return this.viewControllers[current + 1];
};
this.GetPreviousViewController = (c, r) =>
{
var current = this.viewControllers.IndexOf(this.ViewControllers[0]);
if (current <= 0)
return null;
return this.viewControllers[current - 1];
};
}
How can I force my children view controllers to have full height (equals to the frame height of the parent container)?
I think you can solve this issue using code and custom layout. I mean create your UIPageViewController and insert its view to your UIViewController in code not on storyboard. I think you
should override UIViewController.viewDidLayoutSubviews() and set your rects "manually" (at least the one of the UIPageViewController.) Well, when you do it in code, sometimes you even don't need to override UIViewController.viewDidLayoutSubviews() because the template by Apple itself didn't do this. I think because any created view has translatesAutoresizingMaskIntoConstraints = true. So you can also follow this approach.
There is an example when you create a new project and state it is a page based app.
Here is the template if you want (WARNING: This is a part of a template by Apple itself)
var pageViewController: UIPageViewController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Configure the page view controller and add it as a child view controller.
self.pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
self.pageViewController!.delegate = self
let startingViewController: DataViewController = self.modelController.viewControllerAtIndex(0, storyboard: self.storyboard!)!
let viewControllers = [startingViewController]
self.pageViewController!.setViewControllers(viewControllers, direction: .forward, animated: false, completion: {done in })
self.pageViewController!.dataSource = self.modelController
self.addChildViewController(self.pageViewController!)
self.view.addSubview(self.pageViewController!.view)
// Set the page view controller's bounds using an inset rect so that self's view is visible around the edges of the pages.
var pageViewRect = self.view.bounds
if UIDevice.current.userInterfaceIdiom == .pad {
pageViewRect = pageViewRect.insetBy(dx: 40.0, dy: 40.0)
}
self.pageViewController!.view.frame = pageViewRect
self.pageViewController!.didMove(toParentViewController: self)
}
You can extend this functionality by extending
I had a similar issue that only happened in the X sizes and after hours of trails and errors I got it fixed.
I am not sure if it's applicable for you or not but the way I have my page view controller VCs is that each VC has an image filling its background. I have 3 pages. Scrolling for the first time looked normal but when I would reverse scroll from page 2 to 1 or from 1 to 0, page 1's image would show around 40 pixels from the side when it should be completely hidden (similar to your screenshots).
So to fix it I had to either set the images to Aspect Fit or clips to bounds = true. I used the latter because it worked better for the UI.
I achieved to have my UIPageViewController display with full screen height with the following code :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let contentScrollView = view.subviews.first(where: { $0 is UIScrollView }) else { return }
contentScrollView.frame.size.height = view.frame.height
}

How to animate the height of an Animated View dynamically based on the position of another Animated View?

Concept: User moves a circle animated view up and down in the y direction on the screen which increases and decreases the height of a separate animated view.
Problem: The "this.state.pan.getLayout()" that is used to move the circle up and down gives an error when computations are made with it.
e.g.
position = function() {
let Window = Dimensions.get('window');
return {
height: (Window.height / 2) + this.state.pan.getLayout()
}
}
returns an error [object Object] of type NSString cannot be converted to NSNumber
getLayout() method returns an {left: String, top: String} for use in style attribute. To retrieve the actual number, access x and y properties directly :
position = function() {
let Window = Dimensions.get('window');
return {
height: (Window.height / 2) + this.state.pan.y
}
}
More info : https://facebook.github.io/react-native/docs/animated.html#getlayout

Old value in scrollbar event

I have a vertical scroll bar. When it resized triggers an event that checks if it is 100% or more to hide it but instead to get the new size value just get the old one. The visibility changes OK but not the size:
scrollBarL.heightProperty().addListener((ObservableValue<? extends Number> ov,
Number old_val, Number new_val) -> {
if (scrollBarL.getVisibleAmount() >= 1 ) {
scrollBarL.setMaxWidth(0); // Not change width immediately
scrollBarL.setPrefWidth(0); // Not change width immediately
scrollBarL.setMinWidth(0); // Not change width immediately
scrollBarL.setVisible(false); // OK. Change visibility immediately
} else {
scrollBarL.setMaxWidth(19); // Not change width immediately
scrollBarL.setPrefWidth(19); // Not change width immediately
scrollBarL.setMinWidth(19); // Not change width immediately
scrollBarL.setVisible(true); // OK. Change visibility immediately
}
});

How to set the position and size (programmatically) for the main window?

I'm using the newest default project for OS X (10.11, Xcode 7.0). It uses storyboards and the hierarchy is as follows:
Window Controller -> View Controller
I want to set the initial position and frame size for the window. Obviously the user can change that, but I want it to start with some defaults. I've tried subclassing NSWindowController, but that has no effect.
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
guard let window = window else {
return
}
let windowOriginPoint = CGPoint(x: 0, y: 0)
let windowSize = CGSize(width: 800, height: 400)
window.setFrame(NSRect(origin: windowOriginPoint, size: windowSize), display: true)
print("windowDidLoad")
}
}
What's the proper way of doing this?
I faced the same problem. I solved it by moving the setFrame code inside windowDidBecomeMain instead of windowDidLoad; there is a bad side effect if the window has been moved manually and is reselected to become main: window jumps when dragged and immediately returns to the right position. To avoid it, I used a patch: In the windowController, I declare a
private var firstAppearance = true;
In the windowDidBecomeMain I do the setFrame only on firstAppearance; then I set firstAppearance to false.

Resources