13

I am trying to allow users to swipe between filters on a static image. The idea is that the image stays in place while the filter scrolls above it. Snapchat recently released a version which implements this feature. This video shows exactly what I'm trying to accomplish at 1:05.

So far I've tried to put three UIImageView's into a scrollview, on to the left and one to the right of the original image, and adjust their frames origin.x and size.width with the scrollView's contentOffset.x. I found this idea in another post here. Changing the content mode of the left and right to UIViewContentModeLeft and UIViewContentModeRight did not help.

Next I tried stacking all three UIImageView's on top of one another. I made two CALayer masks and inserted them into the scrollView on the left and right of the stack so when you scroll the mask would unveil the filtered image. This did not work for me. Any help would be greatly appreciated.

Community
  • 1
  • 1
Rich86man
  • 6,377
  • 2
  • 24
  • 27
  • Changing position and size along with `UIViewContentModeLeft`/`UIViewContentModeRight` seems like it should work. Can you post the code for what you tried to do? You'll need your image sizes to match the actual size of your UIImageViews btw, because `UIViewContentModeLeft` does not do any scaling. You'll also need the UIImageView to have `clipsToBounds = YES` (just a few things that might go wrong off the top of my head). – Taum Apr 29 '14 at 17:07
  • The video is too long. Which part is the one you're talking about? – Rivera Apr 30 '14 at 02:06
  • 1:05. I tried to start the video at the correct time. I guess it didn't work – Rich86man Apr 30 '14 at 19:28
  • Looks to me like these are actually overlays on top of the image, rather than multiple images. – Leo Natan Apr 30 '14 at 19:50
  • Although the other aspects of the demo seem to come from a transparent UIScrollview over a fixed UIImage. The filters themselves are probably something else - maybe a UIView transition from 1 image to another (UIView animation) the UIViewAnimationOptionShowHideTransitionViews looks promising. – Paulo May 02 '14 at 03:10

4 Answers4

11

You should only need 2 image views (the current one and the incoming one, as this is a paginated style scroll), and they switch role after each filter change. And your approach of using a layer mask should work, but not on a scroll view.

So, ensure that your view organisation is something like:

UIView // receives all gestures
    UIScrollView // handles the filter name display, touch disabled
    UIImageView // incoming in front, but masked out
    UIImageView // current behind

Each image view has a mask layer, it's just a simple layer, and you modify the position of the mask layer to change how much of the image is actually visible.

Now, the main view handles the pan gesture, and uses the translation of the gesture to change the incoming image view mask layer position and the scroll view content offset.

As a change completes, the 'current' image view can't be seen any more and the 'incoming' image view takes the whole screen. The 'current' image view now gets moved to the front and becomes the incoming view, its mask gets updated to make it transparent. As the next gesture starts, its image is updated to the next filter and the change process starts over.

You can always be preparing the filtered images in the background as the scrolling is happening so that the image is ready to push into the view as you switch over (for rapid scrolling).

Wain
  • 117,132
  • 14
  • 131
  • 151
  • Awesome man awesome. I originally used a scrollview because I wanted to get the page snapping for free, but I'm more than happy to recreate that. THANK YOU https://www.youtube.com/watch?v=j75d7Khf6iM – Rich86man May 03 '14 at 21:07
  • 1
    @Rich86man Can you please share the code of swipable filters in gist or pastie. I am trying to do the same thing and stuck with scrollview. Thank you. – kevin Sep 08 '14 at 19:37
  • Is there any way to add swipe filter on zoomable imageview like instagram add story page? – Yogendra Patel May 22 '20 at 09:15
4

On my first attempt made a mistake in trying to mask a UIImage instead of a UIImage view, but eventually got a pretty decent working solution (which uses a UIImageView mask) below. If you have questions feel free to ask.

I basically create the current image and the filtered image. I mask the UIView (with a rectangle) and then adjust the mask based on the swipe.

Link to result: https://www.youtube.com/watch?v=k75nqVsPggY&list=UUIctdpq1Pzujc0u0ixMSeVw

Mask credit: https://stackoverflow.com/a/11391478/3324388

@interface FilterTestsViewController ()

@end

@implementation FilterTestsViewController

NSArray *_pictureFilters;
NSNumber* _pictureFilterIterator;
UIImage* _originalImage;
UIImage* _currentImage;
UIImage* _filterImage;
UIImageView* _uiImageViewCurrentImage;
UIImageView* _uiImageViewNewlyFilteredImage;
CGPoint _startLocation;
BOOL _directionAssigned = NO;
enum direction {LEFT,RIGHT};
enum direction _direction;
BOOL _reassignIncomingImage = YES;

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self initializeFiltering];
}

