Create shadow around a UIVisualEffectView without covering the whole view - ios

Is it possible to create a shadow around a UIVisualView with UIBlurEffect without letting the UIVisualView get coloured by the shadow underneath?
I basically just want the shadow around the view but with this code the shadow will cover the whole view which darkens the whole view to much:
let borderPath = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 15, height: 15)).cgPath
shadowView.frame = view.bounds
shadowView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
shadowView.layer.shadowOpacity = 0.3
shadowView.layer.shadowRadius = 3.0
shadowView.backgroundColor = UIColor.clear
shadowView.layer.shadowPath = borderPath
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
self.view.insertSubview(shadowView, at: 0)
let blurEffect = UIBlurEffect(style: .extraLight)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = view.bounds
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
blurView.clipsToBounds = true
blurView.layer.cornerRadius = 15
view.insertSubview(blurView, aboveSubview: shadowView)
EDIT.
I need to achieve the same thing as in Apple's Maps application. Where the draggable favourite view both uses the UIVisualEffectView and a shadow around its top, without interfering with the UIVisualEffectView's background.
See example screenshots:

Ok, so the problem was that my background in the underlying view was white. And with the UIBlurEffect .extraLight used on a background which is lighter than the BlurEffect the shadow beneath a UIVisualView appears darker than with a more vivid background.
Also described in this question:
Fix UIVisualEffectView extra light blur being gray on white background
UPDATE
I found a project explaining how to solve this on Github The solution involves creating a 9-part UIImage to represent the shadow. The creator also explains the underlying layers of the iOS 10 Maps.

So i'm trying to recreate the look of iOS 10 maps. I decided to attach the maps app in the simulator to the debugger to see what was going on...
Apple actually get around this by having a UIImage over the top of the content with the border and shadow. Not the most elegant way to do it but i'm going for the exact look so I'm going to take the exact approach.
I also grabbed the asset (using this) from the Maps app to save making your own one. Shame they only have #2x artwork in it though :/

I found in iOS 12, this is not a problem, the UIVisualEffectView ignore the shadow of underneath views, it just sees through the shadow, like the shadow not exist.

The trick is to not set the shadowPath of the layer. If it is set, the shadow is painted below the visual effect view and, in consequence, darkens the blurred view.
The documentation for shadowPath states:
If you specify a value for this property, the layer creates its shadow
using the specified path instead of the layer’s composited alpha
channel.
The "instead…" part is what we actually need: the shadow should be composed after rendering the content, based on what the content leaves transparent. Now you might be deterred from not setting a shadowPath because the documentation also states that an "explicit path usually improves rendering performance". However, I didn't see any issues in real life so far. In comparison to the rendering cost of the UIVisualEffectView, painting the shadow doesn't make a difference, I assume.
Also make sure to set the shadow on a superview of the UIVisualEffectView, not on a sibling view.

I usually solve this kind of situation using composition of views. Instead of setting the shadow in the target view, I create a ShadowView and I put it behind the target view, both views with the same frame.
I've posted and example and code in this question.
An example of the result of this approach is the following:

Related

What do I need for masking a UIImageView and how do I do it in Swift 3?

