changeset 101:57e5196feb08

Add LineChartTutorial, BarChartTutorial, implementing LineChart on Stock view
author Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com>
date Sun, 31 Jan 2021 18:16:26 +0100
parents f304bb0d8dee
children a17ccf20f4a3
files LazyBear.xcodeproj/project.pbxproj LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate lazybear/Jobs/ScalateChart.swift lazybear/LazyBearApp.swift lazybear/Network/IexApi.swift lazybear/Tests/BarChartTutorial.swift lazybear/Tests/LineChartTutorial.swift lazybear/Views/LineChart.swift lazybear/Views/LineChartShape.swift lazybear/Views/Price.swift lazybear/Views/Stock.swift
diffstat 11 files changed, 250 insertions(+), 35 deletions(-) [+]
line wrap: on
line diff
--- a/LazyBear.xcodeproj/project.pbxproj	Sat Jan 30 19:58:06 2021 +0100
+++ b/LazyBear.xcodeproj/project.pbxproj	Sun Jan 31 18:16:26 2021 +0100
@@ -10,6 +10,10 @@
 		95002580256D17D9008FFD28 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9500257F256D17D9008FFD28 /* StoreKit.framework */; };
 		95078FD125BF4E640004FA75 /* CloudKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95078FD025BF4E640004FA75 /* CloudKitManager.swift */; };
 		950B79F625B1CB7A00E5DB5B /* CompanyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950B79F525B1CB7A00E5DB5B /* CompanyList.swift */; };
+		9520F0AB25C7074D00692610 /* LineChartTutorial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9520F0AA25C7074D00692610 /* LineChartTutorial.swift */; };
+		9520F0AE25C7115100692610 /* BarChartTutorial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9520F0AD25C7115100692610 /* BarChartTutorial.swift */; };
+		9520F0B225C712D000692610 /* LineChartShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9520F0B125C712D000692610 /* LineChartShape.swift */; };
+		9520F0B525C7131300692610 /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9520F0B425C7131300692610 /* LineChart.swift */; };
 		9537923625BDF85D0001F82B /* LogoApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9537923525BDF85D0001F82B /* LogoApi.swift */; };
 		954D992525A2123B001F7F60 /* HistoricalPricesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954D992425A2123B001F7F60 /* HistoricalPricesModel.swift */; };
 		954DDF0425C456E800848A4B /* QuoteModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954DDF0325C456E800848A4B /* QuoteModel.swift */; };
@@ -27,7 +31,6 @@
 		95B04EB525212369000AD27F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B04EB425212369000AD27F /* ContentView.swift */; };
 		95B04EB72521236A000AD27F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95B04EB62521236A000AD27F /* Assets.xcassets */; };
 		95B395A525BDF42E009A7EB0 /* companies.json in Resources */ = {isa = PBXBuildFile; fileRef = 95B395A425BDF42E009A7EB0 /* companies.json */; };
-		95C28AB925BC46250033D16A /* ScalateChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C28AB825BC46250033D16A /* ScalateChart.swift */; };
 		95D1BF4925ADCF7700E5D063 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D1BF4825ADCF7700E5D063 /* Persistence.swift */; };
 		95E4118F25BEC35D00A9C23F /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 95E4118E25BEC35D00A9C23F /* SDWebImageSwiftUI */; };
 		95E4119225BEC56F00A9C23F /* SuperTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E4119125BEC56F00A9C23F /* SuperTitle.swift */; };