//set it up for video feed
-(void)initializeVideoFeed
{

}

-(void)initializeFiltering
{
    //create filters
    _pictureFilters = @[@"CISepiaTone",@"CIColorInvert",@"CIColorCube",@"CIFalseColor",@"CIPhotoEffectNoir"];
    _pictureFilterIterator = 0;

    //create initial image and current image
    _originalImage = [UIImage imageNamed:@"ja.jpg"]; //creates image from file, this will result in a nil CIImage but a valid CGImage;
    _currentImage = [UIImage imageNamed:@"ja.jpg"];

    //create the UIImageViews for the current and filter object
    _uiImageViewCurrentImage = [[UIImageView alloc] initWithImage:_currentImage]; //creates a UIImageView with the UIImage
    _uiImageViewNewlyFilteredImage = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,[UIScreen mainScreen].bounds.size.width,[UIScreen mainScreen].bounds.size.height)];//need to set its size to full since it doesn't have a filter yet

    //add UIImageViews to view
    [self.view addSubview:_uiImageViewCurrentImage]; //adds the UIImageView to view;
    [self.view addSubview:_uiImageViewNewlyFilteredImage];

    //add gesture
    UIPanGestureRecognizer* pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRecognized:)];
    [self.view addGestureRecognizer:pan];

}


-(void)swipeRecognized:(UIPanGestureRecognizer *)swipe
{
    CGFloat distance = 0;
    CGPoint stopLocation;
    if (swipe.state == UIGestureRecognizerStateBegan)
    {
        _directionAssigned = NO;
        _startLocation = [swipe locationInView:self.view];
    }else
    {
        stopLocation = [swipe locationInView:self.view];
        CGFloat dx = stopLocation.x - _startLocation.x;
        CGFloat dy = stopLocation.y - _startLocation.y;
        distance = sqrt(dx*dx + dy*dy);
    }

    if(swipe.state == UIGestureRecognizerStateEnded)
    {
        if(_direction == LEFT && (([UIScreen mainScreen].bounds.size.width - _startLocation.x) + distance) > [UIScreen mainScreen].bounds.size.width/2)
        {
            [self reassignCurrentImage];
        }else if(_direction == RIGHT && _startLocation.x + distance > [UIScreen mainScreen].bounds.size.width/2)
        {
            [self reassignCurrentImage];
        }else
        {
            //since no filter applied roll it back
            if(_direction == LEFT)
            {
                _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]-1];
            }else
            {
                _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]+1];
            }
        }
        [self clearIncomingImage];
        _reassignIncomingImage = YES;
        return;
    }

    CGPoint velocity = [swipe velocityInView:self.view];

    if(velocity.x > 0)//right
    {
        if(!_directionAssigned)
        {
            _directionAssigned = YES;
            _direction  = RIGHT;
        }
        if(_reassignIncomingImage && !_filterImage)
        {
            _reassignIncomingImage = false;
            [self reassignIncomingImageLeft:NO];
        }
    }
    else//left
    {
        if(!_directionAssigned)
        {
            _directionAssigned = YES;
            _direction  = LEFT;
        }
        if(_reassignIncomingImage && !_filterImage)
        {
            _reassignIncomingImage = false;
            [self reassignIncomingImageLeft:YES];
        }
    }

    if(_direction == LEFT)
    {
        if(stopLocation.x > _startLocation.x -5) //adjust to avoid snapping
        {
            distance = -distance;
        }
    }else
    {
        if(stopLocation.x < _startLocation.x +5) //adjust to avoid snapping
        {
            distance = -distance;
        }
    }

    [self slideIncomingImageDistance:distance];
}

-(void)slideIncomingImageDistance:(float)distance
{
    CGRect incomingImageCrop;
    if(_direction == LEFT) //start on the right side
    {
        incomingImageCrop = CGRectMake(_startLocation.x - distance,0, [UIScreen mainScreen].bounds.size.width - _startLocation.x + distance, [UIScreen mainScreen].bounds.size.height);
    }else//start on the left side
    {
        incomingImageCrop = CGRectMake(0,0, _startLocation.x + distance, [UIScreen mainScreen].bounds.size.height);
    }

    [self applyMask:incomingImageCrop];
}

-(void)reassignCurrentImage
{
    if(!_filterImage)//if you go fast this is null sometimes
    {
        [self reassignIncomingImageLeft:YES];
    }
    _uiImageViewCurrentImage.image = _filterImage;
    self.view.frame = [[UIScreen mainScreen] bounds];
}

