I'm using a UIPageViewController with Navigation set to Horizontal, Transition Style set to Scroll (in InterfaceBuilder), and no spine. Which gives me a lovely UIPageControl integrated. Now I want to be able to toggle whether it's displaying (because there's artwork underneath it).
I've tried setting presentationCountForPageViewController and presentationIndexForPageViewController to return 0 when the UIPageControl is supposed to be hidden, but those methods aren't being called when I want.
Pausing for stacktrace, I see them being called by [UIPageViewController _updatePageControlViaDataSourceIfNecessary]...I assume my app would be rejected if I tried to use that method.
Should I hunt through subviews for it, or roll my own so I have control over it, or is there some better way to toggle its visibility?
Thanks!
I would say, hunt through the subviews. This code successfully finds the UIPageControl in the subviews hierarchy:
NSArray *subviews = pageController.view.subviews;
UIPageControl *thisControl = nil;
for (int i=0; i<[subviews count]; i++) {
if ([[subviews objectAtIndex:i] isKindOfClass:[UIPageControl class]]) {
thisControl = (UIPageControl *)[subviews objectAtIndex:i];
}
}
I'm using this to customize the color of the dots, I imagine you could do the same with the alpha value or send it to the back or something.
Apple provides no direct interface to the UIPageControl through the UIPageViewController class, but there are no illegal method calls required in order to get to it... I don't see why this would result in an app rejection.
You can access this for all PageControl objects by using appearance (see the UIAppearance protocol), but to get a specific instance you'd have to use recursion. Swift code:
let pageControl = UIPageControl.appearance()
Swift 3 Extension:
extension UIPageViewController {
var pageControl: UIPageControl? {
for view in view.subviews {
if view is UIPageControl {
return view as? UIPageControl
}
}
return nil
}
}
In Swift:
let subviews: Array = self.pageViewController.view.subviews
var pageControl: UIPageControl! = nil
for (var i = 0; i < subviews.count; i++) {
if (subviews[i] is UIPageControl) {
pageControl = subviews[i] as! UIPageControl
break
}
}
I implemented a category to handle this for me which gets the mess out of my code and allows me to access the pageControl via "pageController.pageControl"
Objective-C
// Header
#interface UIPageViewController (PageControl)
#property (nonatomic, readonly) UIPageControl *pageControl;
#end
I also used recursion (handled by blocks) in case Apple decides to change the implementation causing the UIPageControl to not be in the first layer of subviews.
// Implementation
#import "UIPageViewController+PageControl.h"
#implementation UIPageViewController (PageControl)
- (UIPageControl *)pageControl
{
__block UIPageControl *pageControl = nil;
void (^pageControlAssignBlock)(UIPageControl *) = ^void(UIPageControl *blockPageControl) {
pageControl = blockPageControl;
};
[self recurseForPageControlFromSubViews:self.view.subviews withAssignBlock:pageControlAssignBlock];
return pageControl;
}
- (void)recurseForPageControlFromSubViews:(NSArray *)subViews withAssignBlock:(void (^)(UIPageControl *))assignBlock
{
for (UIView *subView in subViews) {
if ([subView isKindOfClass:[UIPageControl class]]) {
assignBlock((UIPageControl *)subView);
break;
} else {
[self recurseForPageControlFromSubViews:subView.subviews withAssignBlock:assignBlock];
}
}
}
#end
This may be overkill for your needs but it worked well for mine
How about a nice, up to date Swift 1-liner?
let pageControl = view.subviews.first { $0 is UIPageControl } as? UIPageControl
Or if you like an extension:
extension UIPageViewController {
var pageControl: UIPageControl? {
return view.subviews.first { $0 is UIPageControl } as? UIPageControl
}
}
For Swift
To get the dots in the page control we can use
//dots will be an array of the dots views
let dots = pageControl.subviews
To get the current dot view
let currentDot = dots[pageControl.currentPage]
To get the other dots views
for i in 0..<dots.count {
let dot = dots[i]
if i == pageControl.currentPage {
//dot => current dot
} else {
//dot => other dot
}
}
After we get the dot view we can change whatever we want like
dot.layer.borderColor = .green
dot.layer.borderWidth = 1
C# extension:
public static class PageViewControllerExtension{
public static UIPageControl GetPageControl(this UIPageViewController pageViewController){
foreach (var view in pageViewController.View.Subviews){
var subView = view as UIPageControl;
if (subView != null){
return subView;
}
}
return null;
}
}
Related
We have a webview in our app to edit some text in a semi-wysiwyg style. It's just a fullscreen text area so we don't want those next/previous/done buttons on the keyboard taking in precious screen space. Previously, the following worked to remove the whole bar where those buttons are shown:
-(void)__removeInputAccessoryView {
UIView* subview;
for (UIView* view in _editorView.scrollView.subviews) {
if([[view.class description] hasPrefix:#"UIWeb"])
subview = view;
}
if(subview == nil) {
return;
}
NSString* name = [NSString stringWithFormat:#"%#RWWebviewHelper", subview.class.superclass];
Class newClass = NSClassFromString(name);
if(newClass == nil) {
newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0);
if(!newClass) {
return;
}
Method method = class_getInstanceMethod([RWWebviewHelper class], #selector(inputAccessoryView));
class_addMethod(newClass, #selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method));
objc_registerClassPair(newClass);
}
object_setClass(subview, newClass);
}
I found similar snippets on SO and Github, both in Objective-C and Swift. I'm currently converting this class from Objective-C to Swift and from using UIWebView to using a WKWebView. I found this on SO somewhere:
private func removeInputAccessoryView() {
final class FauxBarHelper: NSObject {
var inputAccessoryView: AnyObject? { return nil }
}
editorView.scrollView.subviews.forEach { view in
if String(describing: type(of: view)).hasPrefix("WKcon") {
let noInputAccessoryViewClassName = "\(view.superclass!)_NoInputAccessoryView"
var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil {
let targetClass: AnyClass = object_getClass(view)
newClass = objc_allocateClassPair(targetClass, noInputAccessoryViewClassName.cString(using: String.Encoding.ascii)!, 0)
}
let originalMethod = class_getInstanceMethod(FauxBarHelper.self, #selector(getter: FauxBarHelper.inputAccessoryView))
class_addMethod(newClass!.self, #selector(getter: FauxBarHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
object_setClass(view, newClass)
}
}
}
But it doesn't hide the bar. I'm not too well versed in hacking the private API and after playing around with the values in that snippet I can't find a way to hide the bar. I know this is a store rejection risk, but you know how clients are. I need to get this working. I can work with an Objective-C snippet, but prefer Swift.
Does anyone have an idea? I feel it shouldn't be very hard to hide a button bar from an onscreen keyboard, am I missing something simple?
There's a behavior in the Line messenger app (the de facto messenger app in Japan) that I'm trying to emulate.
Basically, they have a modal view controller with a scroll view inside. When the scroll action reaches the top of its content, the view controller seamlessly switches to an interactive dismissal animation. Also, when the gesture returns the view to the top of the screen, control is returned to the scroll view.
Here's a gif of how it looks.
For the life of me, I can't figure out how they did it. I've tried a few different methods, but they've all failed, and I'm out of ideas. Can anyone point me in the right direction?
EDIT2
To clarify, the behavior that I want to emulate isn't just simply dragging the window down. I can do that, no problem.
I want to know how the same scroll gesture (without lifting the finger) triggers the dismissal transition and then transfers control back to the scroll view after the view has been dragged back to the original position.
This is the part that I can't figure out.
End EDIT2
EDIT1
Here's what I have so far. I was able to use the scroll view delegate methods to add a target-selector that handles the regular dismissal animation, but it still doesn't work as expected.
I create a UIViewController with a UIWebView as a property. Then I put it in a UINavigationController, which is presented modally.
The navigation controller uses animation/transition controllers for the regular interactive dismissal (which can be done by gesturing over the navigation bar).
From here, everything works fine, but the dismissal can't be triggered from the scroll view.
NavigationController.h
#interface NavigationController : UINavigationController <UIViewControllerTransitioningDelegate>
#property (nonatomic, strong) UIPanGestureRecognizer *gestureRecog;
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer;
#end
NavigationController.m
#import "NavigationController.h"
#import "AnimationController.h"
#import "TransitionController.h"
#implementation NavigationController {
AnimationController *_animator;
TransitionController *_interactor;
}
- (instancetype)init {
self = [super init];
self.transitioningDelegate = self;
_animator = [[AnimationController alloc] init];
_interactor = [[TransitionController alloc] init];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set the gesture recognizer
self.gestureRecog = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:_gestureRecog];
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
if (animator == _animator && _interactor.hasStarted) {
return _interactor;
}
return nil;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
if (dismissed == self || [self.viewControllers indexOfObject:dismissed] != NSNotFound) {
return _animator;
}
return nil;
}
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecog {
CGFloat threshold = 0.3f;
CGPoint translation = [gestureRecog translationInView:self.view];
CGFloat verticalMovement = translation.y / self.view.bounds.size.height;
CGFloat downwardMovement = fmaxf(verticalMovement, 0.0f);
CGFloat downwardMovementPercent = fminf(downwardMovement, 1.0f);
switch (gestureRecog.state) {
case UIGestureRecognizerStateBegan: {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged: {
if (!_interactor.hasStarted) {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
}
_interactor.shouldFinish = downwardMovementPercent > threshold;
[_interactor updateInteractiveTransition:downwardMovementPercent];
break;
}
case UIGestureRecognizerStateCancelled: {
_interactor.hasStarted = NO;
[_interactor cancelInteractiveTransition];
break;
}
case UIGestureRecognizerStateEnded: {
_interactor.hasStarted = NO;
if (_interactor.shouldFinish) {
[_interactor finishInteractiveTransition];
} else {
[_interactor cancelInteractiveTransition];
}
break;
}
default: {
break;
}
}
}
#end
Now, I have to get that gesture handling to trigger when the scroll view has reached the top. So, here's what I did in the view controller.
WebViewController.m
#import "WebViewController.h"
#import "NavigationController.h"
#interface WebViewController ()
#property (weak, nonatomic) IBOutlet UIWebView *webView;
#end
#implementation WebViewController {
BOOL _isHandlingPan;
CGPoint _topContentOffset;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.webView.scrollView setDelegate:self];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ((scrollView.panGestureRecognizer.state == UIGestureRecognizerStateBegan ||
scrollView.panGestureRecognizer.state == UIGestureRecognizerStateChanged) &&
! _isHandlingPan &&
scrollView.contentOffset.y < self.navigationController.navigationBar.translucent ? -64.0f : 0) {
NSLog(#"Adding scroll target");
_topContentOffset = CGPointMake(scrollView.contentOffset.x, self.navigationController.navigationBar.translucent ? -64.0f : 0);
_isHandlingPan = YES;
[scrollView.panGestureRecognizer addTarget:self action:#selector(handleGesture:)];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(#"Did End Dragging");
if (_isHandlingPan) {
NSLog(#"Removing action");
_isHandlingPan = NO;
[scrollView.panGestureRecognizer removeTarget:self action:#selector(handleGesture:)];
}
}
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer {
[(NavigationController*)self.navigationController handleGesture:gestureRecognizer];
}
This still doesn't work quite right. Even during the dismissal animation, the scroll view is still scrolling with the gesture.
End EDIT1
That is a custom interactive transition.
First, you need set transitioningDelegate of UIViewController
id<UIViewControllerTransitioningDelegate> transitioningDelegate;
Then implment these two method to
//Asks your delegate for the transition animator object to use when dismissing a view controller.
- animationControllerForDismissedController:
//Asks your delegate for the interactive animator object to use when dismissing a view controller.
- interactionControllerForDismissal:
When drag to top, you start the transition, you may use UIPercentDrivenInteractiveTransition to control the progress during scrolling.
You can also refer to the source code of ZFDragableModalTransition
Image of ZFDragableModalTransition
As explained here the solution is quite complex. The person who answered, #trungduc, programmed a little demo published on github doing the sought behaviour. You can find it here.
The easiest way of making this work is to copy the 4 files found in /TestPanel/Presentation/ in the attached github repository, to your project. Then add the PanelAnimationControllerDelegate to your View Controller containing the scroll view (i.e. using the protocol).
Add the following to your View Controller, to satisfy the protocol:
func shouldHandlePanelInteractionGesture() -> Bool {
return (scrollView.contentOffset.y == 0);
}
Add this to deactivate the bouncing effect at the top:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10);
}
Set scrollView.delegate = self
Before presenting your View Controller containing the scroll view set the following propreties to your View Controller:
ScrollViewController.transitioningDelegate = self.panelTransitioningDelegate
ScrollViewController.modalPresentationStyle = .custom
If you want to change the size of your ScrollViewController, you will need to comment out the override of the frameOfPresentedViewInContainerView in the PanelPresentationController file (one of the 4). Then in the presentationTransitionWillBegin method, you will need to set let frameOfPresentedViewInContainerView = self.frameOfPresentedViewInContainerView.insetBy(dx: 0, dy: 20) with the wanted inset of dx and dy.
Thank you to trungduc for this amazing solution!!
I have multiple UIView, then after add and remove button on UIView in last want check my view is empty or not.
above image 3rd view is empty how to check programmatically
this view is empty or not.
You can check the amount of subviews in that specific view:
if([theView.subviews count] == 0) {
// View does not contain subviews
}
If you have multiple UIViews inside a parent view and you wish to figure out which views are empty, then loop over the parent subviews and check each if empty or not:
for(UIView * view in parentView.subviews) {
if([view isKindOfClass:[UIView class]] && [view.subviews count] == 0) {
// We found an empty UIView...
// Can you identify this view?
// If you need to do something with it, do it here.
}
}
Try This:
extension UIView {
var isViewEmpty : Bool {
return self.subviews.count == 0 ;
}
}
Paste extension code outside of the viewController class.
After removing button from view, every time check for isViewEmpty as below,
//if you don't have the object of view, you can get view as below,
let view = bottonToRemove.superview;//this will give you obejct for check
//your code to remove button from the view
if view.isViewEmpty {
//implement your logic for if view is empty
}else{
//view not empty
//do you stuff
}
UIView has a property subViews. This will return you an array of all subViews. If the array is null or has count zero then there is no subview in it.
i think as per your requirement you need check there is any button in view or not you can try
BOOL isEmpty = true;
for (UIButton *btn in viewTest.subviews) {
if ([btn isKindOfClass:[UIButton class]]) {
isEmpty = false;
break;
}
}
if (isEmpty == false) {
// there is button in view
}
else
{
// there no button in view
}
Seems like you are checking on the UINavigationBar,
for (UIView *view in self.navigationController.navigationBar.subviews) {
if(view)
//Do your thing
}
I have a UIView, let's say MyView which contains a UILabel, UISegmentControl and a UIButton.
So
MyView.Hidden = false;
and
MyView.Tag = 1000;
to make sure it doesn't touch it.
now when I click outside that view I want it to hide, to that end I have some code which adds tap recognisers, ignores MyView (I hoped) and hides the view when you click outside. It removes the tab recognisers until MyView shows up again. It works, however when I click on the subviews of MyView (but NOT MyView itself) it calls ViewTap as well and thus disappears it as well. I cannot find why this happens...
class ViewTap : OutsideEditTap
{
public ViewTap(UIView MainView) {
this.AddTarget(() => {
MyView.Hidden = true;
RemoveTapGesture(this.View);
});
CancelsTouchesInView = false;
}
}
class OutsideEditTap : UITapGestureRecognizer
{
}
public static void RemoveTapGesture(UIView view) {
if (view.GestureRecognizers != null) for (int i = 0; i < view.GestureRecognizers.Length; i++) {
if (view.GestureRecognizers[i].IsSubClassOf(typeof(OutsideEditTap))) {
view.RemoveGestureRecognizer (view.GestureRecognizers [i]);
}
}
foreach (UIView subView in view.Subviews) {
RemoveTapGesture (subView);
}
}
public static void AddTapGesture(UIView view, UIView MainView) {
OutsideEditTap tap = null;
if (view.Tag != 1000) { // skip MyView
tap = new ViewTap (MainView);
view.AddGestureRecognizer (tap);
foreach (UIView subView in view.Subviews) {
AddTapGesture (subView, MainView);
}
}
}
I am sure this is probably not the best method (I don't think the whole idea was not the best implementation to begin with, but no-one provided any alternatives unfortunately); the solution I used to solve this was to add to the AddTarget Action:
var p = this.LocationOfTouch(0, this.View);
if (MyView.Frame.Contains(p)) { return; }
which works well. I would still like to hear a better way of doing this.
I have to remove this bar as here link but for iOS 7 this code does not work.
We remove this bar with some Objective C runtime trickery.
We have a class which has one method:
#interface _SwizzleHelper : NSObject #end
#implementation _SwizzleHelper
-(id)inputAccessoryView
{
return nil;
}
#end
Once we have a web view which we want to remove the bar from, we iterate its scroll view's subviews and take the UIWebDocumentView class. We then dynamically make the superclass of the class we created above to be the subview's class (UIWebDocumentView - but we cannot say that upfront because this is private API), and replace the subview's class to our class.
#import "objc/runtime.h"
-(void)__removeInputAccessoryView
{
UIView* subview;
for (UIView* view in self.scrollView.subviews) {
if([[view.class description] hasPrefix:#"UIWeb"])
subview = view;
}
if(subview == nil) return;
NSString* name = [NSString stringWithFormat:#"%#_SwizzleHelper", subview.class.superclass];
Class newClass = NSClassFromString(name);
if(newClass == nil)
{
newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0);
if(!newClass) return;
Method method = class_getInstanceMethod([_SwizzleHelper class], #selector(inputAccessoryView));
class_addMethod(newClass, #selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method));
objc_registerClassPair(newClass);
}
object_setClass(subview, newClass);
}
The equivalent of the above in Swift 3.0:
import UIKit
import ObjectiveC
var swizzledClassMapping = [AnyClass]()
extension UIWebView {
func noInputAccessoryView() -> UIView? {
return nil
}
public func removeInputAccessoryView() {
var subview: AnyObject?
for (_, view) in scrollView.subviews.enumerated() {
if NSStringFromClass(type(of: view)).hasPrefix("UIWeb") {
subview = view
}
}
guard subview != nil else {
return
}
//Guard in case this method is called twice on the same webview.
guard !(swizzledClassMapping as NSArray).contains(type(of: subview!)) else {
return;
}
let className = "\type(of: subview!)_SwizzleHelper"
var newClass : AnyClass? = NSClassFromString(className)
if newClass == nil {
newClass = objc_allocateClassPair(type(of: subview!), className, 0)
guard newClass != nil else {
return;
}
let method = class_getInstanceMethod(type(of: self), #selector(UIWebView.noInputAccessoryView))
class_addMethod(newClass!, #selector(getter: UIResponder.inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method))
objc_registerClassPair(newClass!)
swizzledClassMapping += [newClass!]
}
object_setClass(subview!, newClass!)
}
}
I've made a cocoapod based on this blog post from #bjhomer.
You can replace the inputaccessoryview and not just hide it. I hope this will help people with the same issue.
https://github.com/lauracpierre/FA_InputAccessoryViewWebView
You can find the cocoapod page right here.
I've came across this awesome solution, but I needed to get the inputAccessoryView back as well. I added this method:
- (void)__addInputAccessoryView {
UIView* subview;
for (UIView* view in self.scrollView.subviews) {
if([[view.class description] hasSuffix:#"SwizzleHelper"])
subview = view;
}
if(subview == nil) return;
Class newClass = subview.superclass;
object_setClass(subview, newClass);
}
It does seem to work as intended with no side effects, but I can't get rid of the feeling that my pants are on fire.
If you want Leo Natan's solution to work with WKWebView instead of UIWebView just change prefix from "UIWeb" to "WKContent".
I created a gist to accomplish this:
https://gist.github.com/kgaidis/5f9a8c7063b687cc3946fad6379c1a66
It's a UIWebView category where all you do is change the customInputAccessoryView property:
#interface UIWebView (CustomInputAccessoryView)
#property (strong, nonatomic) UIView *customInputAccessoryView;
#end
You can either set it to nil to remove it or you can set a new view on it to change it.
Keep in mind, this also uses private API's, so use at your own risk, but it seems like a lot of apps do similar things nonetheless.