SwiftUI将底部渐变添加到折线图

9njqaruj  于 2022-12-17  发布在  Swift
关注(0)|答案(2)|浏览(145)

我想在我的线图中添加一个底部渐变,就像图片中一样。我已经画出了线条,但我不知道如何实际添加渐变。我可以在UIKit中完成,但不知道如何在SwiftUI中复制它。
SwiftUI线图视图:

var endingBalanceChart: some View {
    GeometryReader { geometry in
        Path { path in
            for index in viewModel.endingBalance.indices {
                let xPosition: CGFloat = geometry.size.width / CGFloat(viewModel.endingBalance.count) * CGFloat(index + 1)
                
                let yAxis: CGFloat = maxY - minY
                
                let maxYPosition: CGFloat = (1 - CGFloat((Double(viewModel.endingBalance[index].y) - minY) / yAxis)) * geometry.size.height
                let yPosition: CGFloat = index == 0 ? 200 : maxYPosition

                if index == 0 {
                    path.move(to: CGPoint(x: 0, y: yPosition))
                }
                
                path.addLine(to: CGPoint(x: xPosition, y: yPosition))
            }
        }
        .stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round))
    }
}

我尝试将路径转换为UIBezierPath,并尝试UIKit实现,但没有成功
这是我的UIKit实现:

func addGradient(path: UIBezierPath, hexString: String){
        let color = UIColor(hexString: hexString).withAlphaComponent(0.4).cgColor

        guard let clippingPath = path.copy() as? UIBezierPath else { return }
        clippingPath.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
        clippingPath.addLine(to: CGPoint(x: 0, y: bounds.height))
        clippingPath.close()
        clippingPath.addClip()

        let colors = [color, UIColor.clear.cgColor]
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let colorLocations: [CGFloat] = [0.0, 1.0]
        guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations) else { return }

        guard let context = UIGraphicsGetCurrentContext() else { return }
        let startPoint = CGPoint(x: 1, y: 1)
        let endPoint = CGPoint(x: 1, y: bounds.maxY)

        context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
    }
vvppvyoh

vvppvyoh1#

这是一个纯粹的SwiftUI实现。
我建议把var转换成一个自己的struct,符合Shape协议。然后你可以用它来描边和填充背景。
这有一个积极的副作用,即您不再需要GeometryReader,因为Shape为您提供了使用func path(in rect: CGRect)绘制矩形。
结果如下所示:

let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data

struct ContentView: View {
    
    var body: some View {
        VStack {
            Text("Chart")
            
            EndingBalanceChart()
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
                .background(
                    EndingBalanceChart(isBackground: true)
                        .fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
                    )
                .frame(height: 200)
                .padding()
        }
    }
}

struct EndingBalanceChart: Shape { // chnaged var to a Shape struct
    
    var isBackground: Bool = false
    
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            for index in endingBalance.indices {
                let xPosition: CGFloat = rect.width / CGFloat(endingBalance.count) * CGFloat(index + 1)
                
                let maxY = endingBalance.max() ?? 0
                let minY = endingBalance.min() ?? 0
                
                let yAxis: CGFloat = maxY - minY
                
                let yPosition: CGFloat = (1 - CGFloat((Double(endingBalance[index]) - minY) / yAxis)) * rect.height
                
                if index == 0 {
                    path.move(to: CGPoint(x: 0, y: rect.height))
                }
                path.addLine(to: CGPoint(x: xPosition, y: yPosition))
            }
            if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
                path.addLine(to: CGPoint(x: rect.width, y: rect.height))
            }
        }
    }
}
erhoui1w

erhoui1w2#

以下是一种方法:

let amountPaid: [Double] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // dummy data
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data

struct ContentView: View {
    
    @State private var value: Double = 0
    
    var body: some View {
        VStack {
            Text("Chart")
            
            CurveChart(data: endingBalance)
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
                .background(
                    CurveChart(data: endingBalance, isBackground: true)
                        .fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
                    )
                .frame(height: 200)
                .overlay(alignment: .topTrailing, content: {
                    plotPoint(data: endingBalance, index: Int(value))
                        .fill(.blue)
                })

            CurveChart(data: amountPaid)
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
                .background(
                    CurveChart(data: amountPaid, isBackground: true)
                        .fill(.linearGradient(colors: [.green, .clear], startPoint: .top, endPoint: .bottom)) // background fill
                    )
                .frame(height: 60)
                .overlay(alignment: .topTrailing, content: {
                    plotPoint(data: amountPaid, index: Int(value))
                        .fill(.green)
                })

                .padding(.bottom)
            
            Slider(value: $value, in: 0...Double(endingBalance.count-1), step: 1.0)
            
        }
        .padding()
    }
}

struct CurveChart: Shape { // chnaged var to a Shape struct
    
    let data: [Double]
    var isBackground: Bool = false
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            for index in data.indices {
                let xPosition: CGFloat = rect.width / CGFloat(data.count-1) * CGFloat(index)
                
                let maxY = data.max() ?? 0
                let minY = data.min() ?? 0
                
                let yAxis: CGFloat = maxY - minY
                
                let yPosition: CGFloat = (1 - CGFloat((Double(data[index]) - minY) / yAxis)) * rect.height
                
                if index == 0 {
                    path.move(to: CGPoint(x: 0, y: rect.height))
                }
                path.addLine(to: CGPoint(x: xPosition, y: yPosition))
            }
            if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
                path.addLine(to: CGPoint(x: rect.width, y: rect.height))
            }
        }
    }
}

struct plotPoint: Shape { // chnaged var to a Shape struct
    
    let data: [Double]
    let index: Int
    let size = 20.0
    
    func path(in rect: CGRect) -> Path {
        
        let xStep = rect.width / Double(data.count-1)
        let yStep = rect.height / (data.max() ?? 0)
        
        let xCenter = Double(index) * xStep
        let yCenter = rect.height - yStep * data[index]
        
        var path = Path()
        path.addEllipse(in: CGRect(x: xCenter - size/2, y: yCenter - size/2, width: size, height: size))
        
        return path
    }
}

相关问题