SwiftUI ScrollView BlendMode Issues - ios

I have run into an issue with SwiftUI and ScrollView. When I have a subview with a blend mode attribute in a scrollview, the blend does not affect what is behind the scrollview. But if I put the same item in a Stack, it does affect the background.
I tried to apply the blend mode directly to the scrollview, but since there are other items in the subview that are not blended, it messes that up.
Here is a sample to illustrate the issue. with this, you can see the gradient blend works in the Stack but not in the scrollview.
let mode: BlendMode = .overlay
func item(_ num: Int) -> some View {
return AnyView(
ZStack {
Rectangle()
.fill( LinearGradient(gradient: Gradient(colors: [Color.blue, Color.black]), startPoint: .top, endPoint: .bottom))
.frame(width: 100, height: 100)
.blendMode(mode)
Text("Number = \(num)")
.font(.headline)
.foregroundColor(Color.white)
}
)
}
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.fill(RadialGradient(gradient: Gradient(colors: [Color.black, Color.red]), center: .topLeading, startRadius: 200, endRadius: 800))
HStack (spacing: 50) {
VStack (spacing: 50) {
ForEach((1...5).reversed(), id: \.self) {
item($0)
}
}
ScrollView {
VStack (spacing: 50) {
ForEach((1...5).reversed(), id: \.self) {
item($0)
}
}
}
//.blendMode(mode)
/*
blendMode for item() in ScrollView does not work.
Uncomment blendMode modifier above to get gradient blend, but text is not correct
*/
}
}
}
}
I have also tried to apply the black-to-red Rectangle as a background of the Scrollview, and that doesn't fix it, either.
ScrollView {
VStack (spacing: 50) {
ForEach((1...5).reversed(), id: \.self) {
item($0)
}
}
}
.background(
Rectangle()
.fill(RadialGradient(gradient: Gradient(colors: [Color.black, Color.red]), center: .topLeading, startRadius: 200, endRadius: 800))
)
In the pictures below, the look I want is in the left VStack.
No blend mode modifier on ScrollView
Blend mode modifier applied to Scrollview
Any idea how to get around this?
Only thing I could think, which seems very hacky, was to basically create two scrollviews z-stacked with blend applied to only the scrollview needing it and then put the items not blended in the topmost scrollview.
However, I don't think that is viable as I see no way to sync scrolling across the two scrollviews.

I've come across a similar issue and from experimenting it seems as though the blend modes are affected when the SwiftUI View with the blend mode set is directly inside a UIHostingView (such as a ScrollView).
You can get the correct blending by placing another SwiftUI view below the one with the blend mode inside the scroll view, e.g.
ScrollView {
ZStack {
Rectangle()
.fill(RadialGradient(gradient: Gradient(colors: [Color.black, Color.red]), center: .topLeading, startRadius: 200, endRadius: 800))
VStack (spacing: 50) {
ForEach((1...5).reversed(), id: \.self) {
item($0)
}
}
}
}
This doesn't quite fix the issue in your example as the two backgrounds are not quite aligned, but in a case where the scroll view takes up the screen it might help.
It seems that adding a .background to the scroll view adds it below the ScrollView rather than at the bottom of the hierarchy in the ScrollView which is why that doesn't seem to fix it.
My issue was in a List row (which is backed by a UITableView), and setting an explicit background on the item inside the List helped there. Hopefully this will point you/others in the right direction.

Related

How to make `VStack` view inside `ScrollView` expand up and take the space under navigation title while keeping the content in the same spot?

