Center the text in a UITextView, vertically and horizontally [duplicate] - ios

This question already has answers here:
Center text vertically in a UITextView
(19 answers)
Closed 7 years ago.
I have a grouped table view with one cell. In this cell I'm putting a UITextView in cellForRowAtIndexPath:, and I instantly make this first Responder.
My problem is: When I start typing, the text is left-justified, not centered horizontally and vertically as I want. This is on iOS 7.
How can I center the text?

I resolve this issue by observing the contentsize of UITextView, when there is any change in the contentSize, update the contentOffset.
Add observer as follows:
[textview addObserver:self forKeyPath:#"contentSize" options:(NSKeyValueObservingOptionNew) context:NULL];
Handle the observer action as follows:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
UITextView *txtview = object;
CGFloat topoffset = ([txtview bounds].size.height - [txtview contentSize].height * [txtview zoomScale])/2.0;
topoffset = ( topoffset < 0.0 ? 0.0 : topoffset );
txtview.contentOffset = (CGPoint){.x = 0, .y = -topoffset};
}
To make the textview text horizontally center, select the textview from .xib class and go to the library and in that set Alignment as center.
Enjoy. :)

Very good solution! This is a Swift + Interface Builder solution so that you can enable it in IB.
I'll put it as part of the LSwift library.
extension UITextView {
#IBInspectable var align_middle_vertical: Bool {
get {
return false // TODO
}
set (f) {
self.addObserver(self, forKeyPath:"contentSize", options:.New, context:nil)
}
}
override public func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject:AnyObject], context: UnsafeMutablePointer<Void>) {
if let textView = object as? UITextView {
var y: CGFloat = (textView.bounds.size.height - textView.contentSize.height * textView.zoomScale)/2.0;
if y < 0 {
y = 0
}
textView.content_y = -y
}
}
}
public extension UIScrollView {
public var content_x: CGFloat {
set(f) {
contentOffset.x = f
}
get {
return contentOffset.x
}
}
public var content_y: CGFloat {
set(f) {
contentOffset.y = f
}
get {
return contentOffset.y
}
}
}

Related

Obj-C- Detecting textView line break?

