SwiftUI: stretch LinearGradient on a rectangle - ios

I am trying to render a linear gradient on a rectangle. Here is the code:
Rectangle()
.foregroundColor(Color.red)
.overlay(
LinearGradient(gradient: Gradient(colors: [.red, .yellow, .blue]), startPoint: .topLeading, endPoint: .bottomTrailing)
)
.frame(width: 300, height: 200)
.position(x: 170, y: 120)
When I render it on square, everything looks correct:
When I render it on a rectangle, however, it stops looking like it's going from topLeading corner to bottomTrailing. It just looks like it's the same one which was clipped:
Here is how it's rendered in svg (blue is not the same it seems, but that's not the important part), and how I want it to look in swift:
The yellow diagonal should go directly from one corner to another, since I am specifying startPoint: .topLeading, endPoint: .bottomTrailing. It says here SwiftUI diagonal LinearGradient in a rectangle that this is a standard behaviour, and that's ok - I'm not trying to say SwiftUI renders it incorrectly, I just need a way to make it look like in svg.

What a fun problem :)
The "naive" approach would be to render the gradient as a square and then either rotate it or squeeze or stretch it to fit the rectangle. Rotating requires more maths so here's the "squeezing" version:
struct ContentView: View {
var body: some View {
Rectangle()
.overlay(
GeometryReader { g in
LinearGradient(
gradient: Gradient(colors: [.red, .yellow, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(width: g.size.width, height: g.size.width)
.scaleEffect(x: 1.0, y: g.size.height / g.size.width, anchor: .top)
}
)
.frame(width: 300, height: 200)
}
}
Notes:
To do the squeeze, we need to know the proportions of the rectangle, you were already using an overlay() and overlays are bounded by the Rectangle() in this case, so we can just read the size with the GeometryReader();
We need to start with the gradient applied to a square and there is a couple of ways to do squares. As we already know the size of the rectangle, I went with a square with a side equal to the width of the enclosing rectangle;
Then we apply the scaleEffect(), squeezing the height of the square to the height of the rectangle.
Please let me know if that's what you were looking for or if there is a better way to do to this!
Cheers,
–Baglan

Related

How to resize a rectangle to fit a device in SwiftUI

I have a rectangle in a frame as shown below
Rectangle in iPhone 13
Rectangle in iPhone 8
I am trying to proportionally adjust the height of rectangle so that there is an equal proportion of white in every iPhone model, but as is clearly visible the rectangle takes over the entire screen in a smaller iPhone model.
I am sure SwiftUI has a formal feature that allows for the rectangle to be resized proportionally.
How can I do this? Code shown below
VStack {
ZStack {
//Rectangle 12
Rectangle()
.fill(Color(#colorLiteral(red: 0.7137255072593689, green: 0.10196077823638916, blue: 0.10196077823638916, alpha: 1)))
.frame(width: 391, height: 800)
}
Spacer()
}.ignoresSafeArea(edges: .top)
EDIT: It seems like my question, while describing what I want in isolation did not truly encapsulate what I want
I want to create this design
enter image description here
However, I want it to scale properly across devices.
How would I go about doing this?
This is exactly what a GeometryReader is for. It reads the size given to it, in this case the whole screen. Then, you can put a .frame() on the Spacer which is a compressible view to keep it at a set size. In the example code below, it is 10% of the view height.
struct ProportionalRectangle: View {
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color(#colorLiteral(red: 0.7137255072593689, green: 0.10196077823638916, blue: 0.10196077823638916, alpha: 1)))
Spacer()
.frame(height: geometry.size.height * 0.1)
}
}
.ignoresSafeArea()
}
}
Do you mean you want to divide the view into red rectangle and white.
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.fill(.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Rectangle()
.fill(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}.ignoresSafeArea()
}
}

How to center crop an image in SwiftUI

I'm new to SwiftUI. I guess everyone is at this point. I've been an app developer for about 6 years now and I feel stupid asking this question on StackOverflow. But I looked everywhere. How do I center crop an image in an ImageView in SwiftUI?
I know there's an option to change the aspect ratio but I only see fit and fill. I just want the imageView to centerCrop(android term) the image. Does anybody know?
Android's ImageView.ScaleType documentation describes CENTER_CROP as:
CENTER_CROP
Scale the image uniformly (maintain the image's aspect ratio) so that
both dimensions (width and height) of the image will be equal to or
larger than the corresponding dimension of the view (minus padding).
The image is then centered in the view.
This is essentially what Aspect Fill Scaling (aka .scaledToFill()) does, except (surprisingly) Aspect Fill doesn't clip the parts that fall outside of the frame.
By making the image .resizable, and applying .scaledToFill(). the image will be scaled proportionally to fill its available frame leaving off the top and bottom or sides as necessary. .clipped() then removes the parts of the image outside of the frame.
Image("myImage")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200, alignment: .center)
.clipped()
To make this more convenient, I created this extension of Image:
extension Image {
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
}
To use the Image extension, just put it in a file in your project (a name like image-centercropped.swift will work nicely). Then just add .centerCropped() to any image you want to be center cropped.
Image("apolloimage").centerCropped()
It uses GeometryReader to figure out its frame so that it can crop the image correctly, which means you don't have to specify the frame to get proper clipping. You are free to size the image however you like using an explicit frame, or by just adding padding() and Spacer() to keep it nicely placed relative to other user interface items.
For example: If you want an image to fill the screen of the phone:
struct ContentView: View {
var body: some View {
Image("apolloimage")
.centerCropped()
.edgesIgnoringSafeArea(.all)
}
}
will nicely show the center of the image by scaling the image to show either the full height or the full width of the image and cropping the parts the hang over on the other dimension.
Demonstration:
Here's a demo that shows how the image is centered and cropped as the image grows. In this demo, the frame width is a constant 360 while the frame height varies from 50 to 700 as the slider advances to the right. At the beginning when the frame is short, the tops and bottoms of the image are cropped. As the frame exceeds the aspectRatio of the original image, the resulting image is centered but cropped on the left and right sides.
struct ContentView: View {
#State private var frameheight: CGFloat = 50
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("apolloimage")
.resizable()
.scaledToFill()
.frame(width: 360, height: self.frameheight)
.clipped()
Spacer()
Slider(value: self.$frameheight, in: 50...700)
.padding(.horizontal, 20)
}
}
}
or an equivalent test using .centerCropped():
struct ContentView: View {
#State private var frameheight: CGFloat = 50
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("apolloimage")
.centerCropped()
.frame(width: 360, height: self.frameheight)
Spacer()
Slider(value: self.$frameheight, in: 50...700)
.padding(.horizontal, 20)
}
}
}
Alternate Solution
Another way to make a center cropped image is to make the image an .overlay() of Color.clear. This allows Color.clear to establish the clipping bounds.
Color.clear
.overlay(
Image("apolloimage")
.resizable()
.scaledToFill()
)
.clipped()
and the corresponding extension to Image looks like this:
extension Image {
func centerCropped() -> some View {
Color.clear
.overlay(
self
.resizable()
.scaledToFill()
)
.clipped()
}
}
I was able to crop the square centre of image for view like iPhone Photo app.
extension Image {
func centerSquareCropped() -> some View {
GeometryReader { geo in
let length = geo.size.width > geo.size.height ? geo.size.height : geo.size.width
self
.resizable()
.scaledToFill()
.frame(width: length, height: length, alignment: .center)
.clipped()
}
}
}
I was originally using #vacawama's answer for this which uses the GeometryReader but discovered that actually this isn't necessary.
(I'm writing this with Xcode 13 and running in iOS15 if that makes any difference?)
Using this is sufficient...
Image(uiImage: image) // insert your own image here :D
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
I'm using this as the label parameter of a Button inside a List so the whole thing is like...
Section("Photo") {
Button {
// the action
} label: {
if let image = viewStore.imagePickerState.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
} else {
PersonAvatarButton()
}
}
}
.aspectRatio(1, contentMode: .fill)
.listRowBackground(Color.gray)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
I haven't defined the frame anywhere for this, just the aspect ratio of the Section.
What I end up with is a square button with rounded corners and the photo all the way to the edge. Resized to fill but not squashed in any way. And the button size is determined by the screen size.
So I don't have any concrete sizes in my code.