//left is forward right is back
-(void)reassignIncomingImageLeft:(BOOL)left
{
    if(left == YES)
    {
        _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]+1];
    }else
    {
        _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]-1];
    }

    NSNumber* arrayCount = [NSNumber numberWithInt:(int)_pictureFilters.count];

    if([_pictureFilterIterator integerValue]>=[arrayCount integerValue])
    {
        _pictureFilterIterator = 0;
    }
    if([_pictureFilterIterator integerValue]< 0)
    {
        _pictureFilterIterator = [NSNumber numberWithInt:(int)_pictureFilters.count-1];
    }

    CIImage* ciImage = [CIImage imageWithCGImage:_originalImage.CGImage];
    CIFilter* filter = [CIFilter filterWithName:_pictureFilters[[_pictureFilterIterator integerValue]] keysAndValues:kCIInputImageKey,ciImage, nil];
    _filterImage = [UIImage imageWithCIImage:[filter outputImage]];
    _uiImageViewNewlyFilteredImage.image = _filterImage;
    CGRect maskRect = CGRectMake(0,0,[UIScreen mainScreen].bounds.size.width,[UIScreen mainScreen].bounds.size.height);
    [self applyMask:maskRect];
}

//apply mask to filter UIImageView
-(void)applyMask:(CGRect)maskRect
{
    // Create a mask layer and the frame to determine what will be visible in the view.
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];

    // Create a path with the rectangle in it.
    CGPathRef path = CGPathCreateWithRect(maskRect, NULL);

    // Set the path to the mask layer.
    maskLayer.path = path;

    // Release the path since it's not covered by ARC.
    CGPathRelease(path);

    // Set the mask of the view.
    _uiImageViewNewlyFilteredImage.layer.mask = maskLayer;
}


-(void)clearIncomingImage
{
    _filterImage = nil;
    _uiImageViewNewlyFilteredImage.image = nil;
    //mask current image view fully again
    [self applyMask:CGRectMake(0,0,[UIScreen mainScreen].bounds.size.width,[UIScreen mainScreen].bounds.size.height)];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end
Community
  • 1
  • 1
Aggressor
  • 12,511
  • 18
  • 88
  • 165
3

Using Aggressor's solution partly, I came up with what I see as the simplest way to set it up, with the least lines of code.

@IBOutlet weak var topImage: UIImageView!
@IBOutlet weak var bottomImage: UIImageView!
@IBOutlet weak var scrollview: UIScrollView!

override func viewDidLoad() {
    super.viewDidLoad()

    scrollview.delegate=self
    scrollview.contentSize=CGSizeMake(2*self.view.bounds.width, self.view.bounds.height)

    applyMask(CGRectMake(self.view.bounds.width-scrollview.contentOffset.x, scrollview.contentOffset.y, scrollview.contentSize.width, scrollview.contentSize.height))

}

func applyMask(maskRect: CGRect!){
    var maskLayer: CAShapeLayer = CAShapeLayer()
    var path: CGPathRef = CGPathCreateWithRect(maskRect, nil)
    maskLayer.path=path
    topImage.layer.mask = maskLayer
}

func scrollViewDidScroll(scrollView: UIScrollView) {
    println(scrollView.contentOffset.x)
    applyMask(CGRectMake(self.view.bounds.width-scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.contentSize.width, scrollView.contentSize.height))
}

Then just set the images, and make sure you have the scrollView above the imageViews. For behaviour as requested (like snapchat), make sure the scroll view has paging enabled set to true, and make sure its background colour is clear. The benefit of this method is you get all the scrollView behaviour for free... because you use a scrollView

TimWhiting
  • 2,207
  • 4
  • 18
  • 37
  • I would watch the use of hardcoded 320 since it is making an assumption on size that wouldn't necessarily hold. Otherwise conceptually it was pointed in a good direction for my needs. – SuperGuyAbe Jun 12 '15 at 19:37
  • I can't see where the filters are used in this code – cannyboy Aug 04 '15 at 16:10
  • @cannyboy At some point in your loading of the images you would have applied a filter to them. This question referred to how to swipe between the images, and not how to filter them. If you are looking for some basic image filtering code I can supply it but there is a lot of documentation on this https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CoreImageFilterReference/ – TimWhiting Aug 05 '15 at 00:00
  • @TimWhiting thanks, i'll look at that as well as GPUImage. For multiple filters, would you recommend multiple imageViews, or swapping the images when the scroll view is in a suitable contentOffset.x ? – cannyboy Aug 05 '15 at 08:59
3

you can create swipable filters with scroll view which carries paging scrolling of CIImage.

or

You can use this: https://github.com/pauljeannot/SnapSliderFilters