@@ -51,6 +54,10 @@
 		9500257F256D17D9008FFD28 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
 		95078FD025BF4E640004FA75 /* CloudKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CloudKitManager.swift; path = LazyBear/CloudKitManager.swift; sourceTree = SOURCE_ROOT; };
 		950B79F525B1CB7A00E5DB5B /* CompanyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyList.swift; sourceTree = "<group>"; };
+		9520F0AA25C7074D00692610 /* LineChartTutorial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LineChartTutorial.swift; path = lazybear/Tests/LineChartTutorial.swift; sourceTree = SOURCE_ROOT; };
+		9520F0AD25C7115100692610 /* BarChartTutorial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BarChartTutorial.swift; path = lazybear/Tests/BarChartTutorial.swift; sourceTree = SOURCE_ROOT; };
+		9520F0B125C712D000692610 /* LineChartShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LineChartShape.swift; path = lazybear/Views/LineChartShape.swift; sourceTree = SOURCE_ROOT; };
+		9520F0B425C7131300692610 /* LineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LineChart.swift; path = lazybear/Views/LineChart.swift; sourceTree = SOURCE_ROOT; };
 		9537923525BDF85D0001F82B /* LogoApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LogoApi.swift; path = LazyBear/Network/LogoApi.swift; sourceTree = SOURCE_ROOT; };
 		954D992425A2123B001F7F60 /* HistoricalPricesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HistoricalPricesModel.swift; path = lazybear/Models/HistoricalPricesModel.swift; sourceTree = SOURCE_ROOT; };
 		954DDF0325C456E800848A4B /* QuoteModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QuoteModel.swift; path = lazybear/Models/QuoteModel.swift; sourceTree = SOURCE_ROOT; };
@@ -71,7 +78,6 @@
 		95B04EB62521236A000AD27F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		95B04EBB2521236A000AD27F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		95B395A425BDF42E009A7EB0 /* companies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = companies.json; path = lazybear/Data/companies.json; sourceTree = SOURCE_ROOT; };
-		95C28AB825BC46250033D16A /* ScalateChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScalateChart.swift; path = LazyBear/Jobs/ScalateChart.swift; sourceTree = SOURCE_ROOT; };
 		95D1BF4825ADCF7700E5D063 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Persistence.swift; path = LazyBear/Persistence.swift; sourceTree = SOURCE_ROOT; };
 		95E4119125BEC56F00A9C23F /* SuperTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuperTitle.swift; sourceTree = "<group>"; };
 		95E411A625BEE03000A9C23F /* Watchlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Watchlist.swift; sourceTree = "<group>"; };
@@ -115,6 +121,8 @@
 		952F791C2598B1CD00FF929F /* Tests */ = {
 			isa = PBXGroup;
 			children = (
+				9520F0AA25C7074D00692610 /* LineChartTutorial.swift */,
+				9520F0AD25C7115100692610 /* BarChartTutorial.swift */,
 			);
 			path = Tests;
 			sourceTree = "<group>";
@@ -149,6 +157,8 @@
 				95AD892325C5D8A200BCE8E4 /* AddWatchlist.swift */,
 				95F6C30425BAF599003CF389 /* CompanyHeader.swift */,
 				95F6C30825BAF7C2003CF389 /* DateSelection.swift */,
+				9520F0B125C712D000692610 /* LineChartShape.swift */,
+				9520F0B425C7131300692610 /* LineChart.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -157,7 +167,6 @@
 			isa = PBXGroup;
 			children = (
 				95AB4A79259DCBAE0064C9C1 /* ReadJson.swift */,
-				95C28AB825BC46250033D16A /* ScalateChart.swift */,
 			);
 			path = Jobs;
 			sourceTree = "<group>";
@@ -300,10 +309,13 @@
 				95F6F45C25C20D8D002AC66A /* Price.swift in Sources */,
 				9597CE0125C1DC0A004DDFED /* LogoModifier.swift in Sources */,
 				954DDF0425C456E800848A4B /* QuoteModel.swift in Sources */,
+				9520F0B525C7131300692610 /* LineChart.swift in Sources */,
 				9597CE0425C1DFE7004DDFED /* LogoPlaceholder.swift in Sources */,
 				95FE646B25C30B880052832E /* ApiModel.swift in Sources */,
 				95F6C30525BAF599003CF389 /* CompanyHeader.swift in Sources */,
 				95612C512598D48200F7698F /* SearchBar.swift in Sources */,
