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 release
you can create swipable filters with scroll view which carries paging scrolling of CIImage.
or
You can use this: https://github.com/pauljeannot/SnapSliderFilters
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
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).
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