Here is an example of what i currenly have:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack() {
Text("Foo")
}
.frame(maxWidth: .infinity, idealHeight: 200)
.background(Color(uiColor: .systemBackground))
}
.background(Color(uiColor: .secondarySystemBackground))
.navigationTitle("Title")
}
}
}
Here is what it renders:
What i want to achieve is the white background all the way up to the top of the screen while preserving the padding so that the actual content of the VStack is displayed below the title and not underneath it and when scroll happens VStack's content follows the title when it goes from large to inline.
Alright, i found a way to do that. It's hacky and most probably won't work properly for all screen sizes or when changing orientation of the device but i recon it would work for most iPhone models in portrait mode.
The idea is to place a ZStack inside ScrollView and put my VStack there along with a Rectangle of required color and then offset the Rectangle up to occupy the space underneath the navigation bar and more to keep the color the same when scroll spring effect happens.
Here it the code:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
ZStack(alignment: .top) {
Rectangle()
.foregroundColor(Color(uiColor: .systemBackground))
.frame(height: UIScreen.main.bounds.height * 0.5)
.offset(y: -UIScreen.main.bounds.height * 0.5)
.padding(.bottom, -UIScreen.main.bounds.height * 0.5)
VStack() {
Text("Foo")
}
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(Color(uiColor: .systemBackground))
}
}
.background(Color(uiColor: .secondarySystemBackground))
.navigationTitle("Title")
}
}
}
Note: don't forget to set alignment: .top to ZStack!

onDrag preview isn’t just of the dragged view but also displays the background the view is laid on top of

I’m using an .onDrag modifier on a view that has rounded corners:
struct RootView: View {
#State var dragging: Bool = false
var body: some View {
VStack {
Color.red.cornerRadius(32)
.frame(width: 300, height: 300)
.onDrag {
dragging = true
return NSItemProvider(object: NSString())
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
}
}
The problem is when I invoke a drag - due to the automatic drop shadow effect applied by the system - it doesn’t feel as though I’m dragging the card on its own. See below.
Is there any way to get the card to look dragged with a transparent background, as opposed to above whatever background colour it was laid on top of?
There is content shape type for this,
Color.red.cornerRadius(32)
.frame(width: 300, height: 300)
.contentShape(.dragPreview, RoundedRectangle(cornerRadius: 32)) // << here !!
.onDrag {

SwiftUI TabView with PageTabViewStyle in Landscape on device with safeArea adding odd leading edge inset

I have a TabView with PageTabViewStyle and when my device is in landscape orientation, there is an odd inset on the leading edge. This is not there on devices without a safeArea and it is not there on any edge in portrait.
struct ContentView: View {
var body: some View {
TabView {
Color.red
.edgesIgnoringSafeArea(.all)
}
.tabViewStyle(PageTabViewStyle())
.edgesIgnoringSafeArea(.all)
}
}
That would be the TabView doing that. That is actually the "prior" view, which would be an empty view for your set up. You can see if you run this code in landscape:
var body: some View {
TabView {
Color.red
.edgesIgnoringSafeArea(.all)
Color.green
.edgesIgnoringSafeArea(.all)
Color.blue
.edgesIgnoringSafeArea(.all)
}
.tabViewStyle(PageTabViewStyle())
.edgesIgnoringSafeArea(.all)
}
Then swipe to the next view. You will see that slice of the screen has turned red. Swipe again and it turns blue.
The fix: set a .frame on the TabView that is the device size, but all of it in a ScrollView and put an .edgesIgnoringSafeArea(.all) on that:
var body: some View {
ScrollView {
TabView {
Color.red
Color.green
Color.blue
}
.frame(
width: UIScreen.main.bounds.width ,
height: UIScreen.main.bounds.height
)
.tabViewStyle(PageTabViewStyle())
}
.edgesIgnoringSafeArea(.all)
}
And yes, the TabView can scroll vertically, but it will spring back.
If you want to have only the background in fullscreen try it like this:
var body: some View {
ZStack {
// Background ignoring safe area
LinearGradient(colors: [Color(hue: 0.106, saturation: 0.234, brightness: 0.922, opacity: 1.0), Color.white], startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
// TabView
TabView() {
Onboarding1()
Onboarding2()
Onboarding3()
}
.tabViewStyle(PageTabViewStyle())
}
}

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:

Resources