+				9520F0AE25C7115100692610 /* BarChartTutorial.swift in Sources */,
+				9520F0AB25C7074D00692610 /* LineChartTutorial.swift in Sources */,
 				95E411BE25BEEA6C00A9C23F /* WatchlistRow.swift in Sources */,
 				95AD892425C5D8A200BCE8E4 /* AddWatchlist.swift in Sources */,
 				950B79F625B1CB7A00E5DB5B /* CompanyList.swift in Sources */,
@@ -321,8 +333,8 @@
 				95AB4A7D259DCC0C0064C9C1 /* CompanyModel.swift in Sources */,
 				95700BC625BD9D12009CEEFE /* IexApi.swift in Sources */,
 				9537923625BDF85D0001F82B /* LogoApi.swift in Sources */,
-				95C28AB925BC46250033D16A /* ScalateChart.swift in Sources */,
 				95E411B625BEE84E00A9C23F /* Stock.swift in Sources */,
+				9520F0B225C712D000692610 /* LineChartShape.swift in Sources */,
 				954D992525A2123B001F7F60 /* HistoricalPricesModel.swift in Sources */,
 				95FE646725C2DC580052832E /* WatchlistCompany+CoreDataClass.swift in Sources */,
 				95E411A725BEE03000A9C23F /* Watchlist.swift in Sources */,
Binary file LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed
--- a/lazybear/Jobs/ScalateChart.swift	Sat Jan 30 19:58:06 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-//
-//  ScalateChart.swift
-//  LazyBear
-//
-//  Created by Dennis Concepción Martín on 23/1/21.
-//
-
-import SwiftUI
-
-func scalateChart(prices: [Double], selectedPeriod: Int) -> [Double] {
-    // Remove every two items to shorter the chart data points
-    var indexesToRemove = Set<Int>()
-
-    if selectedPeriod >= 4 {
-        for index in 0..<prices.count {
-            indexesToRemove.insert(index*2)
-        }
-    }
-    let prices = prices
-        .enumerated()
-        .filter { !indexesToRemove.contains($0.offset) }
-        .map { $0.element }
-    
-    return prices
-}
--- a/lazybear/LazyBearApp.swift	Sat Jan 30 19:58:06 2021 +0100
+++ b/lazybear/LazyBearApp.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -19,4 +19,14 @@
                 .environmentObject(apiAccess)  // Api info (url and token)
         }
     }
+    
+    // Line chart tutorial
+    func randomSample() -> [Double] {
+        var randomArray = [Double]()
+        for _ in 0..<100 {
+            randomArray.append(Double.random(in: 1...100))
+        }
+        
+        return randomArray
+    }
 }