When a user is typing inside of my textView, I want to know when a new line begins, as I want to change the height of my textView when this happens. That said, I know how to accomplish this if the return button is hit - but if a new line just begins automatically (e.g. a new line is started simply because a user keeps typing), this doesn't seem as simple. The code I'm using below works successfully when 'return' is tapped - but I can't seem to get it to detect any sort of line change otherwise.
Hope I worded this well enough. Thanks!
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text
{
if ([text isEqualToString:#"\n"]) {
self.replyField.text=[NSString stringWithFormat:#"%#\n",self.replyField.text];
CGFloat fixedWidth = self.replyField.frame.size.width;
CGSize newSize = [self.replyField sizeThatFits:CGSizeMake(fixedWidth, MAXFLOAT)];
CGRect newFrame = self.replyField.frame;
newFrame.size = CGSizeMake(fmaxf(newSize.width, fixedWidth), newSize.height);
self.replyField.frame = newFrame;
CGRect newFrame1 = self.upView.frame;
NSLog(#"WHAT IS THE NEW HEIGHT %f", newFrame1.size.height);
self.upView.frame = CGRectOffset(self.upView.frame, 0, -15);
return NO;
}
// For any other character return TRUE so that the text gets added to the view
return YES;
}
Im not sure if this is gonna help at all since im also fairly new to obj-c but give it a go
-(void)textViewDidChange:(UITextView *)textView
{
CGFloat fixedWidth = textView.frame.size.width;
CGSize newSize = [textView sizeThatFits:CGSizeMake(fixedWidth, MAXFLOAT)];
CGRect newFrame = textView.frame;
newFrame.size = CGSizeMake(fmaxf(newSize.width, fixedWidth),
newSize.height);
textView.frame = newFrame;
}
Make sure you also disable scrolling
textview.scrollEnabled = NO;
To detect the change in number of lines, you can use KVO, as #rmaddy suggested with a comment.
- (void)viewDidLoad {
[textView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (change != nil && [keyPath isEqualToString:#"contentSize"] && [object isKindOfClass:UITextView.class]) {
NSNumber *oldValue = change[NSKeyValueChangeOldKey];
NSNumber *newValue = change[NSKeyValueChangeNewKey];
//Do anything you need with the height values here
}
}
In swift
override func viewDidLoad() {
textView.addObserver(self, forKeyPath: "contentSize", options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let change = change, keyPath == "contentSize" && object is UITextView else {
return
}
let oldHeight = (change[.oldKey] as! CGSize).height
let newHeight = (change[.newKey] as! CGSize).height
//Do anything you need with the height values here
}
deinit {
// iOS10 was crashing for me when I didn't explicitly remove
// the observer before destroying the containing object
inputTextView.removeObserver(self, forKeyPath: "contentSize", context: nil)
}
Try this:
For dynamically change UITextView height you can create property of height constraints and set/connect this to textview height constraints from storyboard.
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *txtViewHeightConstraint;
and then after add didChange for checking textview inputs
-(void)textViewDidChange:(UITextView *)textView
{
[self SetupTextField] ;
}
- (void)SetupTextField{
CGRect newFrame = self.txtInfo.frame;
newFrame.size.height = self.txtInfo.contentSize.height;
NSLog(#"NEW HEIGHT %f", newFrame1.size.height);
if (newFrame.size.height < 100.0) { // if condition is optional
self.txtViewHeightConstraint.constant = newFrame.size.height ;
}
}
check screenshot for set height constraint of textview

How can I move a view to see a UITextField when the keyboard is present, when using UICollectionViewFlowLayout

I have a method to move a CollectionView if two text fields inside it are obscured by the frame of the iPad keyboard:
private void OnKeyboardNotification(NSNotification notification)
{
var activeTextField = FindFirstResponder(CollectionView);
NSDictionary userInfo = notification.UserInfo;
CGSize keyboardSize = ((NSValue)userInfo[UIKeyboard.FrameBeginUserInfoKey]).RectangleFValue.Size;
var contentInset = new UIEdgeInsets(0, 0, keyboardSize.Height, 0);
CollectionView.ContentInset = contentInset;
CollectionView.ScrollIndicatorInsets = contentInset;
CGRect oldRect = CollectionView.Frame;
CGRect aRect = new CGRect(oldRect.X, oldRect.Y, oldRect.Width,
oldRect.Height -= keyboardSize.Height);
if (!aRect.Contains(activeTextField.Frame.Location))
{
CGPoint scrollPoint = new CGPoint(0, activeTextField.Frame.Location.Y - (keyboardSize.Height - 15));
CollectionView.SetContentOffset(scrollPoint, true);
}
}
I don't believe the code is working as intended. It's complicated by the fact that I have a custom layout defined in a subclass of UICollectionViewFlowLayout. The layout allows me to have cells scrolling vertically which snap into focus.
Every time I call OnKeyboardNotification, override UICollectionViewLayoutAttributes[] is called afterwards in the custom layout. I thought this might be cancelling out the effect of the method, but if that's the case, then how can I change when UICollectionViewLayoutAttributes[] is called?
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
var array = base.LayoutAttributesForElementsInRect(rect);
var visibleRect = new CGRect(CollectionView.ContentOffset, CollectionView.Bounds.Size);
foreach (var attributes in array)
{
if (attributes.Frame.IntersectsWith(rect))
{
float distance = (float)(visibleRect.GetMidX() - attributes.Center.X);
float normalizedDistance = distance / ACTIVE_DISTANCE;
if (Math.Abs(distance) < ACTIVE_DISTANCE)
{
float zoom = 1 + ZOOM_FACTOR * (1 - Math.Abs(normalizedDistance));
attributes.Transform3D = CATransform3D.MakeScale(zoom, zoom, 1.0f);
attributes.ZIndex = 1;
}
}
}
return array;
}
Edit:
Here is an example of the problem.
I have two fields
and here, when 'edit mode' is entered, the keyboard hides the age field.
Seems like you use the sample from Xamarin Samples.
UICollectionViewLayoutAttributes will be invoked every time you change the size of your UICollectionView.
I suggest you changing the location of your view instead of changing size in your OnKeyboardNotification method:
CGRect oldRect = CollectionView.Frame;
CGRect aRect = new CGRect(oldRect.X, oldRect.Y - keyboardSize.Height, oldRect.Width,oldRect.Height);
Hope it can solve your problem, I'm midnight right now, if it can not work, leave some message, I will check latter when tomorrow morning.
Edit -----------------------------------------------------------------
I change some code in the Sample you using to make the effect.
At first, I choose the LineLayout in AppDelegate.cs, like this:
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
//Don't change anything
// simpleCollectionViewController = new SimpleCollectionViewController (flowLayout);
simpleCollectionViewController = new SimpleCollectionViewController (lineLayout);
// simpleCollectionViewController = new SimpleCollectionViewController (circleLayout);
simpleCollectionViewController.CollectionView.ContentInset = new UIEdgeInsets (50, 0, 0, 0);
window.RootViewController = simpleCollectionViewController;
window.MakeKeyAndVisible ();
return true;
}
And then, I add a static property to LineLayout.cs, like this:
public class LineLayout : UICollectionViewFlowLayout
{
private static bool flagForLayout = true;
public static bool FlagForLayout {
get {
return flagForLayout;
}
set {
flagForLayout = value;
}
}
public const float ITEM_SIZE = 200.0f;
public const int ACTIVE_DISTANCE = 200;
public const float ZOOM_FACTOR = 0.3f;
public LineLayout ()
{
//Don't change anything
}
public override bool ShouldInvalidateLayoutForBoundsChange (CGRect newBounds)
{
return flagForLayout;
}
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect (CGRect rect)
{
//Don't change anything
}
public override CGPoint TargetContentOffset (CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
//Don't change anything
}
}
Then in the SimpleCollectionViewController.cs, I add a UITextField for every cell, like this:
public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
{
var animalCell = (AnimalCell)collectionView.DequeueReusableCell (animalCellId, indexPath);
var animal = animals [indexPath.Row];
animalCell.Image = animal.Image;
animalCell.AddSubview (new UITextField (new CGRect (0, 0, 100, 30)){ BackgroundColor = UIColor.Red });
return animalCell;
}
And still in SimpleCollectionViewController.cs, I add some code in ViewDidload method and add two method to handle the keyboard display event, like this:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
CollectionView.RegisterClassForCell (typeof(AnimalCell), animalCellId);
CollectionView.RegisterClassForSupplementaryView (typeof(Header), UICollectionElementKindSection.Header, headerId);
UIMenuController.SharedMenuController.MenuItems = new UIMenuItem[] {
new UIMenuItem ("Custom", new Selector ("custom"))
};
this.View.AddGestureRecognizer(new UITapGestureRecognizer(()=>{
this.View.EndEditing(true);
}));
NSNotificationCenter.DefaultCenter.AddObserver(this,new ObjCRuntime.Selector("onKeyboardWillShowNotification:"),UIKeyboard.WillShowNotification,null);
NSNotificationCenter.DefaultCenter.AddObserver(this,new ObjCRuntime.Selector("onKeyboardWillHideNotification:"),UIKeyboard.WillHideNotification,null);
}
[Export("onKeyboardWillShowNotification:")]
private void OnKeyboardWillShowNotification(NSNotification notification)
{
LineLayout.FlagForLayout = false;
NSDictionary userInfo = notification.UserInfo;
CGSize keyboardSize = ((NSValue)userInfo[UIKeyboard.FrameBeginUserInfoKey]).RectangleFValue.Size;
CGRect oldRect = CollectionView.Frame;
CGRect aRect = new CGRect(oldRect.X, oldRect.Y - keyboardSize.Height, oldRect.Width,
oldRect.Height);
this.CollectionView.Frame = aRect;
}
[Export("onKeyboardWillHideNotification:")]
private void OnKeyboardWillHideNotification(NSNotification notification)
{
LineLayout.FlagForLayout = true;
this.CollectionView.Frame = UIScreen.MainScreen.Bounds;
}
Now LayoutAttributesForElementsInRect will be invoked when keyboard show, hope it can solve your problem.

Keyboard to move the view only if the textfield is hidden

I'm trying to get my app to move the view when the keyboard appears, and so far the results have been... mixed to say the least. I can get it to move, thing is it's either hard coded or only works partially.
I have multiple Textfields in my view, when I tap on them, sometimes depending on where my scroll is it get's hidden by the keyboard.
Now what I need my app to do is to move the view to see the textfield only if the active textfield is hidden by the keyboard.
My Hierarchy for the view goes like this :
So I have a Scroll view, and in the scroll View I have a UIView named ContentView, in the ContentView I have all my textfields and labels.
thing is, I can't hard code it since My app is universal, I need to have the keyboard move the view only if it hides the textfield. Because in a situation where the user is on an iPad, the View will likely never have to move
I used the following Stack overflow answers with no results :
Swift: Scroll View only when a TextField or Button is hidden by the Keyboard
Move view with keyboard using Swift
here's my code that actually comes from one of those answers :
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(NewPaxController.keyboardWillShow), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(NewPaxController.keyboardWillHide), name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification:NSNotification) {
if keyboardIsPresent == false {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
self.ContentView.frame.origin.y -= keyboardSize.height
keyboardIsPresent = true
}
}
}
func keyboardWillHide(notification:NSNotification) {
if keyboardIsPresent == true {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
self.ContentView.frame.origin.y += keyboardSize.height
keyboardIsPresent = false
}
}
}
I'm almost 100% sure all my error come from the fact the I have a ContentView... but I need it in my case. Thanks in advance for your help
You should not modify frames when keyboard shows up.
Instead you need to set the bottom inset of the scrollview scrollView.contentInset.bottom from zero to keyboard height. When the keyboard disappears, you set the inset back to zero.
This whole problem is solvable by mere 10 lines of code.
literally get the keyboard height from notification, store it ion a local variable in the class while KB is showing, and use the value to set insets in delegate callback/event handler methods.
The trick here is that setting nonzero insets will effectively scroll the scrollview together with the content for you up by and that will push the current textfield up as well.
You should first try to locate the UITextField instance by using the following code.
extension UIView {
func firstResponder() -> UIView? {
if self.isFirstResponder() {
return self
}
for subview in self.subviews {
if subview.isFirstResponder() {
return subview
}
}
return nil
}
}
In the keyboardWillShow: function decide if the textfield is visible or not if the keyboard comes up.
func keyboardWillShow(notification:NSNotification) {
if keyboardIsPresent == false {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue(),
let inputView = self.ContentView.firstResponder()
where inputView.frame.maxY > self.ContentView.frame.size.height - keyboardSize.height {
self.ContentView.frame.origin.y -= keyboardSize.height
keyboardIsPresent = true
}
}
}
Than only move the view back in the hide function if it was moved away.
Here is the code for it, It is pretty straight forward, but if you still need help with it, I will explain it further.
#pragma mark - Keyboard Observer events
-(void)keyboardWillShow:(NSNotification*)notification {
NSDictionary *info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
keyboardHeight = kbSize.height;
[self updateScrollViewPosition];
}
-(void)keyboardDidChange:(NSNotification *)notification {
NSDictionary *info = [notification userInfo];
CGSize kbSizeBegin = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
CGSize kbSizeEnd = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
if (kbSizeBegin.height != kbSizeEnd.height) {
keyboardHeight = kbSizeEnd.height;
if (activeTextField && [activeTextField isFirstResponder]) {
[self updateScrollViewPosition];
}
}
}
-(void)keyboardWillHide:(NSNotification*)notification {
keyboardHeight = 0;
activeTextField = nil;
[self resignAllTextFields];
}
#pragma mark - UITextFieldDelegate Methods
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
activeTextField = textField;
return YES;
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
activeTextField = textField;
[self updateScrollViewPosition];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
keyboardHeight = 0;
activeTextField = nil;
[textField resignFirstResponder];
return YES;
}
#pragma mark - Update Method
-(void)updateScrollViewPosition {
if (keyboardHeight > 0 && activeTextField) {
CGRect frame = activeTextField.frame;
CGFloat yPoint = scrollView.frame.origin.y+frame.origin.y+frame.size.height+8.0;
CGFloat height = self.view.frame.size.height-keyboardHeight;
CGFloat diff = yPoint-height;
if (diff > 0.0) {
[scrollView setContentOffset:CGPointMake(0, diff) animated:YES];
}
else {
CGFloat diff = scrollView.contentSize.height-scrollView.contentOffset.y;
if (diff<scrollView.frame.size.height) {
diff = scrollView.contentSize.height-scrollView.frame.size.height;
if (diff < 0) {
diff = 0.0;
}
[scrollView setContentOffset:CGPointMake(0, diff) animated:YES];
}
}
}
else {
CGFloat diff = scrollView.contentSize.height-scrollView.contentOffset.y;
if (diff<scrollView.frame.size.height) {
diff = scrollView.contentSize.height-scrollView.frame.size.height;
if (diff < 0) {
diff = 0.0;
}
[scrollView setContentOffset:CGPointMake(0, diff) animated:YES];
}
}
}
#pragma mark
Edit: A simple resignAllTextFields method as requested. containerView is the view which contains all the UITextField.
-(void)resignAllTextFields {
for (UIView *view in containerView.subviews) {
if ([view isKindOfClass:[UITextField class]]) {
UITextField *textField = (UITextField*)view;
[textField resignFirstResponder];
}
}
[self updateScrollViewPosition];
}

