Fade edges of UITableView - ios

I've made a little research about my problem and unfortunately there was no solution for my problem.
The closest was Fade UIImageView as it approaches the edges of a UIScrollView but it's still not for me.
I want my table to apply an "invisibility gradient" on the top. If the cell is at a 50px distance from the top edge it starts to vanish. The closer it is to the upper edge, the more invisible the part is. The cells height is about 200 pixels, so the lower part of the cell need to be visible in 100%. But nevermind - I need a table view (or table view container) to do this task, because similar tables can display other cells.
If the table is a subview of a solid color view, I can achieve that by adding an image which is a horizontal gradient that I can streach to any width. The top pixel of that image starts with the exact color of the background, and going down the same color has less alpha.
But...
we have a UITableView with transparent color. Below the table there is no solid color, but a pattern image/texture, that can also be different on other screens of the app.
Do you have any Idea how I can achieve this behaviour?
Regards

I took this tutorial and made some changes and additions:
It now works on all tableviews - even if they are part of bigger screen.
It works regardless of the background or whatever is behind the tableview.
The mask changes depends on the position of the table view - when scrolled to top only the bottom faded, in when scrolled to bottom only top is faded...
1. Start by importing QuartzCore and setting a mask layer in your controller:
EDIT: No need for reference to CAGradientLayer in class.
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface mViewController : UIViewController
.
.
#end
2. Add this to viewWillAppear viewDidLayoutSubviews:
(See #Darren's comment on this one)
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!self.tableView.layer.mask)
{
CAGradientLayer *maskLayer = [CAGradientLayer layer];
maskLayer.locations = #[[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.2],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0]];
maskLayer.bounds = CGRectMake(0, 0,
self.tableView.frame.size.width,
self.tableView.frame.size.height);
maskLayer.anchorPoint = CGPointZero;
self.tableView.layer.mask = maskLayer;
}
[self scrollViewDidScroll:self.tableView];
}
3. Make sure you are a delegate of UIScrollViewDelegate by adding it in the .h of your controller:
#interface mViewController : UIViewController <UIScrollViewDelegate>
4. To finish, implement scrollViewDidScroll in your controller .m:
#pragma mark - Scroll View Delegate Methods
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGColorRef outerColor = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
CGColorRef innerColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
NSArray *colors;
if (scrollView.contentOffset.y + scrollView.contentInset.top <= 0) {
//Top of scrollView
colors = #[(__bridge id)innerColor, (__bridge id)innerColor,
(__bridge id)innerColor, (__bridge id)outerColor];
} else if (scrollView.contentOffset.y + scrollView.frame.size.height
>= scrollView.contentSize.height) {
//Bottom of tableView
colors = #[(__bridge id)outerColor, (__bridge id)innerColor,
(__bridge id)innerColor, (__bridge id)innerColor];
} else {
//Middle
colors = #[(__bridge id)outerColor, (__bridge id)innerColor,
(__bridge id)innerColor, (__bridge id)outerColor];
}
((CAGradientLayer *)scrollView.layer.mask).colors = colors;
[CATransaction begin];
[CATransaction setDisableActions:YES];
scrollView.layer.mask.position = CGPointMake(0, scrollView.contentOffset.y);
[CATransaction commit];
}
Again: most of the solution is from this tutorial in cocoanetics.

This is a translation of Aviel Gross's answer to Swift
import UIKit
class mViewController: UIViewController, UIScrollViewDelegate {
//Emitted boilerplate code
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if self.tableView.layer.mask == nil {
//If you are using auto layout
//self.view.layoutIfNeeded()
let maskLayer: CAGradientLayer = CAGradientLayer()
maskLayer.locations = [0.0, 0.2, 0.8, 1.0]
let width = self.tableView.frame.size.width
let height = self.tableView.frame.size.height
maskLayer.bounds = CGRect(x: 0.0, y: 0.0, width: width, height: height)
maskLayer.anchorPoint = CGPoint.zero
self.tableView.layer.mask = maskLayer
}
scrollViewDidScroll(self.tableView)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let outerColor = UIColor(white: 1.0, alpha: 0.0).cgColor
let innerColor = UIColor(white: 1.0, alpha: 1.0).cgColor
var colors = [CGColor]()
if scrollView.contentOffset.y + scrollView.contentInset.top <= 0 {
colors = [innerColor, innerColor, innerColor, outerColor]
} else if scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height {
colors = [outerColor, innerColor, innerColor, innerColor]
} else {
colors = [outerColor, innerColor, innerColor, outerColor]
}
if let mask = scrollView.layer.mask as? CAGradientLayer {
mask.colors = colors
CATransaction.begin()
CATransaction.setDisableActions(true)
mask.position = CGPoint(x: 0.0, y: scrollView.contentOffset.y)
CATransaction.commit()
}
}
//Emitted boilerplate code
}