I was wondering what I would need if I wanted to use a mask image to get my UIImageView in a specific shape. From what I understand, to create a mask, I need to have an image with the shape of the mask all black on top of a white background. Something like this, for example:
First of all, is this sufficient to shape an image view, and if so, how do I do it in Swift 3? I can only find masking code that is either outdated or written in Objective-C. I've tried simply assigning the image above to an UIImageView and then assign the image view to the mask property of the UIImageView I want to shape, like so:
self.defaultImageView.mask = self.maskImageView
This didn't do anything. It just made self.maskImageView disappear (both image view's added through the storyboard and connected using IBOutlet properties). I'm sure I'm forgetting to do something. It can't be this simple. I would appreciate it if someone could help me out. Like I said, I put both image views on the exact same spot, on top of each other, in the storyboard.
UPDATE:
My first attempt to set the mask programmatically after deleting it from my storyboard.
let layer:CALayer = CALayer()
let mask:UIImage = UIImage(named: "Black-Star-Photographic-Agency")!
layer.contents = mask
layer.frame = CGRect(x: 0, y: 0, width: ((self.defaultImageView.image?.size.width)!), height: (self.defaultImageView.image?.size.height)!)
self.defaultImageView.layer.mask = layer
self.defaultImageView.layer.masksToBounds = true
The result was that the image view had completely disappeared and wasn't visible anymore. Am I doing something, am I forgetting something or both?
You should use a png image, which supports transparency, unlike jpg.
In Photoshop your image should look similar to this:
It doesn't matter if your shape is black or white. What matters is transparency of each pixel. Opaque area (black in this case) will be visible and transparent area will get trimmed.
Edit:
You should not create mask view from storyboard if you do so. It is not going to be a part of your view hierarchy. Just add it programmatically like this:
let maskView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
maskView.image = UIImage(named: "mask")
imageView.mask = maskView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
maskView.frame = imageView.bounds
}
Output:
Here is a test project to show how it's working.
Also if you're using a custom frame/image and run into the mask not showing properly, try setting the content mode of the mask:
maskView.contentMode = .scaleAspectFit

Fix UIVisualEffectView extra light blur being gray on white background

Apple provides this live blurring view class UIVisualEffectView and you use it with a UIBlurEffect which takes one of three available UIBlurEffectStyles:
enum UIBlurEffectStyle : Int {
case ExtraLight
case Light
case Dark
}
Now for demo purposes I set up two effect views with the styles Light and ExtraLight:
let lightBlur = UIBlurEffect(style: UIBlurEffectStyle.Light)
let extraLightBlur = UIBlurEffect(style: UIBlurEffectStyle.ExtraLight)
let lightView = UIVisualEffectView(effect: lightBlur)
lightView.frame = CGRectMake(10, 30, 150, 150)
self.view.addSubview(lightView)
let extraLightView = UIVisualEffectView(effect: extraLightBlur)
extraLightView.frame = CGRectMake(160, 30, 150, 150)
self.view.addSubview(extraLightView)
So far so good, everything works as expected, they blur images:
and kind of work on colors, too:
but when it comes to a white background this happens:
The Light effect on the left works as expected, but the ExtraLight effect on the right leaves some kind of gray square behind.
Now the question: Is there any kind of trick or method that would enable me to use an extra light blur effect on white background (with live blurring support) AND remove that ugly gray shadow?
As far as I know the additional tint is a built-in feature of the UIVisualEffectView class. If you examine the view hierarchy with Xcode, you can see that there are two default subviews in the visual effect view instance: UIVisualEffectBackdropView and UIVisualEffectSubview. (I assume that these are private classes.) If you inspect UIVisualEffectSubview you can see that it has a background color which causes the unwanted tint that you've noticed.
I am not sure if there's an officially supported way to remove it, but you can modify this background color by filtering to the name of the private subview:
if let vfxSubView = visualEffectView.subviews.first(where: {
String(describing: type(of: $0)) == "_UIVisualEffectSubview"
}) {
vfxSubView.backgroundColor = UIColor.white.withAlphaComponent(0.7)
}
(Swift 5 compatible)
The default tint is around 70% opacity, so the easiest is to use the target background color with 0.7 alpha component.
I've also noticed that this might reset to the default value if the visual effect contains a custom subview. If I add this same snippet to the viewDidLayoutSubviews function of the view's controller, then it will keep the custom background color even after the built-in subview is updated.
Here's an example with a dark blur effect style. The top part shows the default tint and the bottom version has a custom black background color with 70% opacity.
If you just want the blur and your blurred view is gonna be stationary, you could use the UIImageEffects class and change the tintColor to a "full" white:
- (UIImage *)applyExtraLightEffect
{
UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
As far as I know you can't change it in the UIVisualEffectView.
You can try :
var visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .Light))
visualEffectView.frame = imageView.bounds
imageView.addSubview(visualEffectView)
Simple Solution
I could find a simple solution inspired by Shannon Hughes' Blog Post. Just add a white background color with transparency to the effect view. I don't know if it is exactly like extraLight but for me it is close enough.
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
visualEffectView.frame = sectionHeaderView.bounds
visualEffectView.backgroundColor = UIColor(white: 1.0, alpha: 0.9)
sectionHeaderView.addSubview(visualEffectView)
The following code should do the trick to give it a the desired tint:
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
visualEffectView.subviews.forEach { subview in
subview.backgroundColor = UIColor.blue.withAlphaComponent(0.2)
}
Important Note:
I wouldn't recommend adding a check for "_UIVisualEffectSubview" since this class can change on subsequent iOS updates.
Also, there is a possibility of app getting rejected because of this.
iOS 15 has the nice
.background(.ultraThinMaterial)
but it still suffers from the nasty grey tint. Wish there was an option to blur background views without any tint.
I tried a simple color-correct hack which worked for my background color but only in light mode! For dark mode I just made the background solid black (no translucent blur)
.background(
// negate the slight grey tint of ultrathinmaterial
Color("materialTintColorCorrect")
.background(.ultraThinMaterial)
)
My "materialTintColorCorrect" color asset was #F0F8FF 27% opacity for light mode and #000000 100% opacity for dark.
let vc = UIViewController()
vc.view.frame = self.view!.frame
let efv = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.light))
efv.frame = vc.view.frame
vc.view.addSubview(efv)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
// below method has a bug
// self.present(vc, animated: true, completion:nil)
I would recommend adding your extraLightView to a view of UIColor(white: 1.0, alpha: 0.x), where x can be modified based on the scroll view's contentOffset. When there's nothing behind the view, your extraLightView will be white when x is 1. When you scroll and modify x, you won't be modifying the UIVisualEffectView (which is highly discouraged), but rather its parent view, which is perfectly safe.
Works with Swift 5
My way of making the visual effect view completely white when the background view is white.
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
blurView.backgroundColor = UIColor.white.withAlphaComponent(0.7)