Subview have parent's shadow, even with a background

So I just started developing with SwiftUI and I'm running in a small problem. Subviews are also displaying superview's shadow, even if the superview has a background. Does someone know how to fix this?
HStack {
HStack {
[...]
}
.padding(.leading, 12.0)
.padding(.trailing, 4.0)
.padding(.vertical, 16.0)
.background(Color("lightGreen"))
.cornerRadius(10)
}
.padding(8)
.background(Color.white)
.shadow(color: Color("tabShadow"), radius: 0.0, x: 0.0, y: -0.5)
.shadow(color: Color("tabShadow"), radius: 0.0, x: 0.0, y: 0.5)
As stated, the first HStack's shadow shouldn't be replicated into the child one, but it is. Only the first one though. Any hints?
Certain modifiers, when placed on a stack, are inherited by all their children. For instance, if you have a stack containing a bunch of Text views, you can place one .font() modifier on the stack and they will all be modified.
It appears that .shadow() is one of those modifiers. As to why only one is inherited, I suspect that the designers of SwiftUI don't expect .shadow() to be called more than once on a particular view, and didn't test for that.
If you are just trying to get a colored line across the top and bottom of the view, maybe try something like
.background(Color.white)
.background(Color("tabShadow").offset(x: 0, y: -0.5))
.background(Color("tabShadow").offset(x: 0, y: 0.5))
I am also newbie in swiftUI , but I think the problem is related to modifiers order and the fact that they change the View type.
I was able to solve the problem by adding .background(Color.white) and .cornerRadius(2.0) modifiers just before the shadow modifier and that applied the changes in parent (not children) View.
struct TestSwiftUIView: View {
var body: some View {
VStack {
Text("Hello World")
Text("Hello World")
}
.padding()
.background(Color.white)
.cornerRadius(2.0)
.shadow(radius: 3)
}
}
You can try overlay and background tricks when you have to make them render in multiple passes. In above case, the overlay will not be affected by the shadow or other effects.
If you think they are subViews, actually, they just render after the superView. It's a 2D world. So the overlay will be quite independently.
The only problem is just the size of overlay.
The hidden() is used here to occupy the position of invisible overlay. It's very cool if you master these layout skills.
struct ContentView: View {
var body: some View {
HStack {
SubContentView().hidden()
}
.padding(8)
.background(Color.white)
.shadow(color: Color.red, radius: 0, x: 0.0, y: -0.5)
.shadow(color: Color.red, radius: 0, x: 0.0, y: 0.5)
.overlay(SubContentView())
}
}
struct SubContentView: View {
var body: some View {
HStack{
Text("a")
Text("b")
Text("c")
Text("a")
Text("b")
Text("c")
}.padding(.leading, 12.0)
.padding(.trailing, 4.0)
.padding(.vertical, 16.0)
.background(Color.green)
.cornerRadius(10)
}
}
If you want to prevent the shadow the be applied to subviews then use the shadow modifier only inside the .background modifier like so:
VStack {
...
}.background(Color.white.shadow(color: .black.opacity(0.3), radius: 1, x: 1, y: 1))
Just Wrap your main view with some View and setShadow(1) to that view. and setShadow(0) to your main view. it overrides the parents shadow.
Lets say you have:
VStack{
Text("1")
Text("2")
}
and you want to set shadow to this VStack. So, wrap this VStack with another VStack (any View), and setShadow to that stack. Inner shadow overrides outer shadow. Finally your code should be:
VStack{
VStack{
Text("1")
Text("2")
}.shadow(radius: 0)
}
.shadow(radius: 1)

