What I'm trying to Achieve
I am trying to implement the gradient bubble effect in Swift iOS, where the chat bubbles towards the top are a lighter color and the chat bubbles towards the bottom are a darker color, and when you scroll a bubble you see the gradient change.
The link below is an example of the iMessage gradient effect.
An example image of the iMessage gradients
What I've Tried
I created a view and added a gradient layer:
let gradient = CAGradientLayer()
gradient.startPoint = CGPoint(x: 1, y: 0)
gradient.endPoint = CGPoint(x: 0, y: 1)
gradient.colors = [UIColor.systemBlue.cgColor, UIColor.systemPink.cgColor]
gradient.frame = UIScreen.main.bounds
view.layer.addSublayer(gradient)
view.backgroundColor = .yellow
I created a view and used it as a mask
mask.backgroundColor = .yellow
mask.alpha = 1
mask.frame.size = CGSize(width: 100, height: 100)
mask.center = view.center
view.mask = mask
The result is like this Gif below:
Example of my progress using a Mask View
I was originally hoping to add a gradient to a UICollectionView and have the UICollectionViewCells mask the gradient to achieve the above iMessage gradient effect, but then I realized I can only apply one mask to a UIView (not multiple), so I'm stuck on using this approach.
Other Ideas
My other ideas were to apply a gradient to each UICollectionViewCell and determine the gradient offset of each UICollectionViewCell manually based on the location of the cell, however, my main concern is this is not going to have good performance.
Please Help
I was hoping to see if anyone could outline a general method or link to how to achieve the iMessage gradient background effect?
I understand this is a more general question and often times general questions are "bad" questions for stack overflow, but I'm really stuck on this problem and would incredibly appreciate any help or advice for achieving this effect!
Thank you for your time!
Solution Figured Out
Wow, this is sketchy/hacky to implement. Below is a GitHub Gist of how its done.
https://gist.github.com/josharnoldjosh/e04d41f10de6ab378da931ab11370d11
The way it works is you set a background to the gradient, then, you mask each individual cell, "cutting out" a hole in the cell to be transparent. The rest of the cell must be white to simulate the background being white.
There are multiple parts to this. First, you need to set up a gradient that is top-to-bottom. Your current gradient goes from the top right to the bottom left.
Change these 2 lines:
gradient.startPoint = CGPoint(x: 1, y: 0)
gradient.endPoint = CGPoint(x: 0, y: 1)
To
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
Next is the issue of how to make the view take on the gradient color tied to the screen, as you scroll the table view/collection view. That is not easy, and I'm not sure how you'd do that. I would probably have to attach code to the table view/collection view's UIScrollViewDelegate and implement the scrollViewDidScroll(_:) method, figure out the change in scroll view offset, and shift the gradient layer to match the scroll offset.
Related
I've been searching for help on this all day but I can't find an answer.
I have a subview in which I am drawing a rectangle, the rectangle is framed by the subview so I need to know the size of the subview as adjusted by autolayout to correctly frame it. I can't find a way of doing this from ViewDidLoad(), so that the rectangle is correctly rendered at start-up. I have tried the following:
Using dayView.setNeedsLayout() followed by dayView.layoutIfNeeded() before I draw the rectangle in viewDidLoad() but a check either side of these statements shows the dayView.bounds unchanged.
Drawing the view from viewDidLayoutSubviews(), which works, but results in my rectangle being drawn 5 times as viewDidLayoutSubviews() is called for every subview (I have 5 of them) that is redrawn (the relevant subview containing the rectangle is redrawn on call 4 of 5) - this seems wasteful of resources, surely there is a better way?
Drawing the view twice within ViewDidLoad(), hoping the first forced draw will cause the view to be resized, so the second draw will have access to the new bounds after the first draw (desperate I know, but it still doesn't work).
I hope someone can help.
func drawGradient(object: UIView, rect: CGRect, slackX: Int, gradWidth: Int, yPos: Int) -> Void {
// the rectangle width and height set to fit within view with x & y border.
let gradientView = UIView(frame: rect)
let gradient = CAGradientLayer()
gradient.frame = gradientView.frame
gradient.colors = getDayGradientLocations().gradientCol
gradient.locations = getDayGradientLocations().gradientLoc
dayView.layer.addSublayer(gradient)
let civilDawn = getTimeAsProportionOfDay(time: tides.civilDawn) * Double(rect.height) + Double(yPos)
let path = UIBezierPath()
path.move(to: CGPoint(x: slackX, y: Int(civilDawn)))
path.addLine(to: CGPoint(x: slackX + Int(rect.width), y: Int(civilDawn)))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = CGFloat(2)
let civilDusk = getTimeAsProportionOfDay(time: tides.civilDusk) * Double(rect.height) + Double(yPos)
path.move(to: CGPoint(x: slackX, y: Int(civilDusk)))
path.addLine(to: CGPoint(x: slackX + Int(rect.width), y: Int(civilDusk)))
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = CGFloat(2)
object.layer.addSublayer(shapeLayer)
drawLabel(object: object, rect: rect, slackX: slackX, offset: 15, time: tides.civilDawn)
drawLabel(object: object, rect: rect, slackX: slackX, offset: -15, time: tides.civilDusk)
}
A couple of observations:
If you are adjusting a view’s frame in your view controller, the right place to do this is in viewDidLayoutSubviews. Yes, this is called a number of times, but it generally doesn’t have any observable impact on performance.
I wouldn't advise any of those extremely brittle techniques of setNeedsLayout, layoutIfNeeded, or DispatchQueue.main.async inside viewDidLoad. The viewDidLayoutSubviews is the right place if you’re going to do this in the view controller.
Generally, if doing custom subview drawing and layout of subviews, you do this in the layoutSubviews of the view, rather than in any of the view controller methods.
Likewise, if you're doing any manual drawing, you’d put that in the UIView subclass (or any of the relevant CALayer subclasses), not the view controller.
Even better, rather than adjusting frames manually, it’s better to let the auto layout system handle this for you if you can. If you find yourself manually adjusting a frame, there are generally better patterns.
FWIW, you can define a container view’s constraints to be based upon the size (or intrinsic size) of its subviews (and set the content-hugging and compression-resistance of the relevant views). We often think of auto-layout as a top-down engine, but it works both ways.
If you show us your “drawing” code and/or a screen snapshot or two, we can probably offer more concrete counsel.
Got to main thread in viewDidLoad and Perform action will resolve your issue
DispatchQueue.main.async {
// here you can add anything it will have proper frame
}
I know that UIVisualEffectView ist very uncustomizable, so I can't setup the radius of blurness of the view or even the color.
Now I realized I could not even mask one.
I want to realize a Tabbar with blured Background, but to the top corner it gets sharper till 100%.
Because I know I couldn't adjust the blur radius I had the idea to work with a gradient mask to archieve something like this:
But as sad at the beginning I could not even mask a simple Rectangle:
let gradientMask = CAGradientLayer()
gradientMask.frame = effectView.frame
gradientMask.colors = [UIColor.black.cgColor, UIColor.clear.cgColor]
effectView.layer.mask = gradientMask
The result is, the UIVisualEffectView doesn't show at all anymore.
Have you guy a workaround or something else?
EDIT: The view in the screenshot is for example, in the final app the background is a dynamic list with tiles where I can scroll through. So the workaround with snapshots will not work in my case.
so i added a gradient view to my UIView but when i run the app it doesn't display the UIVIEW properly please have a look at the screen shots i tried it running without the gradient and view worked perfectlyhere you can see the code i wrote for gradient view
and this is main story board
First of all, both of your gradient colors are white.
The cause why your gradient is not dispalying is you're specifying the locations wrong. There is no need to setup startPoint and endPoint so you can delete those lines.
gradient.startPoint = CGPoint.zero
gradient.endPoint = CGPoint(x: 0, y: 1)
And edit the locations to
gradient.locations = [0,1]
If you want to show the colors other way around, just switch them in the colors property.
Next time it would be useful to post the code regular way not as an screenshot.
I have viewController in storyboard. And 4 squares. I want to place my squares on view. At first I want to show two squares. If I press on button I want to my red 2 squares move left and I show next 2 blue squares. Like this animation.
Do I need to create a scrollView or collectionView or something else to move the squares? What the best way to create this?
Use UiVew.animate and CGAffineTransform
Setup
Create a UIView that will contain your squares. Make sure that UIView has clip to bounds enabled. Then, add your four squares in, with the blue ones nested inside the UIView, but with their coordinates outside of the container so that they're getting clipped.
Then, when you want to move them, you simply translate all of your squares x to the left. By putting this movement inside of a UIView.animate block, iOS will perform the animation for you.
UIView.animate(withDuration: 2.0) {
self.redTopView.transform = CGAffineTransform(translationX: -160, y: 0)
self.redBottomView.transform = CGAffineTransform(translationX: -160, y: 0)
self.blueTopView.transform = CGAffineTransform(translationX: -160, y: 0)
self.blueBottomView.transform = CGAffineTransform(translationX: -160, y: 0)
}
Final result: http://g.recordit.co/SToMSZ77wu.gif
I have a UICollectionView and I'm implementing sticky headers as per this link: http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using#notes
It works fantastically however my window has a background image applied, and my header views have a transparent background. Consequentially, when my items scroll above the header view, you can still see them.
Ideally I would fade out the cells with a gradient, to the point it is invisible by the time it appears behind the header view.
Thanks.
You haven't posted any code, so here's a go at it without looking at code. Just setup a mask layer over your UICollectionView's superview and you're good to go:
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = self.collectionView.superview.bounds;
gradient.colors = #[(id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor];
// Here, percentage would be the percentage of the collection view
// you wish to blur from the top. This depends on the relative sizes
// of your collection view and the header.
gradient.locations = #[#0.0, #(percentage)];
self.collectionView.superview.layer.mask = gradient;
For this solution to work properly, you'd have to embed your collection view in a super view of its own.
For more information on layer masks, check out the documentation.
I created a fade mask over a collectionview that has this kind of effect. Maybe you're looking for something similar.
// This is in the UICollectionView subclass
private func addGradientMask() {
let coverView = GradientView(frame: self.bounds)
let coverLayer = coverView.layer as! CAGradientLayer
coverLayer.colors = [UIColor.whiteColor().colorWithAlphaComponent(0).CGColor, UIColor.whiteColor().CGColor, UIColor.whiteColor().colorWithAlphaComponent(0).CGColor]
coverLayer.locations = [0.0, 0.5, 1.0]
coverLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
coverLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
self.maskView = coverView
}
// Declare this anywhere outside the sublcass
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
}
Additionally, you can make it sticky (i.e. it will always fade out the cells on the edge, instead of scrolling with the collection) by adding this to the collectionview subclass.
func scrollViewDidScroll(scrollView: UIScrollView) {
self.maskView?.frame = self.bounds
}
would seem to me the code you are following/using has done heavy work for you. As far I can see (not in position to test right now) just pass the alpha attribute:
layoutAttributes.zIndex = 1024;
layoutAttributes.frame = (CGRect){
.origin = origin,
.size = layoutAttributes.frame.size
like such
layoutAttributes.zIndex = 1024;
layoutAttributes.alpha = 0.1; //add this
layoutAttributes.frame = (CGRect){
.origin = origin,
.size = layoutAttributes.frame.size
instead of having a transparent background on your header, I would create a gradient transparent png and use that instead. It'd be a lot more efficient and easier handling the gradient with an image than doing it with code.
You should use a UIScrollViewDelegate for the CollectionView and use the scrollviewdidscroll method to create the fade, or subclass UICollectionViewFlowLayout.
Here is how I achieved that effect. I created in photoshop a gradient image, fading to the color of the background, which is in my case black. Here's what it looks like:
I placed the ImageView on my ViewController. I stretched it to the correct size and location of where I wanted and used AutoLayout constraints to lock it in place. (I had to use the arrow keys on my keyboard to move it around at times because clicking and dragging the location of the image tended to drop it inside of the CollectionView)
Click the ImageView, go to Editor -> Arrange -> Send to Front to make sure it sits on top of the CollectionView.
Image mode is Scale to Fill, and I have deselected User Interaction Enabled.
This will take some tweaking to get everything perfect but it works very well and looks nice.
I'm not entirely sure how you mean by with your background image and whatnot, but maybe make the gradient image part of the actual background image you have, so it blends in.