iOS: Solid Border Outside UIView

A layer's borderWidth and borderColor properties draw a border inside the view. Remykits pointed this out here.
A layer's shadow... properties cannot be used to create a border that both appears on all four sides and is opaque for reasons I showed here.
What I failed to specify in that question (and the reason I've opened a new one) is that I want the border to be outside the view. Increasing the frame of the view to compensate for the space lost, as has been suggested, doesn't work; I'm using a UIImageView, so even if the frame is increased, the image is still cropped.
Another suggestion was to change the contentMode of the UIImageView to .Center, in combination with changing the size of the view, but this doesn't work as the view then isn't the proper size.
The solution I first thought of was to create another UIView "behind" this UIImageView, and give it a backgroundColor to mimic the effect of a border. I also thought of creating a custom subclass of UImageView. Both courses of action, however, involve making calculations based on the frame of the view. I've had many problems with the frame not being set by AutoLayout at the proper time, etc.
Other things that come to mind are digitally adding a border to the image or positioning the image in a specific part of the UIImageView. (My attempt at the latter was imageView.layer.contentsRect = CGRectInset(imageView.bounds, 4, 4), which resulted in a strangely pixellated image.)
To be clear, what I'm looking for is this:
It really feels like there should be a simpler way to do this than creating a new class or view. Any help appreciated.
Aha! Stitching together aykutt's comment about resizing the image and changing the conentMode, Paul Lynch's answer about resizing images, and rene's (life-saving) answer about what to do your subviews actually aren't laid out in viewDidLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.myContainer.setNeedsLayout()
self.myContainer.layoutIfNeeded()
var width: CGFloat = 4 //the same width used for the border of the imageView
var rect = CGRectInset(imageView.bounds, width, width)
var size = CGSizeMake(rect.width, rect.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
image.drawInRect(CGRectMake(0, 0, size.width, size.height))
var new = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.imageView.contentMode = .Center
self.imageView.image = new
}

Shadow lagging the user interface

I have around 15 views for which I apply QuartzCore shadow like this:
for button in buttonsArray {
button.layer.shadowOpacity = 0.75
button.layer.shadowColor = UIColor.blackColor().CGColor
button.layer.shadowRadius = 2.0
button.layer.shadowOffset = CGSize(width: 0.4, height: 1.2)
}
When I have a lot of this shadow in 1 view(like in this example) it really starts to slow and lag the user interface.
How can I fix it, or what other alternatives do I have? Thanks!
Check out the WWDC 2014 video 'Advanced Graphics and Animations for iOS Apps'.
https://developer.apple.com/videos/wwdc/2014/
35:50 in ... it talks about why using that type of code to generate a shadow causes extra off screen passes for the GPU as it figures out the shape of the shadow.
The suggested solution is to also use the shadowPath property on the layer if you already know the shape of the shadow...
It's a great video to watch.
Depending on how your buttons look, one idea would be to use a background image for the buttons that includes the shadow, instead of adding the shadow in code. You can also use resizableImageWithCapInsets to make for the various possible sizes of your buttons.
UIImage* image = [UIImage imageNamed:#"button.png"];
UIEdgeInsets insets = UIEdgeInsetsMake(16, 6, 16, 6); // change the insets to fit your needs
image = [image resizableImageWithCapInsets:insets];
[button setBackgroundImage:image forState:UIControlStateNormal];
Documentation on resizableImageWithCapInsets can be found at https://developer.apple.com/library/ios/documentation/uikit/reference/uiimage_class/index.html#//apple_ref/occ/instm/UIImage/resizableImageWithCapInsets:
This solution may not be fit for you, depending on how your buttons look. But using images instead of adding the shadow from code should speed up your code.

How to mask UIViews in iOS

I've seen similar questions, but haven't found workable answers.
I want to mask a UIView using a grey image (need to convert to alpha scale for masking). The UIView has background. It should be easy to mask an image, but I want to mask any UIView.
Any clues will be appreciated.
I've been working on this problem for a couple of hours and have a solution that I think will do what you want. First, create your masking image using whatever means you see fit. Note that we only need the alpha values here, all other colours will be ignored, so make certain that the method you use supports alpha values. In this example I'm loading from a .png file, but don't try it with .jpg files as they don't have alpha values.
Next, create a new layer, assign your mask to its contents and set this new layer to your UIView's own layer, like so: you should find that this masks the UIView and all its attached subviews:
UIImage *_maskingImage = [UIImage imageNamed:#"mask"];
CALayer *_maskingLayer = [CALayer layer];
_maskingLayer.frame = theView.bounds;
[_maskingLayer setContents:(id)[_maskingImage CGImage]];
[theView.layer setMask:_maskingLayer];
With this done, you can set the UIView's background colour to whatever you like and the mask will be used to create a coloured filter.
EDIT: As of iOS8 you can now mask a view simply by assigning another view to its maskView property. The general rules stay the same in that the maskView's alpha layer is used to determine the opacity of the view it is applied to.
For apps targeting iOS 8.0+ this worked well (in this case, using a gradient as the mask) It avoids any need to resize or position the mask.
// Add gradient mask to view
func addGradientMask(targetView: UIView)
{
let gradientMask = CAGradientLayer()
gradientMask.frame = targetView.bounds
gradientMask.colors = [UIColor.blackColor().CGColor, UIColor.clearColor().CGColor]
gradientMask.locations = [0.8, 1.0]
let maskView: UIView = UIView()
maskView.layer.addSublayer(gradientMask)
targetView.maskView = maskView
}
In my case, I want to remove the mask once the user starts scrolling. This is done with:
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
exerDetailsTableView.maskView = nil
}
where the view is defined as an #IBOutlet:
#IBOutlet weak var exerDetailsTableView: UITableView!
Result:
I don't know the exact code off the top of my head but the basic idea is to have two UIViews. One UIView would have it's image property set to be the grey scale image and the other UIView would be set as usual the only difference is that you would position the initial UIView directly on top of the UIView containing the "normal" image.
I hope that is enough to push your idea a step further.

Resources