SwiftUI - How to add foreground linear gradient on image

I am unable to find any related documentation on how to do a linear gradient on the foreground for an image I have with SwiftUI.
I have tried to do it like so:
Image("IconLoseWeight")
.frame(width: 30.0, height: 30.0)
.padding(.leading, 17)
.foregroundColor(LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .top, endPoint: .bottom))
Actually, the code shown above doesn't display any errors, but it breaks the code with warnings that make no sense in the top level Stacks (which I think is a bug with Xcode or SwiftUI). If I remove the foreground modifier, the code runs perfectly.
That's because foregroundColor wants a Color, but LinearGradient is a struct that conforms to the protocols ShapeStyle and View.
If I understand you correctly you want to fill the intransparent area of an image with a gradient?
ZStack {
Color.white // For the background. If you don't need a background, you don't need the ZStack.
LinearGradient(gradient: Gradient(colors: [.green, .blue]), startPoint: .top, endPoint: .bottom)
.mask(Image("AssetWithTransparency")
.resizable()
.padding()
.aspectRatio(contentMode: .fit))
}.cornerRadius(15)
The result looks like this:
The task here is to display gradient over an image. To display one view over another SwiftUI provides ZStack view, so, the code can have the next structure:
ZStack {
<Image>
<Rectangle with gradient>
}
Additionally, to make sure the image we use is resized correctly to the specified frame resizable modifier should be applied with correct contentMode:
Image("IconLoseWeight")
.resizable() // Make it resizable
.aspectRatio(contentMode: .fit) // Specifying the resizing mode so that image scaled correctly
After all, we need to apply frame and padding parameter to ZStack so that gradient has the same size as the image.
The result would look like that:
ZStack {
Image("IconLoseWeight")
.resizable() // Making the image resizable to the container size
.aspectRatio(contentMode: .fit) // Setting up resizing mode so that the image scaled correctly
Rectangle() // Shapes are resizable by default
.foregroundColor(.clear) // Making rectangle transparent
.background(LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .top, endPoint: .bottom), cornerRadius: 0)
// Specifying gradient (note that one color is .clear)
}
.frame(width: 30, height: 30) // Applying frame
.padding(.leading, 17) // Applying padding
Note, that we use a gradient from .clear to .black as we need a transparent gradient to make the image visible.
Agree with #RyuX51's answer and it's working well. But some how size and alignment of my image got changed. Because LinearGradient's frame isn't set. So here i came up with the solution for just applying gradient to the Image,
VStack{
Spacer()
Button(action: {
print("Add Photos")
}, label: {
LinearGradient(gradient: Gradient(colors: [.green, .blue]), startPoint: .top, endPoint: .bottom)
.mask(Image(systemName: "plus.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
).frame(width: 70, height: 70, alignment: .center)
})
}
The best way to do this as of the most recent SwiftUI release would be to use the .foregroundStyle() view modifier. I'm not sure when this approach became available but this code was tested with Xcode 14 and iOS 16.
Sample code below:
let gradient = Gradient(colors: [.purple, .cyan, .orange])
var body: some View {
Image(systemName: "figure.strengthtraining.traditional")
.font(.title)
.foregroundStyle(.linearGradient(gradient, startPoint: .top, endPoint: .bottom))
}
RyuX51's answer worked for me except the view ended up stretching to fill the available space. I wanted it to shrink to fit the size of the image I was using.
This worked well for my needs:
extension View {
/// Fills in self with `fill`, maintaining self's natural size
/// - Parameter fill: View to fill in self with (i.e., a gradient)
/// - Returns: Filled-in version of self
#ViewBuilder func filled(with fill: () -> some View) -> some View {
self.overlay {
fill().mask { self }
}
}
}
So for example:
Image(systemName: "car.2.fill")
.font(.system(size: 75))
.imageScale(.large)
.aspectRatio(contentMode: .fit)
.filled {
LinearGradient(
gradient: Gradient(colors: [.green, .blue]), startPoint: .top, endPoint: .bottom)
}
.border(.orange, width: 2)
will give you:

