Even with iOS 13 having been out for quite a while now and Xamarin.Forms 4.5+ bringing a lot of improvements and fixes, I still find myself having to customise things for different projects (usually for the same reason as some of my other other posts: Graphic Designers and marketing people 😂).
One thing that was immediately picked up on one project when the upgrade to Xamarin.Forms 4.5+ was finally kicked off, was the frame shadows suddenly looked quite pronounced was the shadows on Frames compared to what used to look quite different with some tweaks:
Now customising the shadows on iOS isn’t a new thing, however suddenly the renderers I had in place to control this just stopped working and the designers insisting it be fixed or the planned technical upgrades should not be allowed to proceed, which at the time was not an option given Apple’s April 2020 deadline* for no UIWebView’s (which requires Xamarin.Forms 4.5+) and apps to target iOS 13.
Prior to Xamarin.Forms 4.5 the following code was working quite nicely:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class FrameRenderer_iOS : FrameRenderer { public override void Draw(CGRect rect) { try { base.Draw(rect); // Update shadow to match better material design standards of elevation Layer.ShadowRadius = 2.0f; Layer.ShadowColor = UIColor.Gray.CGColor; Layer.ShadowOffset = new CGSize(2, 2); Layer.ShadowOpacity = 0.80f; Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath; Layer.MasksToBounds = false; } catch (Exception ex) { Logging.Log(ex); } } } |
Since upgrading though, this code effectively did nothing to the UI. After some digging it turns out the renderer has changed and now the shadow is its own UIView. To customise the shadow now we need to find that view in the renderer and then apply some slightly different code to it so we can can customise how the shadow should look (which is a matter of opinion) for Frames.
I fiddled with a few ways and there’s no saying this is the best approach, but until more customisation in Xamarin.Forms is available, this is how I was able to get around things fairly painlessly and get out an updated product for my client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
using System; using System.ComponentModel; using CoreGraphics; using CustomIosFrameShadowsWithXF45.Controls; using CustomIosFrameShadowsWithXF45.iOS.Renderers; using UIKit; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; [assembly: ExportRenderer(typeof(Frame), typeof(CustomFrameRenderer))] namespace CustomIosFrameShadowsWithXF45.iOS.Renderers { /// <summary> /// Make iOS frame shadows look more natural than the default Xamarin.Forms implementation /// </summary> public class CustomFrameRenderer : FrameRenderer { protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { try { base.OnElementPropertyChanged(sender, e); // If the Frame's position or size changes we need to reset the shadow if (e.PropertyName == "X" || e.PropertyName == "Y" || e.PropertyName == "Width" || e.PropertyName == "Height") { SetFrameShadow(); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } public override void Draw(CGRect rect) { try { base.Draw(rect); SetFrameShadow(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } private void SetFrameShadow() { if (Element != null && Element.HasShadow) { if (Superview != null && Superview.Subviews != null) { foreach (var uiView in Superview.Subviews) { var name = uiView.ToString(); // Find the Xamarin.Forms ShadowView and customise the look and feel if (uiView != this && uiView.Layer.ShadowRadius > 0 && name.Contains("_ShadowView")) { var shadowRadius = 2.5f; uiView.Layer.ShadowRadius = shadowRadius; uiView.Layer.ShadowColor = UIColor.Gray.CGColor; uiView.Layer.ShadowOffset = new CGSize(shadowRadius, shadowRadius); uiView.Layer.ShadowOpacity = 0.8f; uiView.Layer.MasksToBounds = false; uiView.Layer.BorderWidth = 1; } } } Layer.MasksToBounds = true; Layer.BorderColor = Element.BorderColor.ToCGColor(); } } } } |
This renders things out in a more “correct way” (designer term not mine). Correct/cleaner/prettier, whatever you want to call it, using the above code you should be able to customise Frame shadows to whatever requirements you have on your project from simple more card view aligned views to bright Lime areas of fuzziness.
One thing to note about this approach is that it will render for all Frame instances, which may not be desirable, as such I more often than not take the approach of quickly bundling things into a custom control so you have more control over where this is used as well as being able to implement bindable properties to customise things:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System; using Xamarin.Forms; namespace CustomIosFrameShadowsWithXF45.Controls { public class CustomFrame : Frame { public CustomFrame() { } // You could also implement bindable properties such as the // following (or even better take all this code and submit a PR // to Xamarin.Forms for everyone, unfortunately I don't have // time right now :( // ShadowRadius // ShadowColor // ShadowOpacity } } ... [assembly: ExportRenderer(typeof(CustomFrame), typeof(CustomFrameRenderer))] namespace CustomIosFrameShadowsWithXF45.iOS.Renderers { /// <summary> /// Make iOS frame shadows look more natural than the default Xamarin.Forms implementation /// </summary> public class CustomFrameRenderer : FrameRenderer ... |
You could take the same approach with Effect’s, but honestly I find Renderers far more versatile and robust over the longer term.
So that’s it from me on that one, hopefully this helps some of you who have similar requirements that I did and saves you the reading and trial and error I went through.
As usual, if you want the entire working example, please have a look here on GitHub.
* Note: Due to the 2020 COVID-19 crisis Apple relaxed the deadlines for the new requirements, however at time of writing this doesn’t remove the need altogether so the update work has to be done regardless.
Edit 8-Apr-2020: After a round of testing I was advised on screens that needed to manipulate Frame’s (e.g. show/hide, expand collapse etc) things were reverting to the default implementation and causing some other elements to look incorrect. Article updated to reflect this.
Hi, thats awesome. You helped more than you think, thank you so much. This xamarin.forms 4.5 update was real shit but you are the saver of the day. Can you help me little more? I need to remove navigation bar shadow too. Its broken after 4.5 update too.
Glad it was useful :). I haven’t actually come across the issue you’re describing, though I would imagine a custom renderer for the Navigation Page’s would likely solve this if you have a bit of a play there.
Great solution, after the xamarin forms update I had the same issue. I have encountered though an issue regarding your solution. If I put multiple frames inslide a
hierarchy, the frames get stuck up on the top of the layout. I managed to fix this by deleting those two lines from your solution
uiView.Bounds = Bounds;
uiView.Frame = base.Frame;
Good luck!
Thanks for that Edvin, finally got around to updating things!
Thank you, I was exactly facing the same scenario with my client.
I came up with another solution, I noticed that the new shadow is added on top of the old one, so basically the
old code that used to work prior to Xamarin.Forms 4.5 is still valid, but you need to set Frame’s property
HasShadow=False for iOS in order to display it.
So basically I added this lines on my app.xml and it worked:
This solution stopped working for me after a recent upgrade to XF 4.7.0.1179. In case anyone else here faces the same issue, I was able to get things looking good for me again using the following:
private void SetFrameShadow()
{
var shadowRadius = 2.5f;
Layer.ShadowRadius = shadowRadius;
Layer.ShadowColor = UIColor.Gray.CGColor;
Layer.ShadowOffset = new CGSize(shadowRadius, shadowRadius);
Layer.ShadowOpacity = 0.8f;
Layer.MasksToBounds = false;
Layer.BorderWidth = 1;
Layer.CornerRadius = Element.CornerRadius;
Layer.ShadowPath = UIBezierPath.FromRoundedRect(Layer.Bounds, Element.CornerRadius).CGPath;
Layer.BorderColor = UIColor.Clear.CGColor;
}
Thanks Dan, that’s useful. Glad you’re still going hard with Xamarin :).