changeset 275:62f2c675b666

Interactive LineChart implemented
author Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com>
date Thu, 18 Mar 2021 17:23:21 +0100
parents 61208d7aa715
children cd9c72c3bc16
files LazyBear/Functions/Haptics.swift LazyBear/Functions/NormalizeData.swift LazyBear/UI/ChartView.swift LazyBear/UI/CompanyView.swift LazyBear/UI/IconPicker.swift LazyBear/UI/IndicatorPoint.swift LazyBear/UI/LanguagePicker.swift LazyBear/UI/LineChart.swift LazyBear/UI/LineView.swift LazyBear/UI/PriceChartIndicator.swift
diffstat 10 files changed, 160 insertions(+), 86 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LazyBear/Functions/Haptics.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -0,0 +1,20 @@
+//
+//  Haptics.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 18/3/21.
+//
+
+import SwiftUI
+
+struct Haptics: View {
+    var body: some View {
+        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
+    }
+}
+
+struct Haptics_Previews: PreviewProvider {
+    static var previews: some View {
+        Haptics()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LazyBear/Functions/NormalizeData.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -0,0 +1,21 @@
+//
+//  NormalizeData.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 18/3/21.
+//
+
+import SwiftUI
+
+func normalize(_ data: [Double]) -> [Double] {
+    var normalData = [Double]()
+    let min = data.min()!
+    let max = data.max()!
+
+    for value in data {
+        let normal = (value - min) / (max - min)
+        normalData.append(normal)
+    }
+
+    return normalData
+}
--- a/LazyBear/UI/ChartView.swift	Wed Mar 17 20:26:19 2021 +0100
+++ b/LazyBear/UI/ChartView.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -10,23 +10,43 @@
 struct ChartView: View {
     var symbol: String
     @State private var historicalPrices = [HistoricalPriceModel]()
+    @EnvironmentObject var deviceSize: DeviceSize
    
     // Date picker
     var period = ["1W", "1M", "3M", "6M", "1Y", "2Y", "5Y"]
     @State var selectedPeriod = 2
     
+    // Line View
+    @State var showingChartIndicator = false
+    @State var pointInPath: CGPoint = .zero
+    @State var indexValue = Int()
+    
     var body: some View {
+        
         VStack {
-            DateSelection(period: period, selectedperiod: $selectedPeriod)
-                .padding(.horizontal)
-                .onChange(of: selectedPeriod, perform: { (value) in
-                    let url = getUrl(endpoint: .historicalPrices, symbol: symbol, range: period[value])
-                    request(url: url, model: [HistoricalPriceModel].self) { self.historicalPrices = $0 }
-                })
+            let prices = historicalPrices.map { $0.close }
+            let dates = historicalPrices.map { $0.date }
+            ZStack {
+                if showingChartIndicator {
+                    PriceChartIndicator(prices: prices, dates: dates, indexValue: $indexValue)
+                } else {
+                    DateSelection(period: period, selectedperiod: $selectedPeriod)
+                        .padding(.horizontal)
+                        .onChange(of: selectedPeriod, perform: { (value) in
+                            let url = getUrl(endpoint: .historicalPrices, symbol: symbol, range: period[value])
+                            request(url: url, model: [HistoricalPriceModel].self) { self.historicalPrices = $0 }
+                        })
+                }
+            }
+            .frame(height: 40)
             
-            let prices = historicalPrices.map { $0.close }
-            LineChart(data: prices)
-                .padding(.vertical)
+            if !historicalPrices.isEmpty {
+                LineView(width: deviceSize.width, height: deviceSize.width/3, normalizedData: normalize(prices), showingChartIndicator: $showingChartIndicator, pointInPath: $pointInPath, indexValue: $indexValue)
+                    .frame(width: deviceSize.width, height: deviceSize.width / 3)
+                    .rotationEffect(.degrees(180), anchor: .center)
+                    .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0))
+                    .padding([.top, .bottom])
+            }
         }
         .onAppear {
             let url = getUrl(endpoint: .historicalPrices, symbol: symbol, range: "3m")
--- a/LazyBear/UI/CompanyView.swift	Wed Mar 17 20:26:19 2021 +0100
+++ b/LazyBear/UI/CompanyView.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -14,6 +14,7 @@
 struct CompanyView: View {
     var name: String
     var symbol: String
+    let haptics = Haptics()
     @EnvironmentObject var hudManager: HudManager
     @EnvironmentObject var companyOption: CompanyOption
     @Environment(\.managedObjectContext) private var moc
@@ -65,14 +66,13 @@
     
     // Add to watchlist
     private func addCompany() {
-        let generator = UINotificationFeedbackGenerator()  // Haptic
         let company = Company(context: moc)
         company.symbol = symbol
         company.name = name
         do {
             try moc.save()
             hudManager.selectHud(type: .notification)
-            generator.notificationOccurred(.success)
+            haptics.simpleSuccess()
             print("Company saved")
         } catch {
             print(error.localizedDescription)
--- a/LazyBear/UI/IconPicker.swift	Wed Mar 17 20:26:19 2021 +0100
+++ b/LazyBear/UI/IconPicker.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -33,9 +33,10 @@
 struct IconRow: View {
     @Environment(\.colorScheme) var colorScheme  // Detect dark mode
     var icon: IconModel
+    let haptics = Haptics()
     
     var body: some View {
-        Button(action: { changeIcon(key: icon.file) }) {
+        Button(action: { haptics.simpleSuccess(); changeIcon(key: icon.file) }) {
             HStack {
                 Image(icon.file)
                     .resizable()
--- a/LazyBear/UI/IndicatorPoint.swift	Wed Mar 17 20:26:19 2021 +0100
+++ b/LazyBear/UI/IndicatorPoint.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -10,7 +10,8 @@
 struct IndicatorPoint: View {
     var body: some View {
         Circle()
-            .frame(width: 10, height: 10)
+            .frame(width: 20, height: 20)
+            .foregroundColor(.blue)
     }
 }
 
--- a/LazyBear/UI/LanguagePicker.swift	Wed Mar 17 20:26:19 2021 +0100
+++ b/LazyBear/UI/LanguagePicker.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -10,6 +10,7 @@
 struct LanguagePicker: View {
     @Environment(\.managedObjectContext) private var moc
     @State var language: String
+    let haptics = Haptics()
     
     var body: some View {
         Picker("News language", selection: $language) {
@@ -29,6 +30,7 @@
         userSettings.newsLanguage = change as? String
         do {
             try moc.save()
+            haptics.simpleSuccess()
             print("Settings saved")
         } catch {
             print(error.localizedDescription)
--- a/LazyBear/UI/LineChart.swift	Wed Mar 17 20:26:19 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-//
-//  LineChart.swift
-//  LazyBear
-//
-//  Created by Dennis Concepción Martín on 15/3/21.
-//
-
-import SwiftUI
-
-struct LineChart: View {
-    var data: [Double]
-    @EnvironmentObject var deviceSize: DeviceSize
-    
-    var body: some View {
-        if !data.isEmpty {
-            let normalizedData = normalize(data)
-            
-            VStack {
-                LineView(width: deviceSize.width, height: deviceSize.width / 3, normalizedData: normalizedData)
-                    .rotationEffect(.degrees(180), anchor: .center) 
-                    .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0))
-                    
-            }
-            .frame(width: deviceSize.width, height: deviceSize.width / 3)
-        }
-    }
-    
-    func normalize(_ data: [Double]) -> [Double] {
-        var normalData = [Double]()
-        let min = data.min()!
-        let max = data.max()!
-
-        for value in data {
-            let normal = (value - min) / (max - min)
-            normalData.append(normal)
-        }
-
-        return normalData
-    }
-}
-
-struct LineChart_Previews: PreviewProvider {
-    static var previews: some View {
-        LineChart(data: [50.0, 50.5, 51.0, 50.4, 50.8, 51.3, 51.5, 52, 51.9, 52.4])
-            .environmentObject(DeviceSize())
-    }
-}
--- a/LazyBear/UI/LineView.swift	Wed Mar 17 20:26:19 2021 +0100
+++ b/LazyBear/UI/LineView.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -11,9 +11,12 @@
     var width: CGFloat
     var height: CGFloat
     var normalizedData: [Double]
+    let haptics = Haptics()
     
     // Drag gesture
-    @State private var touchLocation: CGPoint = .zero
+    @Binding var showingChartIndicator: Bool
+    @Binding var pointInPath: CGPoint
+    @Binding var indexValue: Int
 
     var body: some View {
         // Substract 2 to skip the first and the last item
@@ -24,38 +27,57 @@
         
         ZStack {
             GeometryReader { geo in
-            Path { path in
-                path.move(to: CGPoint(x: x, y: initialPoint))
-                for y in normalizedData {
-                    // Skip first item
-                    if normalizedData.firstIndex(of: y) != 0 {
-                        x += widthBetweenPoints
-                        let y = y * Double(height)
-                        path.addLine(to: CGPoint(x: x, y: y))
+                Path { path in
+                    path.move(to: CGPoint(x: x, y: initialPoint))
+                    for y in normalizedData {
+                        // Skip first item
+                        if normalizedData.firstIndex(of: y) != 0 {
+                            x += widthBetweenPoints
+                            let y = y * Double(height)
+                            path.addLine(to: CGPoint(x: x, y: y))
+                        }
+                        
+                        pathPoints.append(path.currentPoint!)
                     }
-                    
-                    pathPoints.append(path.currentPoint!)
                 }
-            }
-            .stroke(Color.green, lineWidth: 2)
-            .gesture(DragGesture()  // Add gesture
-            .onChanged({ value in  // Take value of the gesture
-                let (closestXPoint, closestYPoint) = getClosestValueFrom(value.location, inData: pathPoints)
-                self.touchLocation.x = closestXPoint
-                self.touchLocation.y = closestYPoint
+                .stroke(self.showingChartIndicator ? Color.blue: Color.green, lineWidth: 2)
                 
-            }))
-            
-            
-            IndicatorPoint()
-                .position(x: touchLocation.x, y: touchLocation.y)
+                if showingChartIndicator {
+                    IndicatorPoint()
+                        .position(x: pointInPath.x, y: pointInPath.y)
+                    }
             }
         }
+        .contentShape(Rectangle())  // Control tappable area
+        .gesture(
+            LongPressGesture(minimumDuration: 0.2)
+                .sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .local))
+        .onChanged({ value in  // Take value of the gesture
+            switch value {
+            // Start the second gesture -> Drag()
+            case .second(true, let drag):
+                if let longPressLocation = drag?.location {
+                    let (closestXPoint, closestYPoint, yPointIndex) = getClosestValueFrom(longPressLocation, inData: pathPoints)
+                    self.pointInPath.x = closestXPoint
+                    self.pointInPath.y = closestYPoint
+                    self.showingChartIndicator = true
+                    self.indexValue = yPointIndex
+                } else {
+                    haptics.simpleSuccess()
+                }
+            default:
+                break
+            }
+        })
+                .onEnded({ value in
+                    self.showingChartIndicator = false
+                })
+        )
     }
     
     // First search the closest X path point from the touch location. The find the correspondant Y path point
     // given the X path point
-    func getClosestValueFrom(_ value: CGPoint, inData: [CGPoint]) -> (CGFloat, CGFloat) {
+    func getClosestValueFrom(_ value: CGPoint, inData: [CGPoint]) -> (CGFloat, CGFloat, Int) {
         let touchPoint: (CGFloat, CGFloat) = (value.x, value.y)
         let xPathPoints = inData.map { $0.x }
         let yPathPoints = inData.map { $0.y }
@@ -65,7 +87,10 @@
         let closestYPointIndex = xPathPoints.firstIndex(of: closestXPoint.element)!
         let closestYPoint = yPathPoints[closestYPointIndex]
         
-        return (closestXPoint.element, closestYPoint)
+        // Index of the closest points in the array
+        let yPointIndex = yPathPoints.firstIndex(of: closestYPoint)!
+
+        return (closestXPoint.element, closestYPoint, yPointIndex)
     }
 }
 
@@ -74,7 +99,7 @@
         GeometryReader { geo in
             VStack {
                 let normalizedData: [Double] = [0, 0.1, 0.15, 0.2, 0.3, 0.4, 0.35, 0.38, 0.5, 0.55, 0.6, 0.57, 0.8, 1]
-                LineView(width: geo.size.width, height: geo.size.width / 2, normalizedData: normalizedData)
+                LineView(width: geo.size.width, height: geo.size.width / 2, normalizedData: normalizedData, showingChartIndicator: .constant(false), pointInPath: .constant(.zero), indexValue: .constant(0))
             }
         }
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LazyBear/UI/PriceChartIndicator.swift	Thu Mar 18 17:23:21 2021 +0100
@@ -0,0 +1,31 @@
+//
+//  PriceChartIndicator.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 18/3/21.
+//
+
+import SwiftUI
+
+struct PriceChartIndicator: View {
+    var prices: [Double]
+    var dates: [String]
+    @Binding var indexValue: Int
+    
+    var body: some View {
+        HStack {
+            Group {
+                Text(dates[indexValue])
+                Text("\(prices[indexValue], specifier: "%.2f")")
+                    .foregroundColor(.blue)
+            }
+            .font(.subheadline)
+        }
+    }
+}
+
+struct PriceChartIndicator_Previews: PreviewProvider {
+    static var previews: some View {
+        PriceChartIndicator(prices: [100, 50], dates: ["10-10-2020", "11-10-2020"], indexValue: .constant(0))
+    }
+}