Aligning Contained Views in SwiftUI

OK. I am working on reproducing this project as a pure SwiftUI View.
I have my work cut out for me. I know that the system is still pretty buggy. The first update helped a lot, but there's still issues.
Nevertheless, I still want most of the problems to be of my own making. Those, I can fix.
I am laying out the control in the same manner as with the original, with three layers: a background circle, a middle movable element (the spinner), and a top transparency mask.
The circle is done, and I'm working on mapping out the spinner layer.
I want it to look something like this:
I currently have it looking like this:
The issue is that I am having a devil of a time getting the image dimensions in order to inform the offset, but what I'd really like, is to simply make the image anchor point the top, so the offset is sorted.
I tried messing with the alignmentGuide and frame stuff, but I must not be understanding it well, as they seem to be ignored.
Here's the code snippet for one image radii, which is where this should happen:
struct RVS_SwiftUISpinner_ItemDisplayView: View {
#State var itemImage: Image
#State var size: CGSize
var body: some View {
return VStack(alignment: .center, spacing: 0) {
self.itemImage
.resizable()
.aspectRatio(contentMode: .fit)
.offset(y: -self.size.height)
.frame(alignment: .top)
}
.frame(width: self.size.width,
height: self.size.height,
alignment: .bottom)
.background(Color(red: 0, green: 1.0, blue: 1.0, opacity: 0.25))
.offset(y: -self.size.height / 2.0)
}
}
Does anyone see what I'm doing wrong? I will keep at this, and I WILL find and fix the issue eventually, but I'm still basically making this up as I go along.
OK. I think I have it:
struct RVS_SwiftUISpinner_ItemDisplayView: View {
#State var itemImage: Image
#State var size: CGSize
var body: some View {
return VStack(alignment: .center, spacing: 0) {
self.itemImage
.resizable()
.aspectRatio(contentMode: .fit)
.frame(alignment: .top)
}
.frame(width: self.size.width,
height: self.size.height,
alignment: .top)
.background(Color(red: 0, green: 1.0, blue: 1.0, opacity: 0.25))
.offset(y: -self.size.height / 2.0)
}
}
I made the OUTER alignment .top.
Onward and upward...

Resources