SwiftUI - drawingGroup() changing colors - swiftui

Why are my colors changing/inverting when I apply the .drawingGroup() modifier to improve performance?
It almost seems like they are inverting. Going from Red to Black
I am not getting any error messages and code works as intended if I // (don't know the term, Hashing??) out the .drawingGroup() see screenshots at the end if unclear.
struct ColorCyclingCircle: View {
var amount = 0.0
var steps = 100
var body: some View {
ZStack {
ForEach(0..<steps) { value in
Circle()
.inset(by: CGFloat(value))
.strokeBorder(LinearGradient(gradient: Gradient(colors: [
self.color(for: value, brightness: 1),
self.color(for: value, brightness: 0.5)
]), startPoint: .top, endPoint: .bottom), lineWidth: 2)
}
}
.drawingGroup()
}
func color(for value: Int, brightness: Double) -> Color {
var targetHue = Double(value) / Double(self.steps) + self.amount
if targetHue > 1 {
targetHue -= 1
}
return Color(hue: targetHue, saturation: 1, brightness: brightness)
}
}

Related

SwiftUI: Animate layout changes when using Layout (iOS 16/macOS 13)

I'm using the new Layout protocol and try to animate the views when the layout changes. My layout it similar to this example and does not require any arguments from the parent view (so there are no states that are passed into it): https://swiftwithmajid.com/2022/11/16/building-custom-layout-in-swiftui-basics/
So far I was only able to do it by using the .animation modifier on Layout which triggers a deprecation warning:
MyLayout {
...
}
.animation(.default)
What is the correct way to do this? There seems to be nothing I can pass as value in the animation modifier to get rid of the deprecation warning. I also tried using withAnimation in placeSubviews but that does not work.
EDIT: Added link to example layout.
Here's the full code from Majid's tutorial + animation modifier:
struct ContentView: View {
var body: some View {
FlowLayout {
ForEach(0..<5) { _ in
Group {
Text("Hello")
.font(.largeTitle)
Text("World")
.font(.title)
Text("!!!")
.font(.title3)
}
.border(Color.red)
}
}
.animation(.default)
}
}
struct FlowLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
var totalHeight: CGFloat = 0
var totalWidth: CGFloat = 0
var lineWidth: CGFloat = 0
var lineHeight: CGFloat = 0
for size in sizes {
if lineWidth + size.width > proposal.width ?? 0 {
totalHeight += lineHeight
lineWidth = size.width
lineHeight = size.height
} else {
lineWidth += size.width
lineHeight = max(lineHeight, size.height)
}
totalWidth = max(totalWidth, lineWidth)
}
totalHeight += lineHeight
return .init(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
var lineX = bounds.minX
var lineY = bounds.minY
var lineHeight: CGFloat = 0
for index in subviews.indices {
if lineX + sizes[index].width > (proposal.width ?? 0) {
lineY += lineHeight
lineHeight = 0
lineX = bounds.minX
}
subviews[index].place(
at: .init(
x: lineX + sizes[index].width / 2,
y: lineY + sizes[index].height / 2
),
anchor: .center,
proposal: ProposedViewSize(sizes[index])
)
lineHeight = max(lineHeight, sizes[index].height)
lineX += sizes[index].width
}
}
}
So the animation is dependent on the content size, especially its width. You can get that with a GeometryReader and use its width as the animation value.
struct ContentView: View {
var body: some View {
GeometryReader { geo in
FlowLayout {
ForEach(0..<5) { _ in
Group {
Text("Hello")
.font(.largeTitle)
Text("World")
.font(.title)
Text("!!!")
.font(.title3)
}
.border(Color.red)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) // to balance the GeometryReader effect
.animation(.default, value: geo.size.width)
}
}
}

"CALayer position contains NaN: [nan nan]" error message caused by custom slider

I have a video player that starts playing a video (from firebase URL) and in some cases (70% of cases) i get this error message (exception) when running on physical device (no issues when launching in simulator though):
"CALayer position contains NaN: [nan nan]"
I found that the error doesn't appear when i comment "VideoPlayerControlsView()", so i'm pretty sure the problem is is my CustomerSlider object located insider of this VideoPlayerControlsView view.
I think it may somehow be caused by loading remote video, as the video is not loaded, the app doesn't know the size/bounds of AVPlayer object and therefore some parent view (maybe CustomerSlider) can't be created..
Building a Minimal Reproducible Example would be a nightmare, i just hope some can find a mistake in my code/logic.. If not - gonna build it of course. No other choice.
struct DetailedPlayerView : View {
// The progress through the video, as a percentage (from 0 to 1)
#State private var videoPos: Double = 0
// The duration of the video in seconds
#State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
#State private var seeking = false
private var player: AVPlayer = AVPlayer()
init(item: ExerciseItem, hVideoURL: URL?) {
if hVideoURL != nil {
player = AVPlayer(url: hVideoURL!)
player.isMuted = true
player.play()
} else {
print("[debug] hVideoURL is nil")
}
}
var body: some View {
ZStack {
//VStack {
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
//timeline: $timeline,
//videoTimeline: videoTimeline,
player: player)
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth)
VStack {
Spacer()
VideoPlayerControlsView(videoPos: $videoPos, **<<-----------------------**
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.frame(width: UIScreen.screenHeight - 2*Constants.scrollPadding, height: 20)
.padding(.bottom, 20)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
}
struct VideoPlayerControlsView : View {
#Binding private(set) var videoPos: Double
#Binding private(set) var videoDuration: Double
#Binding private(set) var seeking: Bool
// #Binding private(set) var timeline: [Advice]
#State var shouldStopPlayer: Bool = false
#State var player: AVPlayer
//let player: AVPlayer
#State private var playerPaused = false
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
.foregroundColor(Color.mainSubtitleColor)
.contentShape(Rectangle())
.padding(.trailing, 10)
}
// Current video time
if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
// Slider for seeking / showing video progress
CustomSlider(value: $videoPos, shouldStopPlayer: self.$shouldStopPlayer, range: (0, 1), knobWidth: 4) { modifiers in
ZStack {
Group {
Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5799999833106995))//Color((red: 0.4, green: 0.3, blue: 1)
.opacity(0.4)
.frame(height: 4)
.modifier(modifiers.barRight)
Color.mainSubtitleColor//Color(red: 0.4, green: 0.3, blue: 1)
.frame(height: 4)
.modifier(modifiers.barLeft)
}
.cornerRadius(5)
VStack {
Image(systemName: "arrowtriangle.down.fill") // SF Symbol
.foregroundColor(Color.mainSubtitleColor)
.offset(y: -3)
}
.frame(width: 20, height: 20)
.contentShape(Rectangle())
.modifier(modifiers.knob)
}
}
.onChange(of: shouldStopPlayer) { _ in
if shouldStopPlayer == false {
print("[debug] shouldStopPlayer == false")
sliderEditingChanged(editingStarted: false)
} else {
if seeking == false {
print("[debug] shouldStopPlayer == true")
sliderEditingChanged(editingStarted: true)
}
}
}
.frame(height: 20)
// Video duration
if videoDuration.isCanonical && videoDuration.isFinite {
Text(Utility.formatSecondsToHMS(videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
}
.padding(.leading, 40)
.padding(.trailing, 40)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}
extension Double {
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
// Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
}
struct CustomSliderComponents {
let barLeft: CustomSliderModifier
let barRight: CustomSliderModifier
let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
enum Name {
case barLeft
case barRight
case knob
}
let name: Name
let size: CGSize
let offset: CGFloat
func body(content: Content) -> some View {
content
.frame(width: (size.width >= 0) ? size.width : 0)
.position(x: size.width*0.5, y: size.height*0.5)
.offset(x: offset)
}
}
struct CustomSlider<Component: View>: View {
#Binding var value: Double
var range: (Double, Double)
var knobWidth: CGFloat?
let viewBuilder: (CustomSliderComponents) -> Component
#Binding var shouldStopPlayer: Bool
init(value: Binding<Double>, shouldStopPlayer: Binding<Bool>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: #escaping (CustomSliderComponents) -> Component
) {
_value = value
_shouldStopPlayer = shouldStopPlayer
self.range = range
self.viewBuilder = viewBuilder
self.knobWidth = knobWidth
}
var body: some View {
return GeometryReader { geometry in
self.view(geometry: geometry) // function below
}
}
private func view(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .global)
let drag = DragGesture(minimumDistance: 0)
.onChanged { drag in
shouldStopPlayer = true
self.onDragChange(drag, frame)
}
.onEnded { drag in
shouldStopPlayer = false
//self.updatedValue = value
print("[debug] slider drag gesture ended, value = \(value)")
}
let offsetX = self.getOffsetX(frame: frame)
let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5), height: frame.height)
let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
let modifiers = CustomSliderComponents(
barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
return ZStack { viewBuilder(modifiers).gesture(drag) }
}
private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
let xrange = (min: Double(0), max: Double(width.view - width.knob))
var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
value -= 0.5*width.knob // offset from center to leading edge of knob
value = value > xrange.max ? xrange.max : value // limit to leading edge
value = value < xrange.min ? xrange.min : value // limit to trailing edge
value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
//print("[debug] slider drag gesture detected, value = \(value)")
self.value = value
}
private func getOffsetX(frame: CGRect) -> CGFloat {
let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
let xrange: (Double, Double) = (0, Double(width.view - width.knob))
let result = self.value.convert(fromRange: range, toRange: xrange)
return CGFloat(result)
}
}
some extra code showing how DetailedPlayerView is triggered:
struct DetailedVideo: View {
var item: ExerciseItem
var url: URL
#Binding var isPaused: Bool
var body: some View {
ZStack {
DetailedPlayerView(item: self.item, hVideoURL: url)
//.frame(width: 500, height: 500) //##UPDATED: Apr 10
HStack {
VStack {
ZStack {
//Rectangle 126
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-135))
//Rectangle 125
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-45))
}
.frame(width: 35, height: 35)//14.4
.contentShape(Rectangle())
.onTapGesture {
print("[debugUI] isPaused = false")
self.isPaused = false
}
.offset(x:20, y:20)
Spacer()
}
Spacer()
}
}
.ignoresSafeArea(.all)
}
}
#ViewBuilder
var detailedVideoView: some View {
if self.hVideoURL != nil {
DetailedVideo(item: self.exerciseVM.exerciseItems[self.exerciseVM.currentIndex], url: self.hVideoURL!, isPaused: self.$exerciseVM.isPaused) // when is paused - we are playing detailed video?
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth) //UPDATED: Apr 9, 2021
.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
} else {
EmptyView()
}
}

