SwiftUI animate opacity in gradient mask - swift

I want to animate a mountain line chart (see screenshot) so that the line appears smoothly from left to right. I tried animating the endPoint of a LinearGradient in a mask. This works as it should, but leaves the right end of the line with an opacity value < 1. I also tried animating the opacity value itself, but that doesn't animate anything. This is the View:
import SwiftUI
struct ContentView: View {
private var datapoints: [CGFloat] = [100, 250, 200, 220, 200, 250, 350, 300, 325, 250]
#State private var pct: Double = 0.0
var body: some View {
GeometryReader { geometry in
ZStack {
Path() { path in
var x = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
}
.stroke()
Path() { path in
var x: Double = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height))
}
.fill(LinearGradient(colors: [.black.opacity(0.25), .black.opacity(0)], startPoint: .top, endPoint: .bottom))
}
.mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(pct), location: 1)], startPoint: .leading, endPoint: .trailing))
.onAppear() {
withAnimation(.linear(duration: 3)) {
pct = 1.0
}
}
// .mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(0), location: 1)], startPoint: .leading, endPoint: pct == 1 ? .trailing : .leading))
// .onAppear() {
// withAnimation(.linear(duration: 3)) {
// pct = 1.0
// }
// }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is the result if uncomment the commented out lines:

Related

Custom Menu/Tab bar in SwiftUI

I want to make tab bar like this and do not know where to start. Please help me I'm a newbie!
It navigates to different views.
Here's a pretty functional version. This could be made better to further mirror SwiftUI's TabBar interface. Let me know if you run into any issues with this.
(full res)
protocol TopTab: RawRepresentable, Identifiable, CaseIterable, Hashable where AllCases == Array<Self> {
var title: String { get }
}
struct TabBackground: Shape {
var cornerRadius: CGFloat = 8
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: .zero + cornerRadius, y: .zero))
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: .zero))
path.addQuadCurve(to: CGPoint(x: rect.width, y: .zero + cornerRadius), control: CGPoint(x: rect.width, y: .zero))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - cornerRadius))
path.addQuadCurve(to: CGPoint(x: rect.width + cornerRadius, y: rect.height), control: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: .zero - cornerRadius, y: rect.height))
path.addQuadCurve(to: CGPoint(x: .zero, y: rect.height - cornerRadius), control: CGPoint(x: .zero, y: rect.height))
path.addLine(to: CGPoint(x: .zero, y: cornerRadius))
path.addQuadCurve(to: CGPoint(x: .zero + cornerRadius, y: .zero), control: .zero)
return path
}
}
struct TopScrollingTabBarPicker<Tab: TopTab>: View {
#Binding var selection: Tab
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(Tab.allCases) { tab in
Group {
if tab == selection {
Text(tab.title)
.bold()
.foregroundColor(.accentColor)
.padding(12)
.background {
TabBackground(cornerRadius: 16)
.fill(Color.white)
}
} else {
Text(tab.title)
.padding(4)
.foregroundColor(.white)
}
}
.tag(tab)
.onTapGesture {
selection = tab
}
}
}
.padding(.horizontal, 10)
}
.background(Color.accentColor)
}
}
struct TopScrollingTabBar<Tab: TopTab, Content: View>: View {
var alignment: HorizontalAlignment = .leading
#Binding var selection: Tab
#ViewBuilder var content: (_ tab: Tab) -> Content
var body: some View {
GeometryReader { geometry in
VStack(alignment: alignment) {
TopScrollingTabBarPicker(selection: $selection)
.frame(height: 44)
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
self.content(selection)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
}
}
Usage:
enum PreviewTab: String, TopTab {
case saved
case booked
case pending
case confirmed
case archived
var id: String {
rawValue
}
var title: String {
switch self {
case .saved:
return "Saved"
case .booked:
return "Booked"
case .pending:
return "Pending"
case .confirmed:
return "Confirmed"
case .archived:
return "Archived"
}
}
}
struct ContentView: View {
#State var selection: PreviewTab = .saved
var body: some View {
TopScrollingTabBar(selection: $selection.animation()) { tab in
// For demo purposes
List {
Text(tab.title)
.bold()
.font(.largeTitle)
Text("Something that is \(tab.title)")
}
.listStyle(.plain)
// In a full app I'd switch on the Tab enum and generate the appropriate view.
// switch tab {
// case .saved:
// SavedThings()
// case .booked:
// ...etc
// }
}
}
}
Hope this helps! <3

Path transparent only on macOS

Why are paths slightly transparent on macOS and not iOS? How can I make them not transparent on macOS?
Minimal reproducible example:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 150, y: 150))
path.addLine(to: CGPoint(x: 150, y: 300))
}
.stroke(lineWidth: 8)
Path { path in
path.move(to: CGPoint(x: 150, y: 200))
path.addLine(to: CGPoint(x: 150, y: 450))
}
.stroke(lineWidth: 8)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}