Add padding to a UIButton that has no text

I have a UIButton that is 36x36 and I want to add padding (in essence like CSS) so that the touchable area is the recommended 44x44.
I've tried adding edge inserts through the Interface Builder and also with the code below but nothing I've tried increases the touchable area.
resetButton.contentEdgeInsets.top = 50
resetButton.contentEdgeInsets.left = 50
Does an edge inset only work with buttons that include text?
Update:
I've tried setting contentEdgeInsets, imageEdgeInsets, and titleEdgeInsets to no avail.
I could expand touchable area by editing the actual image and increasing the border around the icon. (This wouldn't be the preferred way but I'll probably go this route if I can't find another solution.)
I overwrote in Swift this answer and it works:
import UIKit
import ObjectiveC
private var KEY_HIT_TEST_EDGE_INSETS: String = "HitTestEdgeInsets"
extension UIButton {
public func setHitTestEdgeInsets(inout hitTestEdgeInsets: UIEdgeInsets) {
let value = NSValue(&hitTestEdgeInsets, withObjCType:NSValue(UIEdgeInsets: UIEdgeInsetsZero).objCType)
objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
private func hitTestEdgeInsets() -> UIEdgeInsets {
let value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS)
if (value != nil) {
var edgeInsets: UIEdgeInsets = UIEdgeInsetsZero
value.getValue(&edgeInsets)
return edgeInsets
} else {
return UIEdgeInsetsZero
}
}
override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
if UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets(), UIEdgeInsetsZero) || !self.enabled || self.hidden {
return super.pointInside(point, withEvent: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets());
return CGRectContainsPoint(hitFrame, point)
}
}
Use:
var insets: UIEdgeInsets = UIEdgeInsetsMake(-20, -20, -20, -20)
button.setHitTestEdgeInsets(&insets)