Positioning Views in ForEach SwiftUI

I would like to add animating views to a parent view. I know that the parent view needs to position the children but I'm having trouble coming up with the formula to implement. I have the first couple of views right but once I get to 4 and up its a problem! I would like the views to appear in a grid with 3 columns.
Here is some reproducible code ready to be copy and pasted.
import SwiftUI
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
.frame(width: 50, height: 50)
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
#State private var userSelection: [CustomView] = []
var body: some View {
VStack(spacing: -20) {
Button("Add View") {
self.userSelection.append(CustomView())
}
LazyVGrid(columns: gridStyle) {
ForEach(Array(userSelection.enumerated()), id: \.0 ){ index, equip in
CustomView()
.position(x: widthBasedOn(index: index), y: heightBasedOn(index: index))
}
.padding([])
}
.frame(width: UIScreen.main.bounds.width * 0.5,
height: UIScreen.main.bounds.height * 0.8)
}
}
let gridStyle = [
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50)
]
private func widthBasedOn(index: Int) -> CGFloat {
if index % 3 != 0 {
if index > 3 {
let difference = index - 4
return CGFloat(index * difference * 100)
}
let answer = CGFloat(index * 100)
print("\(index) width should be: \(answer)")
return answer
}
return 0
}
private func heightBasedOn(index: Int) -> CGFloat {
if index > 3 && index < 6 {
return 100
}
return 200
}
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Since most of your question is somewhat vague, and I am not sure about the specifics, this is my solution. Feel free to respond, and I will be glad to answer your question further with more tailored solution.
I removed many of your code that was unnecessary or overly-complicated. For example, I removed the widthBasedOn and heightBasedOn methods. I also changed the array property var userSelection: [CustomView] to var numberOfViews = 0.
Note: Both your original code and my solution cause all the circles to wiggle up and down, whenever a new circle is added.
I suggest that you copy paste this code snippet, run it in Xcode, and see if this is what you want.
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
//Changing the frame size of the circle, making it bigger or smaller
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
//View will display this number of circles
#State private var numberOfViews = 0
var body: some View {
VStack() {
Button("Add View") {
self.numberOfViews += 1
}
.padding(.top, 100)
Spacer()
LazyVGrid(columns: gridStyle) {
//Add a new circle CustomView() to the LazyVGrid for each number of views
ForEach(0..<numberOfViews, id: \.self ){view in
CustomView()
}
}
}
}
//3 columns, flexible spacing for elments. In this case, equal amount of spacing.
let gridStyle = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
]
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Limiting number of circles
To limit the number of circles:
if numberOfViews < 9 {
self.numberOfViews += 1
}
Positioning the button
To position the button, you can add padding:
Button("Add View") {
if numberOfViews < 9 {
self.numberOfViews += 1
}
}
.padding(.top, 100)
Overlap vs. No Overlap
Using there .frame modifier will not have any overlap:
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
But if you do want overlap, use .scaleEffect:
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
P.S. Unfortunately, I can't show you the results with GIF images because Stackoverflow keep giving me upload errors.