How can I clip a Shape with another Shape in SwiftUI?

I want to clip the red square with the green half-circle. I tried .mask and .clipShape but
i can't get this to work. Can someone share how its done please?
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color.red)
.frame(width: 500, height: 500)
Circle()
.trim(from: 0.5, to: 1.0)
.fill(Color.green)
.rotationEffect(Angle(degrees: 90))
.frame(width: 500, height: 500)
.offset(x: -250)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
with using eoFill like in the code:
struct ContentView: View {
var body: some View {
CustomShape()
.fill(Color.purple, style: FillStyle(eoFill: true, antialiased: true))
.frame(width: 200, height: 200, alignment: .center)
.shadow(radius: 10.0)
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: rect.height/2, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: true)
path.addRect(CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height))
}
}
}
Create a struct that conforms to Shape.
Implement func path(in rect: CGRect) -> Path
Create a Path. Add the rectangle, then the circle.
Fill the shape with isEOFilled set to true
My code is this basic one. Maybe someone finds this useful so i am attaching it here as well.
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
BoxCutOut()
.foregroundColor(Color.red)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
struct BoxCutOut: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x:0, y: 0))
path.addLine(to: CGPoint(x:rect.width, y:0))
path.addLine(to: CGPoint(x:rect.width, y:rect.height))
path.addLine(to: CGPoint(x:0, y:rect.height))
path.addArc(center: CGPoint(x: 0, y: rect.height/2), radius: rect.height/2, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true )
}
}
}

SwiftUI: Change the start position for animated progress bar (or Shape drawing in general)

The code below begins to draw a rectangle from the bottom left corner. How can I change it to begin to draw from the top middle? Hopefully there is a simple setting I am missing here.
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
Rectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.rotationEffect(Angle(degrees: 270.0))
.frame(width: 100, height:100)
Rectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
.frame(width: 100, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
You don't need to rotate it (moreover it might be not square), just create own rectangular shape with start point anywhere needed.
Demo prepared with Xcode 12.4 / iOS 14.4
struct MyRectangle: Shape {
func path(in rect: CGRect) -> Path {
Path {
$0.move(to: CGPoint(x: rect.midX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
}
}
}
struct ContentView: View {
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
MyRectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.frame(width: 200, height:100)
MyRectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.animation(.linear)
.frame(width: 200, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
}

Fill circle with wave animation in SwiftUI

I have created a circle in swiftUI, and I want to fill it with sine wave animation for the water wave effect/animation. I wanted to fill it with a similar look:
Below is my code:
import SwiftUI
struct CircleWaveView: View {
var body: some View {
Circle()
.stroke(Color.blue, lineWidth: 10)
.frame(width: 300, height: 300)
}
}
struct CircleWaveView_Previews: PreviewProvider {
static var previews: some View {
CircleWaveView()
}
}
I want to mostly implement it on SwiftUI so that I can support dark mode! Thanks for the help!
Here's a complete standalone example. It features a slider which allows you to change the percentage:
import SwiftUI
struct ContentView: View {
#State private var percent = 50.0
var body: some View {
VStack {
CircleWaveView(percent: Int(self.percent))
Slider(value: self.$percent, in: 0...100)
}
.padding(.all)
}
}
struct Wave: Shape {
var offset: Angle
var percent: Double
var animatableData: Double {
get { offset.degrees }
set { offset = Angle(degrees: newValue) }
}
func path(in rect: CGRect) -> Path {
var p = Path()
// empirically determined values for wave to be seen
// at 0 and 100 percent
let lowfudge = 0.02
let highfudge = 0.98
let newpercent = lowfudge + (highfudge - lowfudge) * percent
let waveHeight = 0.015 * rect.height
let yoffset = CGFloat(1 - newpercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
let startAngle = offset
let endAngle = offset + Angle(degrees: 360)
p.move(to: CGPoint(x: 0, y: yoffset + waveHeight * CGFloat(sin(offset.radians))))
for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
p.addLine(to: CGPoint(x: x, y: yoffset + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))))
}
p.addLine(to: CGPoint(x: rect.width, y: rect.height))
p.addLine(to: CGPoint(x: 0, y: rect.height))
p.closeSubpath()
return p
}
}
struct CircleWaveView: View {
#State private var waveOffset = Angle(degrees: 0)
let percent: Int
var body: some View {
GeometryReader { geo in
ZStack {
Text("\(self.percent)%")
.foregroundColor(.black)
.font(Font.system(size: 0.25 * min(geo.size.width, geo.size.height) ))
Circle()
.stroke(Color.blue, lineWidth: 0.025 * min(geo.size.width, geo.size.height))
.overlay(
Wave(offset: Angle(degrees: self.waveOffset.degrees), percent: Double(percent)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.clipShape(Circle().scale(0.92))
)
}
}
.aspectRatio(1, contentMode: .fit)
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.waveOffset = Angle(degrees: 360)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
CircleWaveView(percent: 58)
}
}