Animation transition of one gradient to another SwiftUI - swift

I was trying to change the gradient of a background depending on the value of a state value which should be possible, and it is. However, I wanted to animate this change of gradient/background (as you often do when a property depends on some transition). So I tried setting up a ternary operator that would change the background to a different gradient, with an animation/transition. The background does change, but there is no smooth transitions, simply a harsh change. Here is a minimal working example I created to illustrate the issue:
import SwiftUI
// Based on https://nerdyak.tech/development/2019/09/30/animating-gradients-swiftui.html
struct ContentView: View {
#State var animCheck: Bool = false
var body: some View {
VStack {
Button(action: {
self.animCheck = true
}){
Text("Change gradient")
}
Button(action: {
}){
Text("random button")
.background(self.animCheck == true ? LinearGradient(gradient: Gradient(colors: [.white, .green]), startPoint: .leading, endPoint: .trailing).transition(.slide).animation(.spring()) : LinearGradient(gradient: Gradient(colors: [.black, .orange]), startPoint: .leading, endPoint: .trailing).transition(.slide).animation(.spring()) )
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Is there any way to get around this/create the intended effect of an animated background change here?
Thanks.

We need to animate container to make transition work, so here is a solution:
Text("random button")
.background(
VStack {
if self.animCheck {
LinearGradient(gradient: Gradient(colors: [.white, .green]), startPoint: .leading, endPoint: .trailing)
.transition(.slide)
} else {
LinearGradient(gradient: Gradient(colors: [.black, .orange]), startPoint: .leading, endPoint: .trailing)
.transition(.slide)
}
}.animation(.spring()))

Related

SwiftUI: underlined text does not work with background material

When I tried to display an underlined text with background material, I faced a problem
After some work around, I succeeded in creating the minimal test case that does not work as I expect
Could you please tell me, that is my wrong expectations, or I just missed something?
BTW: That works fine with other text modifiers, such as .bold() or .italic()
Here is an example to reproduce the bug:
var body: some View {
ZStack {
LinearGradient(colors: [.orange, .yellow, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.ignoresSafeArea()
Text("Some underlined text")
.underline()
.padding()
.background(.ultraThinMaterial)
}
}
Works fine:
var body: some View {
ZStack {
LinearGradient(colors: [.orange, .yellow, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.ignoresSafeArea()
Text("Some underlined text")
.underline()
.padding()
.background(.white)
}
}
var body: some View {
ZStack {
LinearGradient(colors: [.orange, .yellow, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.ignoresSafeArea()
Text("Some underlined text")
.underline()
.padding()
.background(
Rectangle() //Add this one line of code
.fill(.ultraThinMaterial)
)
}
}
Add this one to your code will fix your problem.
It's probably looks like bug. But you can use next modifier to achieve normal behavior
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
Implement it to your text view
Text("Some underlined text")
.underline()
.padding()
.background(Blur(style: .systemUltraThinMaterial))

How do I define a struct property to accept Linear or Angular gradients in SwiftUI?

I want to create a View that can accept a Linear or an angular gradient as a property. Although both gradients conform to ShapeStyle, I can't define the properties type with it.
Here's what the view looks like if it accepts just a LinearGradient:
struct NewView: View {
var fill: LinearGradient
init(fill: LinearGradient = LinearGradient(gradient: Gradient(colors: [Color.green, Color.blue]), startPoint: .top, endPoint: .bottom) {
self.fill = fill
}
var body: some View {
Circle()
.stroke(fill, style: StrokeStyle(lineWidth: 25))
}
}
Ideally it will accept either Gradient in the fill property without the need for any other properties.
I ended up using the AnyShapeStyle type with two init blocks like this:
struct NewView: View {
var fill: AnyShapeStyle
init(fill: LinearGradient = LinearGradient(gradient: Gradient(colors: [Color.green, Color.blue]), startPoint: .top, endPoint: .bottom) {
self.fill = AnyShapeStyle(fill)
}
init(fill: AngularGradient = AngularGradient(colors: [.green, .blue], center: .center) {
self.fill = AnyShapeStyle(fill)
}
var body: some View {
Circle()
.stroke(fill, style: StrokeStyle(lineWidth: 25))
}
}
Set ShapeStyle constraint.
struct NewView<Style: ShapeStyle>: View {
var fill: Style
init(fill: Style) {
self.fill = fill
}
var body: some View {
Circle()
.stroke(fill, style: StrokeStyle(lineWidth: 25))
}
}

Ignore safe area in safe area

I'm trying to use safeAreaInset(edge:alignment:spacing:content:), to create a floating view at the bottom. The green view should ignore the bottom safe area, but this isn't working.
Code:
struct ContentView: View {
var body: some View {
ScrollView {
LinearGradient(
colors: [.red, .blue],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
}
.safeAreaInset(edge: .bottom) {
Color.green
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
}
}
}
Current result:
How do I make the green view ignore the bottom safe area so it touches the bottom?
Once you spoke about keyboard you add another difficulty to your original question! I just made simple code with overlay method it can easily converted to ZStack method as well! You do which way you like! Also I just add some fun on green View just for show and need refactor for sure, but that is not the part of issue or question!
struct ContentView: View {
#State private var string: String = String()
var body: some View {
let myDivider = Spacer().overlay(Color.secondary.frame(width: 1, height: 40))
Color.clear
.ignoresSafeArea(.all)
.overlay(
ScrollView {
ZStack {
LinearGradient(gradient: .init(colors: [.red, .blue]), startPoint: .top, endPoint: .bottom)
.frame(height: 1000)
VStack {
Spacer()
TextField("Enter youer text here ...", text: $string)
Spacer()
}
}
}
)
.overlay(
Color.green
.overlay(HStack { Spacer(); Text("🙈").padding(20); myDivider; Text("🙉").padding(20); myDivider; Text("🙊").padding(); Spacer() }.font(Font.system(.largeTitle)))
.frame(height: 80)
, alignment: .bottom)
.ignoresSafeArea(.container)
}
}
Creating an overlay(alignment:content:) after ignoring the safe area works. A view such as Color.clear can be used as the base.
Code:
struct ContentView: View {
var body: some View {
ScrollView {
LinearGradient(
colors: [.red, .blue],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
}
.safeAreaInset(edge: .bottom) {
Color.clear
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
.overlay(
Color.green
)
}
}
}
Result:
And if you have content and want a background in the safe area:
Color.red
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
.background(
Color.green
)
Result:
Note: be careful with the 2nd version. If you use modifiers such as clipShape on the background, it can cause it to break again. Fix this by using .ignoresSafeArea(edges: .bottom) after you may clip or anything else which breaks this.

Text opacity gradient

I'm looking to make text to appear to fade out on the edge. Here is what I have so far:
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("Hello world!")
// .blendMode(.luminosity) // <- Here?
.overlay(
LinearGradient(colors: [.white.opacity(0), .white], startPoint: .leading, endPoint: .trailing)
.frame(width: 50)
// .blendMode(.exclusion) // <- Here?
.frame(maxWidth: .infinity, alignment: .trailing)
)
}
}
}
Producing:
You can see the white gradient over the right side of the text. This should act like an opacity filter. Where the white isn't over, the text is fully visible. Below the white, the text should be completely transparent. It should be a gradual transition by using the gradient.
I think this can be solved using blendMode(_:), but I'm not too sure. I don't know which mode to use, nor where to apply it.
The solution to this should work in light & dark mode (i.e. the text may be black or white). The actual background behind is not a constant color (such as red) so using a red gradient overlay is not what I'm looking for.
Just use .mask instead.
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("Hello world!")
/// for iOS 14 and below, replace { } with ( )
.mask {
/// no need for .opacity, just use .clear
LinearGradient(colors: [.white, .clear], startPoint: .leading, endPoint: .trailing)
}
}
}
}
Result:
With thanks to aheze's answer, it now works.
Just a small thing I'll add, to show the way to get the exact same results as in the question (because mask sort of inverts it):
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("Hello world!")
.mask(
HStack(spacing: 0) {
Color.white
LinearGradient(colors: [.white, .clear], startPoint: .leading, endPoint: .trailing)
.frame(width: 50)
}
)
}
}
}
Result:

ScrollView causes buggy buttons in SwiftUI

I've been trying to create a form in SwiftUI but because of the limitations using "Form()" I instead chose to create a ScrollView with a ForEach loop that contains a button. When I run the project and I try to click on the buttons it clicks the incorrect one, unless I scroll the view. I am new in SwiftUI and I have not been able to figure it out.
I tried making the ScrollView in different sizes and it doesn't seem to be related
struct DropDown: View {
var datos: [String]
var categoria: String
#State var titulo: String = "Seleccione"
#State var expand = false
var body: some View {
VStack {
Text(categoria).fontWeight(.heavy).foregroundColor(.white)
HStack {
Text(titulo).fontWeight(.light).foregroundColor(.white)
Image(systemName: expand ? "chevron.up" : "chevron.down").resizable().frame(width: 10, height: 6).foregroundColor(.white)
}.onTapGesture {
self.expand.toggle()
}
if expand {
ScrollView(showsIndicators: true) {
ForEach(0..<self.datos.count){ nombre in
Button(action: {
print(self.datos[nombre])
self.titulo = self.datos[nombre]
self.expand.toggle()
diccionarioDatos[self.categoria] = self.titulo
print(diccionarioDatos)
}) {
Text(self.datos[nombre]).foregroundColor(.white)
}
}
}
.frame(maxHeight: 150)
.fixedSize()
}
}
.padding()
.background(LinearGradient(gradient: .init(colors: [.blue, .green]), startPoint: .top, endPoint: .bottom))
.cornerRadius(20)
.animation(.spring())
}
}
I clicked on "2018" under "Modelo" and "2015" got selected for some reason
This is how the dropdown menu looks like
As I tested the observed behaviour is due to order of animatable properties. In your case moving rounding corners into background itself solves the problem.
So instead of
.background(
LinearGradient(gradient: .init(colors: [.blue, .green]), startPoint: .top, endPoint: .bottom)
)
.cornerRadius(20)
Use
.background(
LinearGradient(gradient: .init(colors: [.blue, .green]), startPoint: .top, endPoint: .bottom)
.cornerRadius(20)
)
Consider using .onTapGesture instead of action for the buttons.
Button(action: {}, label: {
Text(self.datos[nombre]).foregroundColor(.white)
}).onTapGesture{
print(self.datos[nombre])
self.titulo = self.datos[nombre]
self.expand.toggle()
diccionarioDatos[self.categoria] = self.titulo
print(diccionarioDatos)
}