# HG changeset patch # User Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com> # Date 1615835184 -3600 # Node ID 9e23e9b0ab3609786e39aabb53303c6ec34230fb # Parent e1610b54015d178ce956986e5fb6c2ac2ac79c77 Implementing Custom Line Chart diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear.xcodeproj/project.pbxproj --- a/LazyBear.xcodeproj/project.pbxproj Sun Mar 14 13:25:21 2021 +0100 +++ b/LazyBear.xcodeproj/project.pbxproj Mon Mar 15 20:06:24 2021 +0100 @@ -49,6 +49,9 @@ 95ACB5A925E0397B00A3CCC8 /* CompanyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ACB5A825E0397B00A3CCC8 /* CompanyView.swift */; }; 95ACB5AC25E03A7D00A3CCC8 /* themes.json in Resources */ = {isa = PBXBuildFile; fileRef = 95ACB5AB25E03A7D00A3CCC8 /* themes.json */; }; 95ACB5AF25E03AA100A3CCC8 /* ThemeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ACB5AE25E03AA100A3CCC8 /* ThemeModel.swift */; }; + 95AEF3AC25FFBB4D001B77BB /* LinePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEF3AB25FFBB4D001B77BB /* LinePath.swift */; }; + 95AEF3B025FFD8CF001B77BB /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEF3AF25FFD8CF001B77BB /* LineChart.swift */; }; + 95AEF3B325FFDC04001B77BB /* DeviceSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEF3B225FFDC04001B77BB /* DeviceSize.swift */; }; 95B3E09F25E127D7007EFDE3 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B3E09E25E127D7007EFDE3 /* Request.swift */; }; 95B3E0A625E1318D007EFDE3 /* SwiftlySearch in Frameworks */ = {isa = PBXBuildFile; productRef = 95B3E0A525E1318D007EFDE3 /* SwiftlySearch */; }; 95BB43C025EA667700B6C965 /* DateSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BB43BF25EA667700B6C965 /* DateSelection.swift */; }; @@ -56,7 +59,6 @@ 95BFAE4E25E2B0C200A70EC3 /* HudManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFAE4D25E2B0C200A70EC3 /* HudManager.swift */; }; 95BFAE5425E2C52300A70EC3 /* HistoricalPriceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFAE5325E2C52300A70EC3 /* HistoricalPriceModel.swift */; }; 95BFAE5825E2C5A700A70EC3 /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFAE5725E2C5A700A70EC3 /* ChartView.swift */; }; - 95D28EC625FD2BBE00FBE5F8 /* DemoChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D28EC525FD2BBE00FBE5F8 /* DemoChart.swift */; }; 95D34C2725EFD5FE006F4A81 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 95D34C2625EFD5FE006F4A81 /* SDWebImageSwiftUI */; }; 95DED9D525F2A752000DFCBA /* transactionCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DED9D425F2A752000DFCBA /* transactionCodes.swift */; }; 95DED9D825F2B1EF000DFCBA /* TopInsiderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DED9D725F2B1EF000DFCBA /* TopInsiderModel.swift */; }; @@ -120,13 +122,15 @@ 95ACB5A825E0397B00A3CCC8 /* CompanyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyView.swift; sourceTree = ""; }; 95ACB5AB25E03A7D00A3CCC8 /* themes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = themes.json; sourceTree = ""; }; 95ACB5AE25E03AA100A3CCC8 /* ThemeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModel.swift; sourceTree = ""; }; + 95AEF3AB25FFBB4D001B77BB /* LinePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinePath.swift; sourceTree = ""; }; + 95AEF3AF25FFD8CF001B77BB /* LineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChart.swift; sourceTree = ""; }; + 95AEF3B225FFDC04001B77BB /* DeviceSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSize.swift; sourceTree = ""; }; 95B3E09E25E127D7007EFDE3 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 95BB43BF25EA667700B6C965 /* DateSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSelection.swift; sourceTree = ""; }; 95BFAE4A25E2AEA000A70EC3 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 95BFAE4D25E2B0C200A70EC3 /* HudManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HudManager.swift; sourceTree = ""; }; 95BFAE5325E2C52300A70EC3 /* HistoricalPriceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoricalPriceModel.swift; sourceTree = ""; }; 95BFAE5725E2C5A700A70EC3 /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; - 95D28EC525FD2BBE00FBE5F8 /* DemoChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChart.swift; sourceTree = ""; }; 95DED9D425F2A752000DFCBA /* transactionCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = transactionCodes.swift; sourceTree = ""; }; 95DED9D725F2B1EF000DFCBA /* TopInsiderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopInsiderModel.swift; sourceTree = ""; }; 95DED9DA25F2B268000DFCBA /* InsiderSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsiderSummary.swift; sourceTree = ""; }; @@ -160,7 +164,6 @@ 950BA46D25E9450B00D065EF /* Tests */ = { isa = PBXGroup; children = ( - 95D28EC525FD2BBE00FBE5F8 /* DemoChart.swift */, ); path = Tests; sourceTree = ""; @@ -200,6 +203,7 @@ 95BFAE4D25E2B0C200A70EC3 /* HudManager.swift */, 95672B9725DDA54700DCBE4A /* Persistence.swift */, 95A5D95925FCEDDB0090C1EA /* CompanyOption.swift */, + 95AEF3B225FFDC04001B77BB /* DeviceSize.swift */, 95B1874925DDAC4D0068A364 /* UI */, 95B1874825DDAC470068A364 /* Models */, 958A735525E01F7E00FD7ECA /* Functions */, @@ -278,6 +282,8 @@ 9520C26E25F4D43D0070DD71 /* TransactionDetail.swift */, 95DED9DA25F2B268000DFCBA /* InsiderSummary.swift */, 95E9D09625F6AA0400A947A1 /* ActionView.swift */, + 95AEF3AB25FFBB4D001B77BB /* LinePath.swift */, + 95AEF3AF25FFD8CF001B77BB /* LineChart.swift */, ); path = UI; sourceTree = ""; @@ -392,7 +398,6 @@ buildActionMask = 2147483647; files = ( 95ABDD3825E167E500310776 /* NewsModel.swift in Sources */, - 95D28EC625FD2BBE00FBE5F8 /* DemoChart.swift in Sources */, 95ABDD3C25E1717300310776 /* NewsRow.swift in Sources */, 950B674925E99FA900BF8593 /* IconPicker.swift in Sources */, 958A735725E01F9E00FD7ECA /* ReadJson.swift in Sources */, @@ -401,6 +406,7 @@ 95F0461025E976B5006A5A17 /* SettingRow.swift in Sources */, 95F0460825E9704F006A5A17 /* ThemePicker.swift in Sources */, 95A5D95A25FCEDDB0090C1EA /* CompanyOption.swift in Sources */, + 95AEF3AC25FFBB4D001B77BB /* LinePath.swift in Sources */, 95672B9825DDA54700DCBE4A /* Persistence.swift in Sources */, 958A735B25E0264E00FD7ECA /* CompanyModel.swift in Sources */, 95B3E09F25E127D7007EFDE3 /* Request.swift in Sources */, @@ -418,11 +424,13 @@ 95ACB5AF25E03AA100A3CCC8 /* ThemeModel.swift in Sources */, 95672B8F25DDA54700DCBE4A /* LazyBearApp.swift in Sources */, 9517626025EEB37E00733235 /* PriceModel.swift in Sources */, + 95AEF3B325FFDC04001B77BB /* DeviceSize.swift in Sources */, 958A734525E00D3D00FD7ECA /* CompanyRow.swift in Sources */, 95DED9D525F2A752000DFCBA /* transactionCodes.swift in Sources */, 95ABDD3525E166BA00310776 /* NewsView.swift in Sources */, 957B816825F2A02C0005E5C0 /* InsiderTranModel.swift in Sources */, 95672B9B25DDA54800DCBE4A /* LazyBear.xcdatamodeld in Sources */, + 95AEF3B025FFD8CF001B77BB /* LineChart.swift in Sources */, 95F0460B25E970DB006A5A17 /* LanguagePicker.swift in Sources */, 95E9D09725F6AA0400A947A1 /* ActionView.swift in Sources */, 95ABDD3125E1602D00310776 /* PriceView.swift in Sources */, diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate Binary file LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/ContentView.swift --- a/LazyBear/ContentView.swift Sun Mar 14 13:25:21 2021 +0100 +++ b/LazyBear/ContentView.swift Mon Mar 15 20:06:24 2021 +0100 @@ -9,55 +9,63 @@ struct ContentView: View { @EnvironmentObject var hudManager: HudManager + @EnvironmentObject var deviceSize: DeviceSize // Fetch user appearence settings @FetchRequest(entity: UserSettings.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \UserSettings.changedAt, ascending: false)]) var userSettings: FetchedResults var body: some View { - ZStack(alignment: .top) { - TabView { - // First view - Watchlist() - .tabItem { - Label("Watchlist", systemImage: "list.dash") - } + GeometryReader { geo in + ZStack(alignment: .top) { + TabView { + // First view + Watchlist() + .tabItem { + Label("Watchlist", systemImage: "list.dash") + } + + // First view + Search() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + + // First view + Settings() + .tabItem { + Label("Settings", systemImage: "gear") + } + } - // First view - Search() - .tabItem { - Label("Search", systemImage: "magnifyingglass") - } + // Show HUDs + // Notification + Notification(text: "Company saved", image: "checkmark.circle") + .offset(y: hudManager.showNotification ? 0 : -100) + .animation(.easeInOut) - // First view - Settings() - .tabItem { - Label("Settings", systemImage: "gear") - } + // Action sheet + ZStack(alignment: .bottom) { + Color(.black) + .edgesIgnoringSafeArea(.all) + .opacity(hudManager.showAction ? 0.2: 0) + .animation(.easeInOut) + .onTapGesture { hudManager.showAction = false } + + ActionView() + .offset(y: hudManager.showAction ? 0 : 250) + .animation(.easeInOut) + .padding() + } } - - // Show HUDs - // Notification - Notification(text: "Company saved", image: "checkmark.circle") - .offset(y: hudManager.showNotification ? 0 : -100) - .animation(.easeInOut) - - // Action sheet - ZStack(alignment: .bottom) { - Color(.black) - .edgesIgnoringSafeArea(.all) - .opacity(hudManager.showAction ? 0.2: 0) - .animation(.easeInOut) - .onTapGesture { hudManager.showAction = false } - - ActionView() - .offset(y: hudManager.showAction ? 0 : 250) - .animation(.easeInOut) - .padding() + .accentColor(Color("\(userSettings.first?.theme?.lowercased() ?? "default")Accent")) + // If this value is not optional it will cause a crash + .onAppear { + // Assign device screen size to the class + self.deviceSize.width = geo.size.width + self.deviceSize.height = geo.size.height } } - .accentColor(Color("\(userSettings.first?.theme?.lowercased() ?? "default")Accent")) - // If this value is not optional it will cause a crash } } diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/DeviceSize.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LazyBear/DeviceSize.swift Mon Mar 15 20:06:24 2021 +0100 @@ -0,0 +1,13 @@ +// +// DeviceSize.swift +// LazyBear +// +// Created by Dennis Concepción Martín on 15/3/21. +// + +import SwiftUI + +class DeviceSize: ObservableObject { + @Published var width = CGFloat(400) + @Published var height = CGFloat(400) +} diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/LazyBearApp.swift --- a/LazyBear/LazyBearApp.swift Sun Mar 14 13:25:21 2021 +0100 +++ b/LazyBear/LazyBearApp.swift Mon Mar 15 20:06:24 2021 +0100 @@ -14,6 +14,7 @@ // Start ObservedObjects @ObservedObject var hudManager = HudManager() @ObservedObject var companyOption = CompanyOption() + @ObservedObject var deviceSize = DeviceSize() var body: some Scene { WindowGroup { @@ -21,6 +22,7 @@ .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(hudManager) .environmentObject(companyOption) + .environmentObject(deviceSize) } } } diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/Tests/DemoChart.swift --- a/LazyBear/Tests/DemoChart.swift Sun Mar 14 13:25:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -// -// DemoChart.swift -// LazyBear -// -// Created by Dennis Concepción Martín on 13/3/21. -// - -import SwiftUI - -struct DemoChart: View { - let sampleData: [Double] = [10, 11.2, 13.4, 10.2, 13.4, 12.4, 15.6, 18.7, 20.9, 21.2, 10.3] - - var body: some View { - let normalizedData = normalize(sampleData) - Path { path in - path.move(to: CGPoint(x: 0, y: 0)) - for point in normalizedData { - path.addLine(to: CGPoint(x: 1, y: point)) - } - - } - .fill(Color.green) - } - - 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 DemoChart_Previews: PreviewProvider { - static var previews: some View { - DemoChart() - } -} diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/UI/ChartView.swift --- a/LazyBear/UI/ChartView.swift Sun Mar 14 13:25:21 2021 +0100 +++ b/LazyBear/UI/ChartView.swift Mon Mar 15 20:06:24 2021 +0100 @@ -9,7 +9,6 @@ struct ChartView: View { var symbol: String - var chartHeight: CGFloat @State private var historicalPrices = [HistoricalPriceModel]() // Date picker @@ -25,17 +24,19 @@ request(url: url, model: [HistoricalPriceModel].self) { self.historicalPrices = $0 } }) - + let prices = historicalPrices.map { $0.close } + LineChart(data: prices) } .onAppear { let url = getUrl(endpoint: .historicalPrices, symbol: symbol, range: "3m") request(url: url, model: [HistoricalPriceModel].self) { self.historicalPrices = $0 } } + } } struct HistoricalPriceView_Previews: PreviewProvider { static var previews: some View { - ChartView(symbol: "aapl", chartHeight: 200) + ChartView(symbol: "aapl") } } diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/UI/CompanyView.swift --- a/LazyBear/UI/CompanyView.swift Sun Mar 14 13:25:21 2021 +0100 +++ b/LazyBear/UI/CompanyView.swift Mon Mar 15 20:06:24 2021 +0100 @@ -20,20 +20,20 @@ @FetchRequest(entity: Company.entity(), sortDescriptors: []) var companies: FetchedResults var body: some View { - GeometryReader { geo in - ScrollView { + ScrollView { + //VStack { if companyOption.view == .stock { PriceView(symbol: symbol, showVertical: false) - ChartView(symbol: symbol, chartHeight: geo.size.width / 2) - NewsView(symbol: symbol) + ChartView(symbol: symbol) + NewsView(symbol: symbol) } else if companyOption.view == .insiders { InsiderSummary(symbol: symbol) InsiderTransactions(symbol: symbol) } - } - .onAppear { companyOption.view = .stock } + //} } + .onAppear { companyOption.view = .stock } .toolbar { ToolbarItem(placement: .principal) { Button(action: { self.hudManager.showAction.toggle() }) { diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/UI/LineChart.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LazyBear/UI/LineChart.swift Mon Mar 15 20:06:24 2021 +0100 @@ -0,0 +1,56 @@ +// +// 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 { + VStack { + ZStack { + Grid(height: deviceSize.width, width: deviceSize.width) + .clipped() + .background(LinePath(width: deviceSize.width, data: data)) + + + } + } + .frame(width: deviceSize.width, height: deviceSize.width / 2) + + } +} + +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()) + } +} + +struct Grid: View { + var height: CGFloat + var width: CGFloat + + var body: some View { + VStack { + Rectangle() + .stroke(Color.gray.opacity(0.2), lineWidth: 0.5) + + Group { + Rectangle() + .stroke(Color.gray.opacity(0.2), lineWidth: 0.5) + + Rectangle() + .stroke(Color.gray.opacity(0.2), lineWidth: 0.5) + } + .offset(y: -8) + .padding(.bottom, -8) + } + } +} diff -r e1610b54015d -r 9e23e9b0ab36 LazyBear/UI/LinePath.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LazyBear/UI/LinePath.swift Mon Mar 15 20:06:24 2021 +0100 @@ -0,0 +1,67 @@ +// +// LinePath.swift +// LazyBear +// +// Created by Dennis Concepción Martín on 15/3/21. +// + +import SwiftUI + +struct LinePath: View { + var width: CGFloat + var data: [Double] + + var body: some View { + let height = width/2 + if !data.isEmpty { + let normalizedData = normalize(data) + + // Substract 2 to skip the first and the last item + let widthBetweenPoints = Double(width) / Double(normalizedData.count - 2) + + Path { path in + let initialPoint = normalizedData[0] * Double(height) + var x: Double = 0 + 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)) + } + } + } + .stroke(Color.green, lineWidth: 2) + .rotationEffect(.degrees(180), anchor: .center) // The path must be rotated + .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0)) + + } + } + + 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 LinePath_Previews: PreviewProvider { + static var previews: some View { + GeometryReader { geo in + VStack { + let data: [Double] = [50.0, 50.5, 51.0, 50.4, 50.8, 51.3, 51.5, 52, 51.9, 52.4] + LinePath(width: geo.size.width, data: data) + } + } + } +}