Make scrollbar always visible on UIScrollView? - iphone

I need to make a scrollbar always visible on viewDidLoad so that the user can understand that there is content to scroll. I did the following:
[myscrollView flashScrollIndicators];
But then the scrollbars only appear for some time after viewDidLoad and disappear again only to reappear when the user touches the screen..
I need to make scrollbars always visible. How can I do it?

Apple indirectly discourage constantly displaying scroll indicators in their iOS Human Interface Guidelines but guidelines are just guidelines for a reason, they don't account for every scenario and sometimes you may need to politely ignore them.
The scroll indicators of any content views are UIImageView subviews of those content views. This means you can access the scroll indicators of a UIScrollView as you would any of its other subviews (i.e. myScrollView.subviews) and modify the scroll indicators as you would any UIImageView (e.g. scrollIndicatorImageView.backgroundColor = [UIColor redColor];).
The most popular solution appears to be the following code:
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
Which is originally credited to this source.
This defines a category for UIImageView that defines a custom setter for the alpha property. This works because at some point in the underlying code for the UIScrollView, it will set its scroll indicator's alpha property to 0 in order to hide it. At this point it will run through our category and, if the hosting UIScrollView has the right tag, it will ignore the value being set, leaving it displayed.
In order to use this solution ensure your UIScrollView has the appropriate tag e.g.
If you want to display the scroll indicator from the moment its UIScrollView is visible simply flash the scroll indicators when the view appears .e.g
- (void)viewDidAppear:(BOOL)animate
{
[super viewDidAppear:animate];
[self.scrollView flashScrollIndicators];
}
Additional SO references:
UIScrollView - showing the scroll bar
UIScrollView indicator always show?
Scroll Indicators Visibility
Make scrollbars always visible in uiscrollview