Swift version of #victorfigol's excellent solution:
class FadingTableView : UITableView {
var percent = Float(0.05)
private let outerColor = UIColor(white: 1.0, alpha: 0.0).cgColor
private let innerColor = UIColor(white: 1.0, alpha: 1.0).cgColor
override func awakeFromNib() {
super.awakeFromNib()
addObserver(self, forKeyPath: "bounds", options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object is FadingTableView && keyPath == "bounds" {
initMask()
}
}
deinit {
removeObserver(self, forKeyPath:"bounds")
}
override func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
func initMask() {
let maskLayer = CAGradientLayer()
maskLayer.locations = [0.0, NSNumber(value: percent), NSNumber(value:1 - percent), 1.0]
maskLayer.bounds = CGRect(x:0, y:0, width:frame.size.width, height:frame.size.height)
maskLayer.anchorPoint = CGPoint.zero
self.layer.mask = maskLayer
updateMask()
}
func updateMask() {
let scrollView : UIScrollView = self
var colors = [CGColor]()
if scrollView.contentOffset.y <= -scrollView.contentInset.top { // top
colors = [innerColor, innerColor, innerColor, outerColor]
}
else if (scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height { // bottom
colors = [outerColor, innerColor, innerColor, innerColor]
}
else {
colors = [outerColor, innerColor, innerColor, outerColor]
}
if let mask = scrollView.layer.mask as? CAGradientLayer {
mask.colors = colors
CATransaction.begin()
CATransaction.setDisableActions(true)
mask.position = CGPoint(x: 0.0, y: scrollView.contentOffset.y)
CATransaction.commit()
}
}
}

This is my version of the fading table view by inheriting UITableView. Tested for iOS 7 & 8.
There is no need to implement scrollViewDidScroll, layoutSubviews can be used instead.
By observing for bounds changes, the mask is
always properly placed when the rotation changes.
You can use the percent parameter to change how much fading you want at the edges, I
find a small value looking better in some cases.
CEFadingTableView.m
#import "CEFadingTableView.h"
#interface CEFadingTableView()
#property (nonatomic) float percent; // 1 - 100%
#end
#implementation CEFadingTableView
- (void)awakeFromNib
{
[super awakeFromNib];
self.percent = 5.0f;
[self addObserver:self forKeyPath:#"bounds" options:0 context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if(object == self && [keyPath isEqualToString:#"bounds"])
{
[self initMask];
}
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:#"bounds"];
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self updateMask];
}
- (void)initMask
{
CAGradientLayer *maskLayer = [CAGradientLayer layer];
maskLayer.locations = #[#(0.0f), #(_percent / 100), #(1 - _percent / 100), #(1.0f)];
maskLayer.bounds = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
maskLayer.anchorPoint = CGPointZero;
self.layer.mask = maskLayer;
[self updateMask];
}
- (void)updateMask
{
UIScrollView *scrollView = self;
CGColorRef outer = [UIColor colorWithWhite:1.0 alpha:0.0].CGColor;
CGColorRef inner = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
NSArray *colors = #[(__bridge id)outer, (__bridge id)inner, (__bridge id)inner, (__bridge id)outer];
if(scrollView.contentOffset.y <= 0) // top
{
colors = #[(__bridge id)inner, (__bridge id)inner, (__bridge id)inner, (__bridge id)outer];
}
else if((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height) // bottom
{
colors = #[(__bridge id)outer, (__bridge id)inner, (__bridge id)inner, (__bridge id)inner];
}
((CAGradientLayer *)scrollView.layer.mask).colors = colors;
[CATransaction begin];
[CATransaction setDisableActions:YES];
scrollView.layer.mask.position = CGPointMake(0, scrollView.contentOffset.y);
[CATransaction commit];
}
#end

Related

Create gradient-based UICollectionViewCell background colour

I want to create custom bar for displaying some progress, that must look something like this:
So, as you can see each bar has it's own gradient background. I want to use UICollectionView for this. But problem is to create gradient background for each individual cell. So is it possible somehow create ONE basic gradient for whole UICollectionView, and then mask it, to be visible inside UICollectionViewCell?
Okay, here is the example, background is the gradient layer with different colours are spread and above you can create a mask layer to appear only on the rectangular shapes, which gives the illusion of the different rectangular layers having different gradient, you can play with gradient to get your desired effect,
First you subclass the UIView or you can create directly, better you can create a subclass as I am doing in the example
obj-c
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
[self createGreadient];
}
return self;
}
//this create a grenadine with the rectangular mask layer
- (void)createGreadient
{
UIColor *theColor = [UIColor redColor];//[[UIColor alloc] initWithRed:146.0/255.0 green:146.0/255.0 blue:146.0/255.0 alpha:1];
CAGradientLayer *gradientLayer = [[CAGradientLayer alloc] init];
gradientLayer.frame = self.bounds;
//u can add your own colours to get your requirement
gradientLayer.colors = [NSArray arrayWithObjects:
(id)[theColor CGColor],
(id)[[theColor colorWithAlphaComponent:0.7f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.4f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.3f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.2f] CGColor],
(id)[[theColor colorWithAlphaComponent:0.1f] CGColor],
(id)[[UIColor clearColor] CGColor],nil];
[self.layer addSublayer:gradientLayer];
CAShapeLayer *layerMask = [[CAShapeLayer alloc] init];
layerMask.frame = self.bounds;
UIBezierPath *rectangularMaskPath = [self getRectangularMaskPathForCount:5];
layerMask.path = rectangularMaskPath.CGPath;
gradientLayer.mask = layerMask;
}
//this creates rectangular path, u can adjust the rectangular and u can
//pass different number of count as the progress proceeds
- (UIBezierPath *)getRectangularMaskPathForCount:(NSInteger)rectCount
{
UIBezierPath *path = [[UIBezierPath alloc] init];
int i = 0;
for(;i <= rectCount; i++)
{
UIBezierPath *aPath;
CGRect aRect = CGRectMake(20+ (i * 20), 20, 10, 100); //set the rectangular position to your requirement
aPath = [UIBezierPath bezierPathWithRect:aRect];
[path appendPath:aPath];
}
return path;
}
swift version
subclass the UIView and past the below code,
override init (frame : CGRect) {
super.init(frame : frame)
createGreadient()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createGreadient()
{
let color:UIColor = UIColor.red
let greadetLayer = CAGradientLayer.init()
greadetLayer.frame = self.bounds
greadetLayer.colors = [color.withAlphaComponent(0.8).cgColor,color.withAlphaComponent(0.7),color.withAlphaComponent(0.4).cgColor,color.withAlphaComponent(0.3).cgColor,color.withAlphaComponent(0.2),color.withAlphaComponent(0.1).cgColor]
self.layer .addSublayer(greadetLayer)
let rectangularMaskPath = self.creatrRectangularPathWithCount(countRect: 5)
let maskLayer:CAShapeLayer = CAShapeLayer()
maskLayer.frame = self.bounds
maskLayer.path = rectangularMaskPath.cgPath
greadetLayer.mask = maskLayer
}
func creatrRectangularPathWithCount(countRect:NSInteger) -> UIBezierPath {
let path : UIBezierPath = UIBezierPath()
for i in 0..<countRect {
let aPath = UIBezierPath.init(rect: CGRect(x: 20 + (i * 20), y:20, width: 10, height: 100))
path.append(aPath)
}
return path
}
You can use the above view like below,
in ViewController
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let customView:CustomView = CustomView(frame: CGRect(x: 20, y: 20, width: 300, height: 300))
self.view.addSubview(customView)
}

How to add label or text in to CAShapeLayer

Here is my class and it'll draw a circle, it looks like this:
class OvalLayer: CAShapeLayer {
let animationDuration: CFTimeInterval = 0.3
override init() {
super.init()
fillColor = Colors.green.CGColor
path = ovalPathSmall.CGPath
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var ovalPathStart: UIBezierPath {
let path = UIBezierPath(ovalInRect: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
return path
}
}
Now I need to add a text to middle of this circle, I tried to find it on google but nothing that works fine. I am not sure if it's possible or not, can anyone help me if it's possible?
I guess you should add CATextLayer as a sublayer to CALayer... That works fine that way: try adding CAShapeLayer first, and then CATextLayer (to same CALayer parent layer), for example in following order...
// assume self - is UIView instance
self.layer.addSublayer(shapedLayer) // shapedLayer - CAShapeLayer instance
self.layer.addSublayer(textLayer) // textLayer - CATextLayer instance
Swift 3.0
let label = UILabel()
label.font = UIFont(name: "Helvetica-Bold", size: 12)
label.frame = CGRect(x: OvalLayer.frame.origin.x + (circleWidth/2), y: OvalLayer.frame.origin.y, width: OvalLayer.bounds.width, height: OvalLayer.bounds.height)
label.text = "Hello"
label.textColor = UIColor.red
label.isHidden = false
OvalLayer.addSublayer(label.layer)
You just need to get the center of your UIBezierPath and add a label or a CATextLayer to your current layer.
let center : CGPoint = CGPoint(x: CGRectGetMidX(ovalPathStart.bounds), y: CGRectGetMidX(ovalPathStart.bounds))
Now, create a UILabel or CATextLayer and set the center.
Text on CAShapeLayer Using CATextLayer
Here i am create CAShapeLayer object and i am adding CATextLayer to CAShapeLayer
Here numberOfArcsCount means some 8
CAShapeLayer *progressLayer = [[CAShapeLayer alloc] init];
[progressLayer setPath:bezierPath.CGPath];
CATextLayer* text = [CATextLayer new];
for (int i = 1; i <= numberOfArcs.count; i++) {
text.string = [NSString stringWithFormat:#"%i", i];
text.font = (__bridge CFTypeRef _Nullable)([UIFont fontWithName:#"akaChen" size:42]);
text.font = (__bridge CFTypeRef _Nullable)([UIFont boldSystemFontOfSize:15]);
text.fontSize=25;
text.frame = CGRectMake(0,0,40,40);
text.position = CGPointMake(CGRectGetMidX(progressLayer.frame) ,CGRectGetMidY(progressLayer.frame) );
CGFloat vert = CGRectGetMidY(progressLayer.frame) / CGRectGetHeight(text.frame);
text.anchorPoint = CGPointMake(0.5, vert );
text.alignmentMode = kCAAlignmentCenter;
text.foregroundColor = [[UIColor whiteColor] CGColor];
[progressLayer addSublayer:text];
}
This way to add label to layer :
labelFrame - CGRect
someString - String
layer - your layer
let label = CATextLayer()
label.frame = labelFarme
label.string = someString
layer.addSublayer(label)

UISearchBar scope bar background image settable, but not its color?

for subView in searchBar.subviews {
if let scopeBar = subView as? UISegmentedControl {
scopeBar.backgroundColor = UIColor.blueColor()
}
}
I've been trying the above code to attempt to get a reference to the scopeBar and subsequently set its background color, but I am not able to get a reference. It seems to only go through the loop once, implying that there is only a single subview for the search bar. In the debugger the search bar appears to have an instance variable called _scopeBar with a type of (UISegmentedControl *).
if let topView = searchBar.subviews.first {
for subView in topView.subviews {
if let cancelButton = subView as? UIButton {
cancelButton.tintColor = UIColor.whiteColor()
cancelButton.enabled = true
}
}
}
The second block of code works for accessing the cancelButton of the search bar.
You can set the scopeBar background image as a solid color image.
First, you will need to create an UIImage from a color. In order to do that, you can create a function, or an extension on the UIImage, such as the following code:
Swift 3
import UIKit
extension UIImage {
class func imageWithColor(color: UIColor) -> UIImage {
let rect: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
After that, you can simply set the backgroundImage on the scope bar:
Swift 3
// Set the scopeBar's background color:
searchBar.scopeBarBackgroundImage = UIImage.imageWithColor(color: UIColor.blue)
I have a sample issue, and I solved this as below code.
CGSize imageSize = CGSizeMake(64, 64);
UIColor *fillColor = [UIColor colorWithRed:228/255.0 green:228/255.0 blue:228/255.0 alpha:1.0];
UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[fillColor setFill];
CGContextFillRect(context, CGRectMake(0, 0, imageSize.width, imageSize.height));
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.searchController.searchBar.scopeBarBackgroundImage = image;
- (void)changeScopeBarColor {
NSMutableArray<UIView*> *views = [NSMutableArray new];
NSArray<UIView *> *subviews = [_searchBar subviews];
[views addObjectsFromArray:subviews];
for (;;) {
if (views.count == 0) {
break;
}
UIView *v = [views firstObject];
[views removeObject:v];
if ([[[v class] description] isEqualToString:#"_UISearchBarScopeBarBackground"]) {
v.backgroundColor = [UIColor whiteColor];
break;
}
if (v.subviews.count > 0)
[views addObjectsFromArray:v.subviews];
}
}

Apply CGAffineTransformScale to UIView makes the layer border go bigger too

I have a UIView, inside it I have a UIImageView. I have a UIPinchGestureRecognizer added to the UIVIew to handle the pinch and zoom and make the UIView grow with the UIImageView altogether.
My UIView has a border. I added the border this way:
self.layer.borderColor = [UIColor blackColor].CGColor;
self.layer.borderWidth = 1.0f;
self.layer.cornerRadius = 8.0f;
And the problem I'm having is I can't find a way of making my UIView bigger while keeping the same width of the border. When pinching and zooming the border gets thicker.
This is my UIPinchGestureRecognizer handler:
- (void)scale:(UIPanGestureRecognizer *)sender{
if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);
CGAffineTransform currentTransform = self.transform;
CGAffineTransform newTransform = CGAffineTransformScale(currentTransform, scale, scale);
_lastScale = [(UIPinchGestureRecognizer*)sender scale];
[self setTransform:newTransform];
}
Thanks a lot!!
I've been googling around A LOT and found this:
self.layer.borderWidth = 2.0f / scaleFactor;
Sadly is not working for me... It makes sense but not working.
Also I read the solution about adding an other view in the back and making the front view to have an offset in the position so the back view is shown and looks like a border. That's not an option because I need my image to view transparent.
I want to recreate what Aviary does in their app. You can scale up and down an "sticker" and the border always stays the same size.
I was trying to do this effect in this way as well in Swift 4.2. Ultimately I could not do it successfully using CGAffine Effects. I ultimately had to update the constraints -- as I was using anchors.
Here is the code I ultimately started using. SelectedView is the view that should be scaled.
Keep in mind that this does not break other CGAffine effects. I'm still using those for rotation.
#objc private func scaleSelectedView(_ sender: UIPinchGestureRecognizer) {
guard let selectedView = selectedView else {
return
}
var heightDifference: CGFloat?
var widthDifference: CGFloat?
// increase width and height according to scale
selectedView.constraints.forEach { constraint in
guard
let view = constraint.firstItem as? UIView,
view == selectedView
else {
return
}
switch constraint.firstAttribute {
case .width:
let previousWidth = constraint.constant
constraint.constant = constraint.constant * sender.scale
widthDifference = constraint.constant - previousWidth
case .height:
let previousHeight = constraint.constant
constraint.constant = constraint.constant * sender.scale
heightDifference = constraint.constant - previousHeight
default:
return
}
}
// adjust leading and top anchors to keep view centered
selectedView.superview?.constraints.forEach { constraint in
guard
let view = constraint.firstItem as? UIView,
view == selectedView
else {
return
}
switch constraint.firstAttribute {
case .leading:
guard let widthDifference = widthDifference else {
return
}
constraint.constant = constraint.constant - widthDifference / 2
case .top:
guard let heightDifference = heightDifference else {
return
}
constraint.constant = constraint.constant - heightDifference / 2
default:
return
}
}
// reset scale after applying in order to keep scaling linear rather than exponential
sender.scale = 1.0
}
Notes: width and height anchors are on the view itself. Top and Leading anchors are on the superview -- hence the two forEach blocks.
I had to figure out a way of keeping my borders the same width during transforms (also creating stickers), and needed the solution to work with CGAffineTransform because my stickers rotate (and you cannot easily use frame-based solutions if you're doing rotations).
My approach was to change the layer.borderWidth in response to the transform changing, like this. Works for me.
class MyView : UIView {
let borderWidth = CGFloat(2)
override var transform: CGAffineTransform {
didSet {
self.setBorder()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.setBorder()
}
fileprivate func setBorder() {
self.layer.borderWidth = self.borderWidth / transform.scale
}
}
extension CGAffineTransform {
var scale: CGFloat {
return sqrt((a * a + c * c))
}
}
while setting CGAffineTransformScale, you wont be able to control the border-width separately.
The solution would be to use another view say borderView as subview in above view hierarchy than the view to be scaled (with backgroundColor as clearcolor)whose SIZE should be changed according to scale factor of other view.
Apply your border-width to the borderView keeping the desired borderwidth.
Best of luck!
Switch from CGAffineTransform to view's frame.
Sample code for pinching a view with constant border width:
#interface ViewController ()
#property (nonatomic, weak) IBOutlet UIView *testView;
#end
#implementation ViewController {
CGFloat _lastScale;
CGRect _sourceRect;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.testView.backgroundColor = [UIColor yellowColor];
self.testView.layer.borderColor = [UIColor blackColor].CGColor;
self.testView.layer.borderWidth = 1.0f;
self.testView.layer.cornerRadius = 8.0f;
_sourceRect = self.testView.frame;
_lastScale = 1.0;
}
- (IBAction)scale:(UIPanGestureRecognizer *)sender{
if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);
CGPoint center = self.testView.center;
CGRect newBounds = CGRectMake(0, 0, _sourceRect.size.width * scale, _sourceRect.size.height * scale);
self.testView.bounds = newBounds;
self.testView.layer.transform = CATransform3DMakeRotation(M_PI * (scale - 1), 0, 0, 1);
}
#end
UPD:
I have checked Aviary app: scale by frame, rotate by CGAffineTransform. You may need to implement some additional logic to make it work.
UPD:
Use bounds and play with rotation
Without setting the transform, scale the size of view by changing it's bounds. This solution works for me.
You can add a method in the view class you want to scale, sample code:
- (void)scale:(CGFloat)scale {
self.bounds = CGRectMake(0, 0, CGRectGetWidth(self.bounds) * scale, CGRectGetHeight(self.bounds) * scale);
}

UIImagePickerController editing view circle overlay

I've been able to get pretty far with what I've been wanting to accomplish, and that's to replicate iOS's built in circular photo cropper for the built in contacts app. However, I'm stuck at trying to get my CAShapeLayers made correctly. I'm trying to make a transparent 320 px diameter circle and the rest of the view filled with an 0.9 alpha black background. The circle and rectangle are in the right place, but, the circle is not completely transparent like I need it to be.
I'm lost as to how to fix this. I appreciate your help! Code and screenshot:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if ([navigationController.viewControllers count] == 3)
{
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenHeight = screenRect.size.height;
UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];
plCropOverlay.hidden = YES;
CAShapeLayer *circleLayer = [CAShapeLayer layer];
if (screenHeight == 568)
{
[circleLayer setPosition:CGPointMake(0.0f,124.0f)];
}
else
{
[circleLayer setPosition:CGPointMake(0.0f,80.0f)];
}
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, 0.0f, 320.0f, 320.0f)];
[circleLayer setPath:[path CGPath]];
[circleLayer setFillColor:[[UIColor whiteColor] CGColor]];
circleLayer.opacity = 0.7f;
// Set to 0.7f to show for screenshot purposes; setting to 0.0 would make it invisible and blend in with the below rectangleLayer.
CAShapeLayer *rectangleLayer = [CAShapeLayer layer];
UIBezierPath *path2 = [UIBezierPath bezierPathWithRect:CGRectMake(0.0f, 0.0f, 320.0f, screenHeight - 72)];
[rectangleLayer setPath:[path2 CGPath]];
[rectangleLayer setFillColor:[[UIColor blackColor] CGColor]];
[rectangleLayer setOpacity:0.9f];
[rectangleLayer addSublayer:circleLayer];
[[viewController.view layer] addSublayer:rectangleLayer];
UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
[moveLabel setText:#"Move and Scale"];
[moveLabel setTextAlignment:NSTextAlignmentCenter];
[moveLabel setTextColor:[UIColor whiteColor]];
[viewController.view addSubview:moveLabel];
}
}
Resolved code:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if ([navigationController.viewControllers count] == 3)
{
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];
plCropOverlay.hidden = YES;
int position = 0;
if (screenHeight == 568)
{
position = 124;
}
else
{
position = 80;
}
CAShapeLayer *circleLayer = [CAShapeLayer layer];
UIBezierPath *path2 = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, position, 320.0f, 320.0f)];
[path2 setUsesEvenOddFillRule:YES];
[circleLayer setPath:[path2 CGPath]];
[circleLayer setFillColor:[[UIColor clearColor] CGColor]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 320, screenHeight-72) cornerRadius:0];
[path appendPath:path2];
[path setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor blackColor].CGColor;
fillLayer.opacity = 0.8;
[viewController.view.layer addSublayer:fillLayer];
UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
[moveLabel setText:#"Move and Scale"];
[moveLabel setTextAlignment:NSTextAlignmentCenter];
[moveLabel setTextColor:[UIColor whiteColor]];
[viewController.view addSubview:moveLabel];
}
}
I've changed the code of #aviatorken89 because it wasn't working on iPhone 6/6+ and iPad. Now it should work with any iPhone's screen size and also on iPad! Tested on iOS 7 and iOS 8.
All these methods aren't really reliable because they are based on the Image Picker subviews hierarchy, and of course Apple could change it. I've tried to protect the code as far as I could, in order to prevent possibile crashes on future iOS releases.
I'll try to keep my solution updated on a gist: https://gist.github.com/andreacipriani/74ea67db8f17673f1b8b
Here is the code:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if ([navigationController.viewControllers count] == 3 && ([[[[navigationController.viewControllers objectAtIndex:2] class] description] isEqualToString:#"PUUIImageViewController"] || [[[[navigationController.viewControllers objectAtIndex:2] class] description] isEqualToString:#"PLUIImageViewController"]))
[self addCircleOverlayToImagePicker:viewController];
}
}
-(void)addCircleOverlayToImagePicker:(UIViewController*)viewController
{
UIColor *circleColor = [UIColor clearColor];
UIColor *maskColor = [[UIColor blackColor] colorWithAlphaComponent:0.8];
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
UIView *plCropOverlayCropView; //The default crop overlay view, we wan't to hide it and show our circular one
UIView *plCropOverlayBottomBar; //On iPhone this is the bar with "cancel" and "choose" buttons, on Ipad it's an Image View with a label saying "Scale and move"
//Subviews hirearchy is different in iPad/iPhone:
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){
plCropOverlayCropView = [viewController.view.subviews objectAtIndex:1];
plCropOverlayBottomBar = [[[[viewController.view subviews] objectAtIndex:1] subviews] objectAtIndex:1];
//Protect against iOS changes...
if (! [[[plCropOverlayCropView class] description] isEqualToString:#"PLCropOverlay"]){
DLog(#"Warning - Image Picker with circle overlay: PLCropOverlay not found");
return;
}
if (! [[[plCropOverlayBottomBar class] description] isEqualToString:#"UIImageView"]){
DLog(#"Warning - Image Picker with circle overlay: BottomBar not found");
return;
}
}
else{
plCropOverlayCropView = [[[viewController.view.subviews objectAtIndex:1] subviews] firstObject];
plCropOverlayBottomBar = [[[[viewController.view subviews] objectAtIndex:1] subviews] objectAtIndex:1];
//Protect against iOS changes...
if (! [[[plCropOverlayCropView class] description] isEqualToString:#"PLCropOverlayCropView"]){
DDLogWarn(#"Image Picker with circle overlay: PLCropOverlayCropView not found");
return;
}
if (! [[[plCropOverlayBottomBar class] description] isEqualToString:#"PLCropOverlayBottomBar"]){
DDLogWarn(#"Image Picker with circle overlay: PLCropOverlayBottomBar not found");
return;
}
}
//It seems that everything is ok, we found the CropOverlayCropView and the CropOverlayBottomBar
plCropOverlayCropView.hidden = YES; //Hide default CropView
CAShapeLayer *circleLayer = [CAShapeLayer layer];
//Center the circleLayer frame:
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0f, screenHeight/2 - screenWidth/2, screenWidth, screenWidth)];
circlePath.usesEvenOddFillRule = YES;
circleLayer.path = [circlePath CGPath];
circleLayer.fillColor = circleColor.CGColor;
//Mask layer frame: it begins on y=0 and ends on y = plCropOverlayBottomBar.origin.y
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, screenWidth, screenHeight- plCropOverlayBottomBar.frame.size.height) cornerRadius:0];
[maskPath appendPath:circlePath];
maskPath.usesEvenOddFillRule = YES;
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = maskColor.CGColor;
[viewController.view.layer addSublayer:maskLayer];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone){
//On iPhone add an hint label on top saying "scale and move" or whatever you want
UILabel *cropLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, screenWidth, 50)];
[cropLabel setText:#"Scale and move"]; //You should localize it
[cropLabel setTextAlignment:NSTextAlignmentCenter];
[cropLabel setTextColor:[UIColor whiteColor]];
[viewController.view addSubview:cropLabel];
}
else{ //On iPad re-add the overlayBottomBar with the label "scale and move" because we set its parent to hidden (it's a subview of PLCropOverlay)
[viewController.view addSubview:plCropOverlayBottomBar];
}
}
Swift 3 version (also with rounded edit layer for pictures taken by camera):
// Insert this code to your view controller
private var editLayer: CAShapeLayer!
private var label: UILabel!
override func viewDidLoad()
{
super.viewDidLoad()
// Rounded edit layer
navigationController?.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(pictureCaptured), name: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidCaptureItem"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(pictureRejected), name: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidRejectItem"), object: nil)
}
#objc private func pictureCaptured()
{
addRoundedEditLayer(to: ...your UIImagePickerController..., forCamera: true)
}
#objc private func pictureRejected()
{
editLayer.removeFromSuperlayer()
label.removeFromSuperview()
}
deinit
{
NotificationCenter.default.removeObserver(self)
}
// MARK: Navigation controller delegate
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
{
// Image picker in edit mode
if let imageVC = NSClassFromString("PUUIImageViewController")
{
if viewController.isKind(of: imageVC) {
addRoundedEditLayer(to: viewController, forCamera: false)
}
}
}
private func addRoundedEditLayer(to viewController: UIViewController, forCamera: Bool)
{
hideDefaultEditOverlay(view: viewController.view)
// Circle in edit layer - y position
let bottomBarHeight: CGFloat = 72.0
let position = (forCamera) ? viewController.view.center.y - viewController.view.center.x - bottomBarHeight/2 : viewController.view.center.y - viewController.view.center.x
let viewWidth = viewController.view.frame.width
let viewHeight = viewController.view.frame.height
let emptyShapePath = UIBezierPath(ovalIn: CGRect(x: 0, y: position, width: viewWidth, height: viewWidth))
emptyShapePath.usesEvenOddFillRule = true
let filledShapePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight - bottomBarHeight), cornerRadius: 0)
filledShapePath.append(emptyShapePath)
filledShapePath.usesEvenOddFillRule = true
editLayer = CAShapeLayer()
editLayer.path = filledShapePath.cgPath
editLayer.fillRule = kCAFillRuleEvenOdd
editLayer.fillColor = UIColor.black.cgColor
editLayer.opacity = 0.5
viewController.view.layer.addSublayer(editLayer)
// Move and Scale label
label = UILabel(frame: CGRect(x: 0, y: 10, width: viewWidth, height: 50))
label.text = "Move and Scale"
label.textAlignment = .center
label.textColor = UIColor.white
viewController.view.addSubview(label)
}
private func hideDefaultEditOverlay(view: UIView)
{
for subview in view.subviews
{
if let cropOverlay = NSClassFromString("PLCropOverlayCropView")
{
if subview.isKind(of: cropOverlay) {
subview.isHidden = true
break
}
else {
hideDefaultEditOverlay(view: subview)
}
}
}
}
For doing this from camera, try using the cameraOverlayView and set your own view. That will work only when picking from camera though and not photo library.
In the code of Jakub Marek, there's an issue with persistent rounded layer if you open a second time the camera.
to solve it add in your openCamera func:
editLayer?.removeFromSuperlayer()
label?.removeFromSuperview()
and replace in private func hideDefaultEditOverlay(view: UIView)
subview.isHidden = true
by
subview.removeFromSuperview()
Code :
func openCamera(){
if(UIImagePickerController .isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){
imagePicker.sourceType = UIImagePickerController.SourceType.camera
//If you dont want to edit the photo then you can set allowsEditing to false
imagePicker.allowsEditing = true
imagePicker.cameraDevice = .rear
imagePicker.showsCameraControls = true
imagePicker.cameraCaptureMode = .photo
imagePicker.delegate = self
editLayer?.removeFromSuperlayer()
label?.removeFromSuperview()
self.present(imagePicker, animated: true, completion: nil)
}
else{
let alert = UIAlertController(title: NSLocalizedString("Attention",comment:""), message: NSLocalizedString("You don't have any camera",comment:""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK",comment:""), style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
private func hideDefaultEditOverlay(view: UIView)
{
for subview in view.subviews
{
if let cropOverlay = NSClassFromString("PLCropOverlayCropView")
{
if subview.isKind(of: cropOverlay) {
subview.removeFromSuperview()
//subview.isHidden = true
break
}
else {
hideDefaultEditOverlay(view: subview)
}
}
}
}

Resources