How can i rotate an image in SwiftUI with one finger only?

I have an image in a view and i want to be able to rotate it with one finger only, how can i do that in SwiftUI?
I checked the RotationGesture but it works only with two fingers...
Thanks
Ok, i got it with this code :
https://gist.github.com/ts95/9f8e05380824c6ca999ab3bc1ff8541f
Ok fixed it with this code :
struct RotationGesture: View {
#State var totalRotation = CGSize.zero
#State var currentRotation = CGSize.zero
var body: some View {
Text("Hello, World!")
.frame(width: 150, height: 60)
.padding()
.background(Color.orange)
.cornerRadius(15)
.rotationEffect(Angle(degrees: Double(-totalRotation.width)))
.gesture(
DragGesture()
.onChanged { value in
totalRotation.width = value.translation.width + currentRotation.width
}
.onEnded { value in
currentRotation = totalRotation
}
)
}
}
But now we have to fixed the vertical movement because this solution is only working when you move around the X axis...
I want to solution to work when you make circle movement around the view you want to rotate...
one finger rotation
struct RotationGesture: View {
#State var gestureValue = CGSize.zero
var body: some View {
Text("Hello, World!")
.frame(width: 150, height: 60)
.padding()
.background(Color.orange)
.cornerRadius(15)
.rotationEffect(Angle(degrees: Double(-gestureValue.width)))
.gesture(
DragGesture().onChanged({ (value) in
self.gestureValue = value.translation
}).onEnded({ (value) in
self.gestureValue = .zero
})
)
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
}
}
Using the solutions from above this is what I have for rotating with one finger. This will keep the position when you finish rotating as well as starting the next rotation where the last one left off.
struct SwiftUIView: View {
#State private var rotation: Angle = Angle(degrees: 0)
#State private var previousRotation: Angle?
var body: some View {
Circle()
.fill(AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, angle: .degrees(90)))
.frame(width: 760, height: 760)
.rotationEffect(rotation, anchor: .center)
.gesture(DragGesture()
.onChanged{ value in
if let previousRotation = self.previousRotation {
let deltaY = value.location.y - (760 / 2)
let deltaX = value.location.x - (760 / 2)
let fingerAngle = Angle(radians: Double(atan2(deltaY, deltaX)))
let angle = fingerAngle - previousRotation
rotation += angle
self.previousRotation = fingerAngle
} else {
let deltaY = value.location.y - (760 / 2)
let deltaX = value.location.x - (760 / 2)
let fingerAngle = Angle(radians: Double(atan2(deltaY, deltaX)))
previousRotation = fingerAngle
}
}
.onEnded{ _ in
previousRotation = nil
})
}
}
A dragGesture will give us the value of a location where user has dragged on screen (in our case, the circle). Here, the circle frame will be equal height and equal width.
Capturing the location point where the user tapped and subtracting height/2 from y point and width/2 from x point results in deltaX and deltaY. Once we have deltaX and deltaY, we can convert it into radians using the atan2 function (which is provided by the Swift Standard Library).
struct SwiftUIView: View {
#State var angle: Angle = .zero
var circleFrame: CGRect = CGRect(x: 0, y: 0, width: 300, height: 300)
var body: some View {
Circle()
.fill(AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, angle: .degrees(90)))
.padding()
.rotationEffect(angle)
.gesture(
DragGesture()
.onChanged { value in
let deltaY = value.location.y - (circleFrame.height / 2)
let deltaX = value.location.x - (circleFrame.width / 2)
angle = Angle(radians: Double(atan2(deltaY, deltaX)))
}
)
}
}