I want to offer my solution. I don't like the most popular variant with category (overriding methods in category can be the reason of some indetermination what method should be called in runtime, since there is two methods with the same selector).
I use swizzling instead. And also I don't need to use tags.
Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
The scroll indicator (vertical scroll indicator in this case) will be always at the screen.
Update November, 2019
Starting from iOS 13 UIScrollView subclasses are changed. Now scroll indicators are inherited from UIView and has their own private class called _UIScrollViewScrollIndicator. This means, that they are not subclasses of UIImageView now, so old method won't work anymore.
Also we are not able to implement subclass of _UIScrollViewScrollIndicator because it is private class and we don't have access to it. So the only solution is to use runtime. Now to have support for iOS 13 and earlier implement the next steps:
Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
} else if ([NSStringFromClass(view.class) isEqualToString:#"_UIScrollViewScrollIndicator"]) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found scroll indicator, (be sure to create AlwaysOpaqueScrollIndicator in runtime earlier!)
// Current implementation is in AlwaysOpaqueScrollTableView class
object_setClass(view, NSClassFromString(#"AlwaysOpaqueScrollIndicator"));
break;
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class (this is for iOS earlier than 13)
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
Add these methods somewhere in you code (either the same view controller as in step 1, or to the desired UIScrollView subclass).
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Create child class from _UIScrollViewScrollIndicator since it is private
Class alwaysOpaqueScrollIndicatorClass = objc_allocateClassPair(NSClassFromString(#"_UIScrollViewScrollIndicator"), "AlwaysOpaqueScrollIndicator", 0);
objc_registerClassPair(alwaysOpaqueScrollIndicatorClass);
// Swizzle setAlpha: method of this class to custom
Class replacementMethodClass = [self class];
SEL originalSelector = #selector(setAlpha:);
SEL swizzledSelector = #selector(alwaysOpaque_setAlpha:);
Method originalMethod = class_getInstanceMethod(alwaysOpaqueScrollIndicatorClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(replacementMethodClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(alwaysOpaqueScrollIndicatorClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(alwaysOpaqueScrollIndicatorClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)alwaysOpaque_setAlpha:(CGFloat)alpha {
[self alwaysOpaque_setAlpha:1.0];
}
This step creates the subclass of _UIScrollViewScrollIndicator called AlwaysOpaqueScrollIndicator in runtime and swizzle setAlpha: method implementation to alwaysOpaque_setAlpha:.
Do not forget to add
#import <objc/runtime.h>
to the files you've inserted this code. Thanks to #Smartcat for reminder about this

I dont know whether this will work or not. But just a hint for you.
Scrollbar inside the Scrollview is a Imageview. Which is a subview of UIScrollview
So get the Scrollbar Imageview of the UIscrollview. Then try to set that image property hidden to NO or Change Alpha value
static const int UIScrollViewHorizontalBarIndexOffset = 0;
static const int UIScrollViewVerticalBarIndexOffset = 1;
-(UIImageView *)scrollbarImageViewWithIndex:(int)indexOffset
{
int viewsCount = [[yourScrollview subviews] count];
UIImageView *scrollBar = [[yourScrollview subviews] objectAtIndex:viewsCount - indexOffset - 1];
return scrollBar;
}
-(void) viewDidLoad
{
//Some Code
//Get Scrollbar
UIImageView *scrollBar = [self scrollbarImageViewWithIndex: UIScrollViewVerticalBarIndexOffset];
//The try setting hidden property/ alpha value
scrollBar.hidden=NO;
}
Got reference from here

This is Swift version of #Accid Bright's answer:
class AlwaysOpaqueImageView: UIImageView {
override var alpha: CGFloat {
didSet {
alpha = 1
}
}
static func setScrollbarToAlwaysVisible(from scrollView: UIScrollView) {
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for view in scrollView.subviews {
if view.isKind(of: UIImageView.self),
view.alpha == 0 && view.autoresizingMask == UIView.AutoresizingMask.flexibleLeftMargin,
view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width,
scrollView.frame.size.height < scrollView.contentSize.height {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, AlwaysOpaqueImageView.self)
break
}
}
// Ask to flash indicator to turn it on
scrollView.flashScrollIndicators()
}
}
One difference is that setting scrollbar is extracted out as a static method.

Related

Getting the lowest view on UIView using hitTest:?

So my view look like this from lowest view
[L] UIView -> [K] view -> [M] view
Since [K] is under [M] in some cases, when I do UIView hitTest: check on [L], it always returns [M] view.
Is there any way to get the lowest subview from hitTest: ?
Thanks.
I hope I understood your question:
You have the following view hierarchy and View 1 is covered by View 2
+ Root View
| - View 1
| - View 2
Then you perform -[UIView hitTest:withEvent:] on the Root View, knowing the point lies inside View 1 as well as inside View 2. But this method returns View 2 because it is on the top.
If you want to get View 1 instead you can use the following:
#implementation UIView (ExtendedHitTest)
- (UIView *)extendedHitTest:(CGPoint)point withEvent:(UIEvent *)event
{
__block UIView *result;
NSArray *hitTestSiblings = [self hitTest:point withEvent:event].superview.subviews;
[hitTestSiblings enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
result = view;
*stop = YES;
}
}];
return result;
}
#end
With anUIView.superview.subviews you get all the siblings of anUIView with the topmost view as the last object—this means you'd first encounter View 1 and then View 2
You can traverse the superviews to find out the top one.
// Assume self is a UIView. If it's a UIViewController, replace self with self.view
UIView* v = [self hitTest:p withEvent:e];
if (v == self) {
// The current view has been hit ...
}
while (v.superview != self) {
v = v.superview;
}
// Now v is the view that you wanted

Customizing UIPickerView

I have a requirement where UIPickerView should be customized. The picker view should look something like this:
The application which has customized the pickerView similarly is:
http://itunes.apple.com/us/app/convert-the-unit-calculator/id325758140?mt=8
I have tried removing the default pickerView selection bar by resetting the property showsSelectionIndicator of UIImagePicker and adding a overlay view. But the problem is, the overlay view should be transparent so that the wheel behind it is visible. But the other application somehow does it even though the selection bar is not transparent.
Any ideas on how to achieve this feat?
Thanks and Regards,
Raj
You're going to have to write your own from scratch on this one. UIPickerview isn't customizable. At. All. Sucks, but that's how it is. I'd start out creating a uitableview and layering a frame around it, and trying to mimic uipickerview.
I think you can print the subviews under the picker and modify them.
The UIPickerView create subviews after the data is first loaded.
With performSelecter:WithObject:afterDelay:, you can remove them or insert whatever you need.
- (void)viewDidLoad
{
[super viewDidLoad];
[self refreshClock];
[timePicker_ performSelector:#selector(leakSelf) withObject:nil afterDelay:0];
[self performSelector:#selector(setupTimePicker) withObject:nil afterDelay:0];
}
- (void)setupTimePicker {
[[timePicker_ subviewOfClass:NSClassFromString(#"_UIPickerViewTopFrame")] removeFromSuperview];
CGRect frame = timePicker_.frame;
frame.size.width += 20;
frame.origin.x -= 10;
timePicker_.frame = frame;
}
#implementation UIView(UIViewDebugTool)
- (void)leakSubview:(UIView*)subroot atDepth:(NSUInteger)dep {
DLog( #"trace sub view[%u] %#", dep, [subroot description] );
CALayer* layer = subroot.layer;
for( CALayer* l in layer.sublayers ) {
DLog( #"layer[%u] %#", dep, l );
}
for( UIView* v in subroot.subviews ) {
[self leakSubview:v atDepth:dep+1];
}
}
- (void)leakSelf {
NSUInteger dep = 0;
[self leakSubview: self atDepth:dep];
}
#end

Allowing user to select a UIPickerView row by tapping

I'm trying to use a UIPicker View with a behavior somehow different of what's usually seen in iPhone code examples.
What I want to do is to allow users to scroll through the picker contents, but not to select a picker's row automatically (using the "didSelectRow" method from picker's delegate). Instead, I want to allow the user to touch the center row of the picker, which gets highlighted, and becomes the selection.
Is there any way to achieve this?
Thanks in advance.
Add a gesture recogniser to the UIPickerView which triggers a target method in your object:
myGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(pickerTapped:)];
[myPicker addGestureRecognizer:myGR];
// target method
-(void)pickerTapped:(id)sender
{
// your code
}
make a new UIControl
same position as the UIPickerView
[yourcontrol setBackGroundColor: [UIColor clearColor]];
make a method
- (IBAction) pickerControlTapped
{
[yourpicker selectRow: rand()% yourpickersize
inComponent: 0
animated: YES];
}
.3. make a connection between 1 and 2
[yourcontrol addTarget: self
action: #selector(pickerControlTapped)
forControlEvents: UIControlEventTouchUpInsied];
Building on Martin Linklater's answer to support tapping on the pickers other rows:
Has some magic numbers but works for me.
- (void) pickerTapped:(UITapGestureRecognizer*)gestureRecognizer
{
CGPoint location = [gestureRecognizer locationInView:self.pickerView];
CGFloat halfViewHeight = self.pickerView.frame.size.height / 2;
NSInteger row = -1;
if (location.y < halfViewHeight - 22
&& location.y > halfViewHeight - 66)
{
row = [self.pickerView selectedRowInComponent:0] - 1;
}
else if (location.y < halfViewHeight + 22
&& location.y > halfViewHeight - 22)
{
row = [self.pickerView selectedRowInComponent:0];
}
else if (location.y < halfViewHeight + 66
&& location.y > halfViewHeight + 22)
{
row = [self.pickerView selectedRowInComponent:0] + 1;
}
if (row >= 0 && row < [self.content count])
{
id element = [self.content objectAtIndex:row];
if (element)
{
[self.pickerView selectRow:row inComponent:0 animated:YES];
// do more stuff
}
}
}
I have a relatively simple solution to this problem that has worked well for me. Using a hidden custom button you can achieve the tap functionality without a gesture recogniser. This solution works for a picker with one component, however I'm sure it could be adapted to work with more.
Firstly add a button, either in the Interface Builder or programatically. Make it hidden and as wide as the picker then place it so that it sits exactly in the centre of the picker and also in front of it in the view hierarchy.
I'm using an IBAction like this to show my picker. However it's really up to you how you show and hide the picker.
- (IBAction)showPicker:(id)sender
{
_picker.hidden = NO;
_buttonPicker.hidden = NO;
}
All the action for choosing the picker value happens in an IBAction for the UIControlEventTouchUpInside event, something like this.
- (IBAction)selectPicker:(id)sender
{
//Hide the button so that it doesn't get in the way
_buttonPicker.hidden = YES;
//Make sure we're within range
NSInteger max = _values.count;
NSInteger row = [_picker selectedRowInComponent:0];
if(row >= 0 && row < max) {
NSString *value = [_values objectAtIndex:row];
//Set the label value and hide the picker
_label.text = value;
_picker.hidden = YES;
}
}
I've slightly modified the code for this answer from working code so apologies if it's broken at all.
There are only 2 delegates for UIPickerView.
UIPickerViewDelegate
UIPickerViewDataSource
So, we can use only 7 methods to control UIPickerView by delegate.
– pickerView:rowHeightForComponent:
– pickerView:widthForComponent:
– pickerView:titleForRow:forComponent:
– pickerView:viewForRow:forComponent:reusingView:
– pickerView:didSelectRow:inComponent:
– numberOfComponentsInPickerView:
– pickerView:numberOfRowsInComponent:
that'all.
In UITableViewDelegate case, there are more methods for UITableView for managing selections.
such as,
– tableView:willSelectRowAtIndexPath:
– tableView:didSelectRowAtIndexPath:
– tableView:willDeselectRowAtIndexPath:
– tableView:didDeselectRowAtIndexPath:
However...
In UIPickerViewDelegate case, there is only 1 method for responding to row selection.
– pickerView:didSelectRow:inComponent:

Remove UIWebView Shadow?

Does anyone know if its possible to remove the shadow that is placed on the UIWebView window?
Example: http://uploadingit.com/files/1173105_olub5/shadow.png
If its possible how do you do it?
Thanks
This is a cleaner alternative to "Nikolai Krill" solution. This only hides UIImageViews within the UIWebView and not the UIWebBrowserView.
for (UIView *view in [[[webView subviews] objectAtIndex:0] subviews]) {
if ([view isKindOfClass:[UIImageView class]]) view.hidden = YES;
}
Thanks
James
the small for loop is very dangerous because it can crash if apple changes the number of the subviews.
this way it does at least not crash when something changes:
if ([[webView subviews] count] > 0)
{
for (UIView* shadowView in [[[webView subviews] objectAtIndex:0] subviews])
{
[shadowView setHidden:YES];
}
// unhide the last view so it is visible again because it has the content
[[[[[webView subviews] objectAtIndex:0] subviews] lastObject] setHidden:NO];
}
There is a private method with the selector setAllowsRubberBanding: that takes a BOOL value. If passed NO, you will not be able to scroll the web view past the top or bottom of the content area, but will still let you scroll through the web view normally. Unfortunately, this method IS private, and your app will likely not be allowed onto the store if you use it.
You could, however, potentially try and extract the method implementation and bind it to a different selector that you've created, using the dynamic nature of Objective-C's runtime.
Still, the method is private and may no longer exist in future versions of the OS. If you still want to try, here's some sample code that will extract the setAllowsRubberBanding: method implementation and call it for you.
static inline void ShhhDoNotTellAppleAboutThis (UIWebView *webview)
{
const char *hax3d = "frgNyybjfEhooreOnaqvat";
char appleSelName[24];
for (int i = 0; i < 22; ++i)
{
char c = hax3d[i];
appleSelName[i] = (c >= 'a' && c <= 'z') ? ((c - 'a' + 13) % 26) + 'a' : ((c - 'A' + 13) % 26) + 'A';
}
appleSelName[22] = ':';
appleSelName[23] = 0;
SEL appleSEL = sel_getUid(appleSelName);
UIScrollView *scrollView = (UIScrollView *)[webview.subviews objectAtIndex:0];
Class cls = [scrollView class];
if (class_respondsToSelector(cls, appleSEL) == NO)
{
return;
}
IMP func = class_getMethodImplementation(cls, appleSEL);
func(scrollView, appleSEL, NO);
}
Please note that this will probably still get caught by Apple's static analyzer if you choose to submit an app using this code to the AppStore.
Here is a Swift function that gets rid of the shadow in a UIWebView in iOS 9. It’s safer than any alternative I’ve seen on SO because everything in it is in Apple documentation, and it specifically alters the shadow property (as opposed to hiding the entire view or some other property of the view).
func removeShadow(webView: UIWebView) {
for subview:UIView in webView.scrollView.subviews {
subview.layer.shadowOpacity = 0
for subsubview in subview.subviews {
subsubview.layer.shadowOpacity = 0
}
}
}
You can always access the subviews property of a UIView(documentation). Every UIView has a layer property that is a CALayer (documentation). Every CALayer has shadowOpacity (documentation).
Caveats:
You might have to go deeper in navigating the view hierarchy through subviews depending on your situation.
This works as long as you don’t want any shadows anywhere in the web view controller. If you have a view where you want to keep the shadow (other than the default UIWebView shadow), then you could add an if-check to identify that view and not set that view’s layer’s shadowOpacity to zero.
According to Apple “For complex views declared in UIKit and other system frameworks, any subviews of the view are generally considered private and subject to change at any time. Therefore, you should not attempt to retrieve or modify subviews for these types of system-supplied views. If you do, your code may break during a future system update” . . . in other words, UIWebView can change and its not recommended to be digging into these subviews. However, digging into the UIWebView is the only way to get rid of the shadow and this is a relatively safe way to do it.
This can be done without use of private APIs. All you need to do is hide each UIImageView with the shadow in it. Heres the code:
for (int x = 0; x < 10; ++x) {
[[[[[webView subviews] objectAtIndex:0] subviews] objectAtIndex:x] setHidden:YES];
}
Try this
func webViewDidFinishLoad(_ webView: UIWebView) {
for shadowView in self.webView.scrollView.subviews {
if !shadowView.isKind(of: UIImageView.self) {
shadowView.subviews[0].layer.shadowColor = UIColor.clear.cgColor
} else {
shadowView.layer.shadowColor = UIColor.clear.cgColor
}
}
}
Traverse all subviews, the UIImageViews whose image is only 1 pixel wide are shadow images, you can hide them.
- (void)hideShadows {
[webview traverseViewsWithBlock:^(UIView *view) {
UIImageView *imgView = ([view isKindOfClass:[UIImageView class]] ? (UIImageView*)view : nil;
// image views whose image is 1px wide are shadow images, hide them
if (imgView && imgView.image.size.width == 1) {
imgView.hidden = YES;
}
}];
}
traverseViewsWithBlock does what it looks like:
- (void)traverseViewsWithBlock:(void (^)(UIView* view))block
{
block(self);
for (id subview in self.subviews) {
[subview traverseViewsWithBlock:block];
}
}
I looked at the class properties and didn't find anything there but I can think of two "cover up" strategies:
1. You can use another view (parent of the web view) to clip the webview bounds.
2. You can add another view on top of the webview to cover the needed area with a color that matches the background, you can use an uiimage with a transparent area in the center.
By the way I don't like this standard background of the table views :P, but changing it can be a pain in the ass :P
You have to be careful, the scroll indicators are UIImageViews as well.
I'll improve my code, but here's a basic subclassed solution:
http://forrst.com/posts/A_tiny_UIWebView_hack_remove_shadows_from_behi-gzH
The easiest way to hide scroll indicators and transparent the web view here in UIWebView
To remove the scrolls.
for(UIView *view in webView.subviews){ 
     if ([view isKindOfClass:[UIScrollView class]]) {
          UIScrollView *sView = (UIScrollView *)view;
          //to hide verticalScroller
          sView.showsVerticalScrollIndicator = NO;
sView.showsVerticalScrollIndicator = NO;
     }
}
What about a category on UIWebView like this:
- (BOOL)showsScrollShadows
{
for(UIImageView *imageView in [self imageViewsWithShadows])
{
if(imageView.hidden)
{
return NO;
}
break;
}
return YES;
}
- (void)setShowsScrollShadows:(BOOL)showsScrollShadows
{
[[self imageViewsWithShadows] makeObjectsPerformSelector:#selector(setHidden:) withObject:#(!showsScrollShadows)];
}
- (NSArray *)imageViewsWithShadows
{
NSArray *potentialShadowImageViews = (self.subviews.count > 0) ? [self.subviews[0] subviews] : nil;
if(potentialShadowImageViews.count > 0)
{
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
{
return [evaluatedObject isKindOfClass:[UIImageView class]];
}];
return [potentialShadowImageViews filteredArrayUsingPredicate:predicate];
}
return nil;
}
I've had a look around and can't see anything related to it. Apart from masking it with a view or clipping it somehow, the only thing I can think of is to loop through all of the UIWebView subviews (and sub-subviews etc.) and see if you can see anything there!
I may be wrong, but I think the shadow only shows up when we scroll the webview doesn't it ?
In that case, do you want to prevent the scrolling or really hide the shadow ? I don't know any tips that would hide the shadow. To disable the scrolling, I would setUserInteractionEnabled to NO.
I added a recursive method as a category to the UIView object so that it will do a depth-first walk of the subviews of the method's receiving view, hiding any UIImageView subclasses it finds. It will not crash if there are no subviews. The -apply: method is from BlocksKit. You could rewrite this function not to use it, but the block is applied in parallel to each element of the receiving array, so it's pretty fast.
#implementation UIView (RemoveShadow)
- (void)removeShadow {
if (self.subviews.count == 0 && [self isKindOfClass:[UIImageView class]]) {
self.hidden = YES;
} else if (self.subviews.count > 0) {
[self.subviews apply:^(id sender) {
[(UIView *)sender removeShadow];
}];
}
}
#end
if (UIDevice.currentDevice.systemVersion.intValue < 7)
for (UIImageView *imageView in webView.scrollView.subviews)
if ([imageView isKindOfClass:[UIImageView class]] && imageView.image.size.width == 1)
imageView.hidden = YES;

Handling touch events within a child UIScrollView

I'm displaying a series of images in a UIScrollView. I pretty much want to replicate the Photos application.
My current architecture is:
A parent UIScrollView with content size that is wide enough for x number of pages + some extra space for margins in between the images.
Each image is contained in a UIImageView.
Each UIImageView is contained within its own UIScrollview which are then the subviews of the parent UIScrollView.
So I basically have a row of UIScrollViews within a parent UIScrollView.
The parent UIScrollView has paging enabled so I can scroll from page to page without any problems.
The problem is how to seamlessly pan around a zoomed-in image. I have overridden the viewForZoomingInScrollView method to return the appropriate UIImageView when the user pinches in/out. I have overriden the scrollViewDidEndZooming method to set the parent view's canCancelContentTouches property to NO if the zoom scale is greater than 1.
So users are able to pan around an image. However, they must hold their finger down for a moment to get past the small delay the parent scroll view has before sending touch events down to the subviews. Also, once the user is panning in one image, the next/prev images do not enter the viewable area when the user has reached the border of the current image.
Any ideas?
Thanks.
Yay! I tried to approach the problem with just one UIScrollView, and I think I found a solution.
Before the user starts zooming (in viewForZoomingInScrollView:), I switch the scroll view into zooming mode (remove all additional pages, reset content size and offset). When the user zooms out to scale 1.00 (in scrollViewDidEndZooming:withView:atScale:), I switch back to paging view (add all pages back, adjust content size and offset).
Here's the code of a simple view controller that does just that. This sample switches between, zooms and pans three large UIImageViews.
Note that a single view controller with a handful of functions is all it takes, no need to subclass UIScrollView or something.
typedef enum {
ScrollViewModeNotInitialized, // view has just been loaded
ScrollViewModePaging, // fully zoomed out, swiping enabled
ScrollViewModeZooming, // zoomed in, panning enabled
ScrollViewModeAnimatingFullZoomOut, // fully zoomed out, animations not yet finished
ScrollViewModeInTransition, // during the call to setPagingMode to ignore scrollViewDidScroll events
} ScrollViewMode;
#interface ScrollingMadnessViewController : UIViewController <UIScrollViewDelegate> {
UIScrollView *scrollView;
NSArray *pageViews;
NSUInteger currentPage;
ScrollViewMode scrollViewMode;
}
#end
#implementation ScrollingMadnessViewController
- (void)setPagingMode {
NSLog(#"setPagingMode");
if (scrollViewMode != ScrollViewModeAnimatingFullZoomOut && scrollViewMode != ScrollViewModeNotInitialized)
return; // setPagingMode is called after a delay, so something might have changed since it was scheduled
scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
// reposition pages side by side, add them back to the view
CGSize pageSize = scrollView.frame.size;
NSUInteger page = 0;
for (UIView *view in pageViews) {
if (!view.superview)
[scrollView addSubview:view];
view.frame = CGRectMake(pageSize.width * page++, 0, pageSize.width, pageSize.height);
}
scrollView.pagingEnabled = YES;
scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = NO;
scrollView.contentSize = CGSizeMake(pageSize.width * [pageViews count], pageSize.height);
scrollView.contentOffset = CGPointMake(pageSize.width * currentPage, 0);
scrollViewMode = ScrollViewModePaging;
}
- (void)setZoomingMode {
NSLog(#"setZoomingMode");
scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
CGSize pageSize = scrollView.frame.size;
// hide all pages besides the current one
NSUInteger page = 0;
for (UIView *view in pageViews)
if (currentPage != page++)
[view removeFromSuperview];
// move the current page to (0, 0), as if no other pages ever existed
[[pageViews objectAtIndex:currentPage] setFrame:CGRectMake(0, 0, pageSize.width, pageSize.height)];
scrollView.pagingEnabled = NO;
scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = YES;
scrollView.contentSize = pageSize;
scrollView.contentOffset = CGPointZero;
scrollViewMode = ScrollViewModeZooming;
}
- (void)loadView {
CGRect frame = [UIScreen mainScreen].applicationFrame;
scrollView = [[UIScrollView alloc] initWithFrame:frame];
scrollView.delegate = self;
scrollView.maximumZoomScale = 2.0f;
scrollView.minimumZoomScale = 1.0f;
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"red.png"]];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"green.png"]];
UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"yellow-blue.png"]];
// in a real app, you most likely want to have an array of view controllers, not views;
// also should be instantiating those views and view controllers lazily
pageViews = [[NSArray alloc] initWithObjects:imageView1, imageView2, imageView3, nil];
self.view = scrollView;
}
- (void)setCurrentPage:(NSUInteger)page {
if (page == currentPage)
return;
currentPage = page;
// in a real app, this would be a good place to instantiate more view controllers -- see SDK examples
}
- (void)viewDidLoad {
scrollViewMode = ScrollViewModeNotInitialized;
[self setPagingMode];
}
- (void)viewDidUnload {
[pageViews release]; // need to release all page views here; our array is created in loadView, so just releasing it
pageViews = nil;
}
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(setPagingMode) object:nil];
CGPoint offset = scrollView.contentOffset;
NSLog(#"scrollViewDidScroll: (%f, %f)", offset.x, offset.y);
if (scrollViewMode == ScrollViewModeAnimatingFullZoomOut && ABS(offset.x) < 1e-5 && ABS(offset.y) < 1e-5)
// bouncing is still possible (and actually happened for me), so wait a bit more to be sure
[self performSelector:#selector(setPagingMode) withObject:nil afterDelay:0.1];
else if (scrollViewMode == ScrollViewModePaging)
[self setCurrentPage:roundf(scrollView.contentOffset.x / scrollView.frame.size.width)];
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)aScrollView {
if (scrollViewMode != ScrollViewModeZooming)
[self setZoomingMode];
return [pageViews objectAtIndex:currentPage];
}
- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
NSLog(#"scrollViewDidEndZooming: scale = %f", scale);
if (fabsf(scale - 1.0) < 1e-5) {
if (scrollView.zoomBouncing)
NSLog(#"scrollViewDidEndZooming, but zoomBouncing is still true!");
// cannot call setPagingMode now because scrollView will bounce after a call to this method, resetting contentOffset to (0, 0)
scrollViewMode = ScrollViewModeAnimatingFullZoomOut;
// however sometimes bouncing will not take place
[self performSelector:#selector(setPagingMode) withObject:nil afterDelay:0.2];
}
}
#end
Runnable sample project is available at http://github.com/andreyvit/ScrollingMadness/ (if you don't use Git, just click Download button there). A README is available there, explaining why the code was written the way it is.
(The sample project also illustrates how to zoom a scroll view programmatically, and has ZoomScrollView class that encapsulates a solution to that. It is a neat class, but is not required for this trick. If you want an example that does not use ZoomScrollView, go back a few commits in commit history.)
P.S. For the sake of completeness, there's TTScrollView — UIScrollView reimplemented from scratch. It's part of the great and famous Three20 library. I don't like how it feels to the user, but it does make implementing paging/scrolling/zooming dead simple.
P.P.S. The real Photo app by Apple has pre-SDK code and uses pre-SDK classes. One can spot two classes derived from pre-SDK variant of UIScrollView inside PhotoLibrary framework, however it is not immediately clear what they do (and they do quite a lot). I can easily believe this effect used to be harder to achieve in pre-SDK times.
I had to do a similar setup, but I basically custom wrote the whole thing. I'm not sure how you're going to get around the problem of 'handing off' touch events from the child UIScrollView to the parent UISscrollView when you reach the edge. You might try overriding UITouchesBegan:withEvent: in your parent UIScrollView, and dumping directly to the child. Good luck!
This is the Andrey's code with c# translation for monotouch developers...
First you need to edit your xib file.. Insert 3 view controllers and make outlets like _page1, _page2, _mainpage.. and link these outlets with views. note you must referance to view controller's view outlet with _mainpage view. (sorry for my english)
public partial class Test_Details_Controller : UIViewController
{
private UIPageControl _pageCont;
private UIScrollView _scView;
private Object[] _pageViews;
private int _currentPageIndex;
private bool _rotationInProgress;
void InitializeAfterLoad ()
{
this.Title = "Test";
this._pageCont = CreatePageControll();
}
private UIPageControl CreatePageControll()
{
UIPageControl pageControll = new UIPageControl( new RectangleF( 146,348, 38, 20 ) );
pageControll.BackgroundColor = UIColor.Red;
pageControll.Alpha = 0.7f;
return pageControll;
}
private void UpdatePageControll(UIPageControl cont, int current, int pages, UIView showed)
{
cont.CurrentPage = current;
cont.Pages = pages;
cont.UpdateCurrentPageDisplay();
UIPageControl.AnimationsEnabled = true;
UIPageControl.BeginAnimations(string.Empty, this.Handle);
cont.Frame = new RectangleF(showed.Frame.Location.X
, cont.Frame.Location.Y , pageSize().Width, cont.Frame.Height);
UIPageControl.CommitAnimations();
}
private UIView loadViewForPage(int pageIndex){
UIView _view = null;
switch ( pageIndex ) {
case 1:
_view = this._page1;
break;
case 2:
_view = this._page2;
break;
default:
_view = this._page1;
break;
}
return _view;
}
private int numberOfPages(){
return (int)this._pageViews.Count();
}
private UIView viewForPage( int pageIndex ){
UIView pageView;
if(this._pageViews.ElementAt( pageIndex ) == null)
{
pageView = loadViewForPage( pageIndex );
_pageViews[ pageIndex ] = pageView;
}
else{
pageView = (UIView)_pageViews[ pageIndex ];
}
_scView.AddSubview( pageView );
return pageView;
}
private SizeF pageSize(){
return this._scView.Frame.Size;
}
private bool isPageLoaded( int pageIndex ){
return this._pageViews.ElementAt( pageIndex ) != null;
}
private void layoutPage( int pageIndex ){
SizeF pageSize = this.pageSize();
((UIView)this._pageViews[pageIndex]).Frame = new RectangleF( pageIndex * pageSize.Width,0, pageSize.Width, pageSize.Height );
this.viewForPage( pageIndex );
}
private void loadView(){
this._scView = new UIScrollView();
this._scView.Delegate = new ScViewDelegate( this );
this._scView.PagingEnabled = true;
this._scView.ShowsHorizontalScrollIndicator = false;
this._scView.ShowsVerticalScrollIndicator = false;
this._scView.Layer.BorderWidth = 2;
this._scView.AddSubview( _pageCont );
this.View = this._scView;
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
InitializeAfterLoad ();
this._pageViews = new Object[]{ _page1, _page2 };
this.loadView();
}
private void currentPageIndexDidChange(){
this.layoutPage( _currentPageIndex );
if(_currentPageIndex+1 < this.numberOfPages()){
this.layoutPage( _currentPageIndex + 1 );
}
if(_currentPageIndex >0){
this.layoutPage( _currentPageIndex - 1 );
}
this.UpdatePageControll( _pageCont, _currentPageIndex, this.numberOfPages(), ((UIView)this._pageViews[_currentPageIndex]) );
this._scView.BringSubviewToFront( _pageCont );
this.NavigationController.Title = string.Format( "{0} of {1}", _currentPageIndex + 1, this.numberOfPages() );
}
private void layoutPages(){
SizeF pageSize = this.pageSize();
this._scView.ContentSize = new SizeF( this.numberOfPages() * pageSize.Width, pageSize.Height );
// move all visible pages to their places, because otherwise they may overlap
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if(this.isPageLoaded( pageIndex ))
this.layoutPage( pageIndex );
}
}
public override void ViewWillAppear (bool animated)
{
this.layoutPages();
this.currentPageIndexDidChange();
this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
}
class ScViewDelegate : UIScrollViewDelegate
{
Test_Details_Controller id;
public ScViewDelegate ( Test_Details_Controller id )
{
this.id = id;
}
public override void Scrolled (UIScrollView scrollView)
{
if(id._rotationInProgress)
return;// UIScrollView layoutSubviews code adjusts contentOffset, breaking our logic
SizeF pageSize = id.pageSize();
int newPageIndex = ((int)id._scView.ContentOffset.X + (int)pageSize.Width / 2) / (int)pageSize.Width;
if( newPageIndex == id._currentPageIndex )
return;
id._currentPageIndex = newPageIndex;
id.currentPageIndexDidChange();
}
}
public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
{
return toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown;
}
public override void WillRotate (UIInterfaceOrientation toInterfaceOrientation, double duration)
{
_rotationInProgress = true;
// hide other page views because they may overlap the current page during animation
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if(this.isPageLoaded( pageIndex ))
this.viewForPage( pageIndex ).Hidden = ( pageIndex != _currentPageIndex );
}
}
public override void WillAnimateRotation (UIInterfaceOrientation toInterfaceOrientation, double duration)
{
// resize and reposition the page view, but use the current contentOffset as page origin
// (note that the scrollview has already been resized by the time this method is called)
SizeF pageSize = this.pageSize();
UIView pageView = this.viewForPage( _currentPageIndex );
this.viewForPage( _currentPageIndex ).Frame = new RectangleF( this._scView.ContentOffset.X, 0, pageSize.Width, pageSize.Height );
}
public override void DidRotate (UIInterfaceOrientation fromInterfaceOrientation)
{
base.DidRotate (fromInterfaceOrientation);
// adjust frames according to the new page size - this does not cause any visible changes
this.layoutPages();
this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
//unhide
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if( this.isPageLoaded( pageIndex ) )
this.viewForPage( pageIndex ).Hidden = false;
}
_rotationInProgress = false;
}
public override void DidReceiveMemoryWarning ()
{
//SuperHandle = DidReceiveMemoryWarning();
if(this._pageViews != null)
{
// unload non-visible pages in case the memory is scarse
for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
if( pageIndex < _currentPageIndex - 1 || pageIndex > _currentPageIndex + 1 )
if( this.isPageLoaded(pageIndex) ){
UIView pageview = (UIView)this._pageViews[ pageIndex ];
this._pageViews[ pageIndex ] = null;
pageview.RemoveFromSuperview();
}
}
}
}
public override void ViewDidUnload ()
{
this._pageViews = null;
this._scView = null;
}
}
I have a sample project and it works perfect.
https://code.google.com/p/uiscrollview-touch-events/