Progress of UIPageViewController

I would like to receive updates from the uipageviewcontroller during the page scrolling process. I want to know the transitionProgress in %. (This value should update when the user move the finger in order to get to another page). I'm interested in the animation progress from one page to another, not the progress through the total number of pages.
What I have found so far:
There is a class called UICollectionViewTransitionLayout that have the property corresponding to what I am looking for, "transitionProgress". Probably uipageviewcontroller implement this method somehow?
I can call the following method on the uipagecontroller but I only get 0 as result!
CGFloat percentComplete = [self.pageViewController.transitionCoordinator percentComplete];
in SWIFT to copy paste ;) works perfect for me
extension UIPageViewController: UIScrollViewDelegate {
public override func viewDidLoad() {
super.viewDidLoad()
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
scrollView.delegate = self
}
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = abs(point.x - view.frame.size.width)/view.frame.size.width
print("percentComplete: ",percentComplete)
}
}
At last I found out a solution, even if it is probably not the best way to do it:
I first add an observer on the scrollview like this:
// Get Notified at update of scrollview progress
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
[sW addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];
And when the observer is called:
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
CGPoint point = sW.contentOffset;
float percentComplete;
//iPhone 5
if([ [ UIScreen mainScreen ] bounds ].size.height == 568){
percentComplete = fabs(point.x - 568)/568;
} else{
//iphone 4
percentComplete = fabs(point.x - 480)/480;
}
NSLog(#"percentComplete: %f", percentComplete);
I'm very happy that I found this :-)
Since I thought that the functionality of scrolling would stay forever, but that the internal implementation may change to something other than a scroll view, I found the solution below (I haven't tested this very much, but still)
NSUInteger offset = 0;
UIViewController * firstVisibleViewController;
while([(firstVisibleViewController = [self viewControllerForPage:offset]).view superview] == nil) {
++offset;
}
CGRect rect = [[firstVisibleViewController.view superview] convertRect:firstVisibleViewController.view.frame fromView:self.view];
CGFloat absolutePosition = rect.origin.x / self.view.frame.size.width;
absolutePosition += (CGFloat)offset;
(self is the UIPageViewController here, and [-viewControllerForPage:] is a method that returns the view controller at the given page)
If absolutePosition is 0.0f, then the first view controller is shown, if it's equal to 1.0f, the second one is shown, etc... This can be called repeatedly in a CADisplayLink along with the delegate methods and/or UIPanGestureRecognizer to effectively know the status of the current progress of the UIPageViewController.
EDIT: Made it work for any number of view controllers
Use this -
for (UIView *v in self.pageViewController.view.subviews) {
if ([v isKindOfClass:[UIScrollView class]]) {
((UIScrollView *)v).delegate = self;
}
}
to implement this protocol : -(void)scrollViewDidScroll:(UIScrollView *)scrollView
and then use #xhist's code (modified) in this way
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint point = scrollView.contentOffset;
float percentComplete;
percentComplete = fabs(point.x - self.view.frame.size.width)/self.view.frame.size.width;
NSLog(#"percentComplete: %f", percentComplete);
}
Based on Appgix solution, I'm adding this directly on my 'UIPageViewController' subclass. (Since I only need it on this one)
For Swift 3:
class MYPageViewControllerSubclass: UIPageViewController, UIScrollViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
for subView in view.subviews {
if subView is UIScrollView {
(subView as! UIScrollView).delegate = self
}
}
}
// MARK: - Scroll View Delegate
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = fabs(point.x - view.frame.size.width)/view.frame.size.width
NSLog("percentComplete: %f", percentComplete)
}
// OTHER CODE GOES HERE...
}
While Appgix' solution seemed to work at first, I noticed that when the user pans in a UIPageViewController, lifts the finger shortly and then immediately starts dragging again while the "snap-back" animation is NOT YET finished and then lifts his finger again (which will again "snap-back"), the scrollViewDidScroll method is only called when the page view controller finished the animation.
For the progress calculation this means the second pan produces continuous values like 0.11, 0.13, 0.16 but when the scroll view snaps back the next progress value will be 1.0 which causes my other scroll view to be out of sync.
To fight this I'm now listening to the scroll view's contentOffset key, which is still updated continuously in this situation.
KVO approach for Swift 4
var myContext = 0
override func viewDidLoad() {
for view in self.view.subviews {
if view is UIScrollView {
view.addObserver(self, forKeyPath: "contentOffset", options: .new, context: &introPagingViewControllerContext)
}
}
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
guard let change = change else { return }
if context != &myContext {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
if keyPath == "contentOffset" {
if let contentOffset = change[NSKeyValueChangeKey.newKey] as? CGPoint {
let screenWidth = UIScreen.main.bounds.width
let percent = abs((contentOffset.x - screenWidth) / screenWidth)
print(percent)
}
}
}

Resources