--- a/lazybear/Network/IexApi.swift	Sat Jan 30 19:58:06 2021 +0100
+++ b/lazybear/Network/IexApi.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -59,7 +59,7 @@
         var path: String {
             switch self {
             case .chartCloseOnly:
-                return "chartCloseOnly=true"
+                return "chartCloseOnly=true&"
             }
         }
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lazybear/Tests/BarChartTutorial.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -0,0 +1,41 @@
+//
+//  BarChartTutorial.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 31/1/21.
+//
+
+import SwiftUI
+
+struct DataPoint2: Identifiable {
+    let id: Int
+    let values: Double
+    let color: Color
+    let title: String
+    
+    init(value: Double, color: Color, title: String = "") {
+        self.id = Int.random(in: 1..<Int.max)
+        self.values = value
+        self.color = color
+        self.title = title
+    }
+    
+    init(id: Int, value: Double, color: Color, title: String = "") {
+        self.id = Int.random(in: 1..<Int.max)
+        self.values = value
+        self.color = color
+        self.title = title
+    }
+}
+
+struct BarChartTutorial: View {
+    var body: some View {
+        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
+    }
+}
+
+struct BarChartTutorial_Previews: PreviewProvider {
+    static var previews: some View {
+        BarChartTutorial()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lazybear/Tests/LineChartTutorial.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -0,0 +1,52 @@
+//
+//  LineChartTutorial.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 31/1/21.
+//
+
+import SwiftUI
+
+struct LineChartTutorial: View {
+    @State private var data = makeDataPoints()
+    
+    var body: some View {
+        LineChart(dataPoints: data, lineColor: .blue, lineWidth: 5, pointColor: .red, pointSize: 10)
+            .frame(width: 300, height: 200)
+            .onTapGesture {
+                data = Self.makeDataPoints()
+            }
+    }
+    
+    // Generate random data usefull for representation
+    static func makeDataPoints() -> [DataPoint] {
+        var isGoingUp = true
+        var currentValue = 50.0
+        
+        return (1...50).map { _ in
+            if isGoingUp {
+                currentValue += Double.random(in: 1...10)
+            } else {
+                currentValue -= Double.random(in: 1...10)
+            }
+            
+            if isGoingUp {
+                if Int.random(in: 0..<10) == 0 {
+                    isGoingUp.toggle()
+                }
+            } else {
+                if Int.random(in: 0..<7) == 0 {
+                    isGoingUp.toggle()
+                }
+            }
+            
+            return DataPoint(value: abs(currentValue))
+        }
+    }
+}
+
+struct LineChartTutorial_Previews: PreviewProvider {
+    static var previews: some View {
+        LineChartTutorial()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lazybear/Views/LineChart.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -0,0 +1,33 @@
+//
+//  LineChart.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 31/1/21.
+//
+
+import SwiftUI
+
+struct LineChart: View {
+    let dataPoints: [DataPoint]
+    var lineColor = Color.primary
+    var lineWidth: CGFloat = 2
+    
+    var pointColor = Color.primary
+    var pointSize: CGFloat = 5
+    
+    var body: some View {
+        ZStack {
+            if lineColor != .clear {
+                LineChartShape(dataPoints: dataPoints, pointSize: pointSize, drawingLines: true)
+                    .stroke(lineColor, lineWidth: lineWidth)
+            }
+            
+            /*
+            if lineColor != .clear {
+                LineChartShape(dataPoints: dataPoints, pointSize: pointSize, drawingLines: false)
+                    .fill(pointColor)
+            }
+             */
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lazybear/Views/LineChartShape.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -0,0 +1,61 @@
+//
+//  LineChartShape.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 31/1/21.
+//
+
+import SwiftUI
+
+struct DataPoint {
+    let value: Double
+}
+
+struct LineChartShape: Shape {
+    let dataPoints: [DataPoint]
+    let pointSize: CGFloat
+    let maxValue: Double
+    let drawingLines: Bool
+    
+    init(dataPoints: [DataPoint], pointSize: CGFloat, drawingLines: Bool) {
+        self.dataPoints = dataPoints
+        self.pointSize = pointSize
+        self.drawingLines = drawingLines
+        
+        let highestPoint = dataPoints.max { $0.value < $1.value }
+        maxValue = highestPoint?.value ?? 1
+    }
+    
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+        let drawRect = rect.insetBy(dx: pointSize, dy: pointSize)
+        
+        let xMultiplier = drawRect.width / CGFloat(dataPoints.count - 1)
+        let yMultiplier = drawRect.height / CGFloat(maxValue)
+        
+        for(index, dataPoint) in dataPoints.enumerated() {
+            var x = xMultiplier * CGFloat(index)
+            var y = yMultiplier * CGFloat(dataPoint.value)
+            
+            y = drawRect.height - y
+            
+            x += drawRect.minX
+            y += drawRect.minY
+            
+            if drawingLines {
+                if index == 0 {
+                    path.move(to: CGPoint(x: x, y: y))
+                } else {
+                    path.addLine(to: CGPoint(x: x, y: y))
+                }
+            } else {
+                x -= pointSize / 2
+                y -= pointSize / 2
+                
+                path.addEllipse(in: CGRect(x: x, y: y, width: pointSize, height: pointSize))
+            }
+        }
+        
+        return path
+    }
+}
--- a/lazybear/Views/Price.swift	Sat Jan 30 19:58:06 2021 +0100
+++ b/lazybear/Views/Price.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -12,12 +12,12 @@
     @State var symbol: String
     @State var showVertical: Bool
     
-    @State var url = String() { didSet { giveMePrices() }}
-    @State var showingView = false
+    @State private var url = String() { didSet { requestPrice() }}
+    @State private var showingView = false
     
     @State var latestPrice = Float()
     @State var changePercent = Double() { didSet { self.showingView = true }}
-    @State var negativeChange = false
+    @State private var negativeChange = false
     
     let iexApi = IexApi()  // Request api function
     let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()  // Set recurrent price request
@@ -46,14 +46,16 @@
         .onDisappear { self.timer.upstream.connect().cancel() }  // Stop timer
     }
     
-    private func getUrl() {
+     private func getUrl() {
         let baseUrl = apiAccess.results[1].url ?? ""  // 1 -> Sandbox / 2 -> Production
         let token = apiAccess.results[1].key ?? ""
         let path = iexApi.getPath(version: .stable, stock: .symbol(company: symbol), endpoint: .quote, range: nil, parameters: nil)
+        
         self.url = baseUrl + path + token
+
     }
     
-    private func giveMePrices() {
+    private func requestPrice() {
         iexApi.request(url: url, model: QuoteModel.self) { result in
             self.latestPrice = result.latestPrice
             if self.changePercent >= 0 { self.negativeChange = true }
--- a/lazybear/Views/Stock.swift	Sat Jan 30 19:58:06 2021 +0100
+++ b/lazybear/Views/Stock.swift	Sun Jan 31 18:16:26 2021 +0100
@@ -11,8 +11,17 @@
     var name: String
     var symbol: String
     
+    var period = ["1W", "1M", "3M", "6M", "1Y", "2Y", "5Y"]
     @State var selectedPeriod = 2
     
+    @State private var url = String() { didSet { requestHistorical() }}
+    @State private var data = [HistoricalPricesModel]() { didSet { self.showingLineChart = true }}
+    @State private var showingLineChart = false
+    
+    let iexApi = IexApi()  // Request api function
+    
+    @EnvironmentObject var apiAccess: ApiAccess
+    
     @Environment(\.managedObjectContext) private var viewContext
     @FetchRequest(entity: WatchlistCompany.entity(), sortDescriptors: [])
     var companies: FetchedResults<WatchlistCompany>  // Fetch core data
@@ -32,8 +41,28 @@
             
             Divider()
             DateSelection(selectedperiod: $selectedPeriod)
+                .onChange(of: selectedPeriod, perform: { (value) in
+                    getUrl(range: period[selectedPeriod])
+                })
+            
+            let prices = data.map { DataPoint(value: $0.close) }
+            LineChart(dataPoints: prices, lineColor: .green, lineWidth: 2)
+                .frame(height: 400)
         }
         .padding([.leading, .trailing])
+        .onAppear { getUrl(range: period[selectedPeriod]) }
+    }
+    
+    private func getUrl(range: String) {
+        let baseUrl = apiAccess.results[1].url ?? ""  // 1 -> Sandbox / 2 -> Production
+        let token = apiAccess.results[1].key ?? ""
+        let path = iexApi.getPath(version: .stable, stock: .symbol(company: symbol), endpoint: .historicalPrices, range: .period(range: range), parameters: .chartCloseOnly)
+        
+        self.url = baseUrl + path + token
+   }
+    
+    private func requestHistorical() {
+        iexApi.request(url: url, model: [HistoricalPricesModel].self) { self.data = $0 }
     }
 }