Animating gradient fill in swiftUI

Having rectangle with gradient fill and trying to animate color change.
...
Rectangle()
.fill(LinearGradient(
gradient: .init(stops: [Gradient.Stop(color: myColor, location: 0.0), Gradient.Stop(color: .blue, location: 1.0)]),
startPoint: .init(x: 0.5, y: startPoint),
endPoint: .init(x: 0.5, y: 1-startPoint)
))
.animation(Animation.easeIn(duration: 2))
...
While change of starting points is animated nicely; the color change is not animated at all and just "switches"
Any hints?
So if anyone will be struggling with that my current solution is having two gradients in ZStack and animating opacity of the top one
Example gif
Here is rough example, the code sure can be written more nicely:
///helper extension to get some random Color
extension Color {
static func random()->Color {
let r = Double.random(in: 0 ... 1)
let g = Double.random(in: 0 ... 1)
let b = Double.random(in: 0 ... 1)
return Color(red: r, green: g, blue: b)
}
}
struct AnimatableGradientView: View {
#State private var gradientA: [Color] = [.white, .red]
#State private var gradientB: [Color] = [.white, .blue]
#State private var firstPlane: Bool = true
func setGradient(gradient: [Color]) {
if firstPlane {
gradientB = gradient
}
else {
gradientA = gradient
}
firstPlane = !firstPlane
}
var body: some View {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: self.gradientA), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: self.gradientB), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
.opacity(self.firstPlane ? 0 : 1)
///this button just demonstrates the solution
Button(action:{
withAnimation(.spring()) {
self.setGradient(gradient: [Color.random(), Color.random()])
}
})
{
Text("Change gradient")
}
}
}
}
Update: *In the end I have explored several ways of animating gradient fills and summarized them here: https://izakpavel.github.io/development/2019/09/30/animating-gradients-swiftui.html *
Wanted to share one more simple option.
Two rectangles with animated solid colors on top of each other.
The top ZStack uses a gradient mask to blend them together.
struct ContentView: View {
#State var switchColor = false
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(switchColor ? Color.purple : Color.green)
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(switchColor ? Color.pink : Color.yellow)
}
.mask(LinearGradient(gradient: Gradient(colors: [.clear,.black]), startPoint: .top, endPoint: .bottom))
}
.padding()
.onTapGesture {
withAnimation(.spring()) {
switchColor.toggle()
}
}
}
}