# HG changeset patch # User Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com> # Date 1623404262 -7200 # Node ID 5f21f7c23c5efcf92750047ec10bf7740df12a5c # Parent 1662a41e2c1a2c6609fe2fe82d9674e6dd03b4f7 Add comments and clean code diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear.xcodeproj/project.pbxproj --- a/LazyBear.xcodeproj/project.pbxproj Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear.xcodeproj/project.pbxproj Fri Jun 11 11:37:42 2021 +0200 @@ -230,13 +230,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 950272CA2635AA9F003E779D /* Company list */ = { + 950272CA2635AA9F003E779D /* Extensive List */ = { isa = PBXGroup; children = ( 95721DB3262787EF00EC527B /* ExtensiveList.swift */, 950272CB2635AABC003E779D /* Helpers */, ); - path = "Company list"; + path = "Extensive List"; sourceTree = ""; }; 950272CB2635AABC003E779D /* Helpers */ = { @@ -326,6 +326,7 @@ 95CCFB55266E7A0F00C384A1 /* InsiderTransactionModel.swift */, 95BB05B12670B8C3005A2029 /* KeyStatsModel.swift */, 95AF0FF82671342E0049C4AB /* DisplayWordsModel.swift */, + 95E8BAA22656D86E0016AE72 /* RequestType.swift */, ); path = "Global Models"; sourceTree = ""; @@ -418,12 +419,11 @@ 95893DD22613CAB5003698C5 /* Global Helpers */ = { isa = PBXGroup; children = ( - 950272CA2635AA9F003E779D /* Company list */, + 950272CA2635AA9F003E779D /* Extensive List */, 95A5188526186F590002D27C /* PriceView.swift */, 9550444826111FC9000E0BCB /* StockRow.swift */, 9550444B26111FED000E0BCB /* StockItem.swift */, 95BD2FB226341D36008B6752 /* BlurBackground.swift */, - 95E8BAA22656D86E0016AE72 /* RequestType.swift */, 950857D2266BE55F005357BA /* RowShape.swift */, ); path = "Global Helpers"; diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate Binary file LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Global Models/RequestType.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LazyBear/Global Models/RequestType.swift Fri Jun 11 11:37:42 2021 +0200 @@ -0,0 +1,14 @@ +// +// RequestType.swift +// LazyBear +// +// Created by Dennis Concepción Martín on 20/5/21. +// + +import SwiftUI + +enum RequestType { + case initial + case streaming + case refresh +} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Global functions/ConvertStringToDate.swift --- a/LazyBear/Global functions/ConvertStringToDate.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Global functions/ConvertStringToDate.swift Fri Jun 11 11:37:42 2021 +0200 @@ -7,8 +7,10 @@ import SwiftUI +/* + Convert String to date with the format specified + */ func convertStringToDate(_ stringDate: String) -> Date { - // Convert string to date let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Global functions/ParseJSON.swift --- a/LazyBear/Global functions/ParseJSON.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Global functions/ParseJSON.swift Fri Jun 11 11:37:42 2021 +0200 @@ -8,6 +8,9 @@ import Foundation +/* + Read JSON File + */ func parseJSON(_ filename: String) -> T { let data: Data diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Company/Chart.swift --- a/LazyBear/Views/Company/Chart.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Company/Chart.swift Fri Jun 11 11:37:42 2021 +0200 @@ -11,15 +11,11 @@ struct Chart: View { @ObservedObject var company: Company var symbol: String - - // Date picker - var ranges = ["1D", "5D", "1M", "3M", "6M", "1Y", "5Y"] - @State private var selectedRange = "3M" + var ranges = ["1D", "5D", "1M", "3M", "6M", "1Y", "5Y"] /// DatePicker ranges - // Set recurrent price request - @State private var timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() - - @State private var showingStatistics = false + @State private var selectedRange = "3M" /// Selected DatePicker range + @State private var timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() /// Set recurrent price request + @State private var showingStatistics = false /// Show StatisticsView of the company var body: some View { if company.showChartView { @@ -34,12 +30,15 @@ .foregroundColor(Color(.secondarySystemBackground)) .frame(height: 270) .overlay( + /* + Show PriceView and Chart + */ VStack { HStack { if let quote = company.chartData.quote![symbol.uppercased()] { let latestPrice = quote.latestPrice ?? 0 let changePercent = quote.changePercent ?? 0 - let priceViewStyle = PriceViewStyle( + let priceViewStyle = PriceViewStyle( /// Define PriceView style horizontalAlignment: .leading, verticalAlignment: .center, orientation: .HStack, @@ -53,7 +52,7 @@ } Spacer() - if let _ = company.chartData.keyStats { + if let _ = company.chartData.keyStats { /// Check if keyStats is empty -> Hide button Button("See stats", action: { showingStatistics = true }) } } @@ -63,13 +62,15 @@ if let historicalPrices = company.chartData.historicalPrices { let prices = historicalPrices.compactMap { $0.close } let dates = historicalPrices.compactMap { $0.date } -// let hours = historicalPrices.compactMap { $0.minute } LineChartView(data: prices, dates: dates, hours: nil, dragGesture: true) .padding(.bottom) } } ) + /* + Show latest news + */ if let latestNews = company.chartData.latestNews { VStack(spacing: 20) { ForEach(latestNews, id: \.self) { new in @@ -83,10 +84,10 @@ StatsView(keyStats: company.chartData.keyStats!) } .onAppear { self.timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() } // Start timer - .onDisappear { self.timer.upstream.connect().cancel() } // Stop timer + .onDisappear { self.timer.upstream.connect().cancel() } /// Stop timer .onReceive(timer) { _ in let url = "https://api.lazybear.app/company/chart/symbol=\(symbol)/type=streaming" - company.request(url, .streaming, "chart") } // Receive timer notification + company.request(url, .streaming, "chart") } /// Receive timer notification } else { ProgressView() .onAppear { diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Company/CompanyView.swift --- a/LazyBear/Views/Company/CompanyView.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Company/CompanyView.swift Fri Jun 11 11:37:42 2021 +0200 @@ -10,11 +10,12 @@ struct CompanyView: View { var symbol: String - @ObservedObject var company = Company() @State private var showViewSelector = false - // Views + /* + Views + */ @State private var showChartView = true @State private var showInsiderView = false @@ -23,8 +24,7 @@ VStack { CompanyHeader(symbol: symbol, showViewSelector: $showViewSelector) .padding(.bottom) - - // Chart View + if showChartView { Chart(company: company, symbol: symbol) } else if showInsiderView { @@ -43,7 +43,7 @@ } /* - Hide all views to show later the one tapped + Hide all views to show the one selected by user */ private func resetViews() { showChartView = false diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Company/Helpers/InsiderList.swift --- a/LazyBear/Views/Company/Helpers/InsiderList.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Company/Helpers/InsiderList.swift Fri Jun 11 11:37:42 2021 +0200 @@ -22,14 +22,10 @@ Button("See all", action: { showFullList = true }) } - // Get total shares owned by the top 10 insiders - let totalPositions = insiderSummary.map { $0.position ?? 0 }.reduce(0, +) + let totalPositions = insiderSummary.map { $0.position ?? 0 }.reduce(0, +) /// Get total shares owned by the top 10 insiders VStack(alignment: .leading, spacing: 20) { ForEach(insiderSummary.prefix(3), id: \.self) { insider in - - // Compute percentage of ownership for each insider - let percentage = Double(insider.position ?? 0) / Double(totalPositions) - + let percentage = Double(insider.position ?? 0) / Double(totalPositions) /// Compute percentage of ownership for each insider InsiderRow(percentageOfWidth: CGFloat(percentage), insiderRoster: insider) } } @@ -43,7 +39,13 @@ struct InsiderList_Previews: PreviewProvider { static var previews: some View { InsiderList(insiderSummary: - [InsiderRosterModel(entityName: "Dennis Concepcion", position: 1234, reportDate: 1234567)] + [ + InsiderRosterModel( + entityName: "Dennis Concepcion", + position: 1234, + reportDate: 1234567 + ) + ] ) } } @@ -55,14 +57,10 @@ var body: some View { NavigationView { ScrollView { - // Get total shares owned by the top 10 insiders - let totalPositions = insiderSummary.map { $0.position ?? 0 }.reduce(0, +) + let totalPositions = insiderSummary.map { $0.position ?? 0 }.reduce(0, +) /// Get total shares owned by the top 10 insiders VStack(alignment: .leading, spacing: 20) { ForEach(insiderSummary, id: \.self) { insider in - - // Compute percentage of ownership for each insider - let percentage = Double(insider.position ?? 0) / Double(totalPositions) - + let percentage = Double(insider.position ?? 0) / Double(totalPositions) /// Compute percentage of ownership for each insider InsiderRow(percentageOfWidth: CGFloat(percentage), insiderRoster: insider) } } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Company/Helpers/StatsView.swift --- a/LazyBear/Views/Company/Helpers/StatsView.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Company/Helpers/StatsView.swift Fri Jun 11 11:37:42 2021 +0200 @@ -9,14 +9,16 @@ struct StatsView: View { var keyStats: KeyStatsModel + @Environment(\.presentationMode) private var presentationStatsView + let displayWords: DisplayWordsModel = parseJSON("DisplayWords.json") var body: some View { NavigationView { Form { let mirror = Mirror(reflecting: keyStats) - ForEach(Array(mirror.children), id: \.label) { child in + ForEach(Array(mirror.children), id: \.label) { child in /// Iterate over each variable within the class if let unwrappedValue = unwrapAnyOptional(value: child.value) { HStack { let label = String(child.label!) @@ -62,40 +64,40 @@ struct StatsView_Previews: PreviewProvider { static var previews: some View { StatsView(keyStats: - KeyStatsModel( - companyName: "Apple inc", - employees: 123, - marketcap: 123, - float: 123, - sharesOutstanding: 123, - beta: 123.12, - peRatio: 123.4, - dividendYield: 123.4, - ttmDividendRate: 123.4, - ttmEPS: 123.4, - avg10Volume: 123, - avg30Volume: 123, - day50MovingAvg: 123.4, - day200MovingAvg: 123.4, - week52Change: 123.4, - week52High: 123.4, - week52Low: 123.4, - week52HighSplitAdjustOnly: 123.4, - week52LowSplitAdjustOnly: 123.4, - maxChangePercent: 123.4, - ytdChangePercent: 123.4, - day5ChangePercent: 123.4, - day30ChangePercent: 123.4, - month1ChangePercent: 123.4, - month3ChangePercent: 123.4, - month6ChangePercent: 123.4, - year1ChangePercent: 123.4, - year2ChangePercent: 123.4, - year5ChangePercent: 123.4, - exDividendDate: "2020-01-01", - nextDividendDate: "2020-01-01", - nextEarningsDate: "2020-01-01" - ) + KeyStatsModel( + companyName: "Apple inc", + employees: 123, + marketcap: 123, + float: 123, + sharesOutstanding: 123, + beta: 123.12, + peRatio: 123.4, + dividendYield: 123.4, + ttmDividendRate: 123.4, + ttmEPS: 123.4, + avg10Volume: 123, + avg30Volume: 123, + day50MovingAvg: 123.4, + day200MovingAvg: 123.4, + week52Change: 123.4, + week52High: 123.4, + week52Low: 123.4, + week52HighSplitAdjustOnly: 123.4, + week52LowSplitAdjustOnly: 123.4, + maxChangePercent: 123.4, + ytdChangePercent: 123.4, + day5ChangePercent: 123.4, + day30ChangePercent: 123.4, + month1ChangePercent: 123.4, + month3ChangePercent: 123.4, + month6ChangePercent: 123.4, + year1ChangePercent: 123.4, + year2ChangePercent: 123.4, + year5ChangePercent: 123.4, + exDividendDate: "2020-01-01", + nextDividendDate: "2020-01-01", + nextEarningsDate: "2020-01-01" + ) ) } } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Company/Helpers/TransactionList.swift --- a/LazyBear/Views/Company/Helpers/TransactionList.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Company/Helpers/TransactionList.swift Fri Jun 11 11:37:42 2021 +0200 @@ -9,6 +9,7 @@ struct TransactionList: View { var transactions: [InsiderTransactionModel] + @State private var showFullList = false var body: some View { @@ -38,17 +39,18 @@ static var previews: some View { TransactionList(transactions: [ - InsiderTransactionModel(filingDate: "2020-01-01", - fullName: "Dennis Concepcion", - postShares: 1234, - reportedTitle: "Director", - transactionCode: "S", - transactionPrice: 20.08, - transactionShares: 12345, - transactionValue: 1234567.0 + InsiderTransactionModel( + filingDate: "2020-01-01", + fullName: "Dennis Concepcion", + postShares: 1234, + reportedTitle: "Director", + transactionCode: "S", + transactionPrice: 20.08, + transactionShares: 12345, + transactionValue: 1234567.0 ) ] - ) + ) } } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Company/Helpers/TransactionRow.swift --- a/LazyBear/Views/Company/Helpers/TransactionRow.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Company/Helpers/TransactionRow.swift Fri Jun 11 11:37:42 2021 +0200 @@ -56,14 +56,15 @@ struct TransactionRow_Previews: PreviewProvider { static var previews: some View { TransactionRow(transaction: - InsiderTransactionModel(filingDate: "2020-01-01", - fullName: "Dennis Concepcion", - postShares: 1234, - reportedTitle: "Director", - transactionCode: "S", - transactionPrice: 20.08, - transactionShares: 12345, - transactionValue: 1234567.0 + InsiderTransactionModel( + filingDate: "2020-01-01", + fullName: "Dennis Concepcion", + postShares: 1234, + reportedTitle: "Director", + transactionCode: "S", + transactionPrice: 20.08, + transactionShares: 12345, + transactionValue: 1234567.0 ) ) } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Global Helpers/Company list/ExtensiveList.swift --- a/LazyBear/Views/Global Helpers/Company list/ExtensiveList.swift Wed Jun 09 20:26:28 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,136 +0,0 @@ -// -// ExtensiveList.swift -// LazyBear -// -// Created by Dennis Concepción Martín on 14/4/21. -// - -import SwiftUI - -struct ExtensiveList: View { - var listName: String - var list: [String: QuoteModel]? - var intradayPrices: [String: [IntradayPriceModel]]? - var latestCurrencies: [String: CurrencyModel]? - var addOnDelete: Bool - - @Environment(\.presentationMode) private var presentationMode - @Environment(\.managedObjectContext) private var moc - @FetchRequest(entity: WatchlistCompany.entity(), sortDescriptors: []) - var watchlistCompany: FetchedResults - - @State private var isEditMode: EditMode = .inactive - @State private var showRenameListAction = false - @State private var showDeleteListAlert = false - @State private var showSearchView = false - - var body: some View { - NavigationView { - ZStack { - VStack { - if let list = list { - List { - ForEach(Array(list.keys.sorted()), id: \.self) { companySymbol in - StockItem(symbol: companySymbol, - company: list[companySymbol]!, - intradayPrices: intradayPrices?[companySymbol], - orientation: .horizontal, - hidePriceView: self.isEditMode == .active // Hide on EditMode - ) - - } - .onDelete(perform: addOnDelete ? deleteCompany: nil) - } - } - - if let latestCurrencies = latestCurrencies { - List(Array(latestCurrencies.keys.sorted()), id: \.self) { currencySymbol in - CurrencyListItem(currencySymbol: currencySymbol, currency: latestCurrencies[currencySymbol]!) - - } - } - } - - // Blur background - Color(.black) - .edgesIgnoringSafeArea(.all) - .opacity(showRenameListAction ? 0.2: 0) - .animation(.easeInOut) - .onTapGesture { showRenameListAction = false } - - // Show rename Action Sheet - TextfieldAlert(listName: listName, showRenameAction: $showRenameListAction, presentationMode: presentationMode) - .offset(y: showRenameListAction ? 0: 700) - .animation(.easeInOut) - } - // Show delete list alert - .alert(isPresented: $showDeleteListAlert) { - Alert( - title: Text("Are you sure you want to delete this list?"), - message: Text("This action can't be undo"), - primaryButton: .destructive(Text("Delete")) { deleteList() }, - secondaryButton: .cancel() - ) - } - .navigationTitle(listName) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - if addOnDelete { - EditButton() - } else { - Button(action: { presentationMode.wrappedValue.dismiss() }) { - Image(systemName: "multiply") - .imageScale(.large) - } - } - } - ToolbarItem(placement: .navigationBarTrailing) { - if addOnDelete { - ToolbarMenu(showRenameListAction: $showRenameListAction, showDeleteListAlert: $showDeleteListAlert) - } - } - } - .environment(\.editMode, self.$isEditMode) // Always after Toolbar - } - } - - /* - Delete company from watchlist - */ - private func deleteCompany(at offsets: IndexSet) { - for index in offsets { - let company = watchlistCompany[index] - moc.delete(company) - } - do { - try moc.save() - print("Company deleted") - } catch { - print(error.localizedDescription) - } - } - - /* - Remove entire list if it's not the last one. It can't be zero watchlists - */ - private func deleteList() { - let selectedWatchlist = watchlistCompany.filter({ $0.watchlist == listName }) - for company in selectedWatchlist { - moc.delete(company) - } - do { - try moc.save() - print("List deleted") - presentationMode.wrappedValue.dismiss() // Dismiss view - } catch { - print(error.localizedDescription) - } - } -} - -struct ExtensiveList_Previews: PreviewProvider { - static var previews: some View { - ExtensiveList(listName: "List name", addOnDelete: false) - } -} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Global Helpers/Company list/Helpers/ToolbarMenu.swift --- a/LazyBear/Views/Global Helpers/Company list/Helpers/ToolbarMenu.swift Wed Jun 09 20:26:28 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +0,0 @@ -// -// ToolbarMenu.swift -// LazyBear -// -// Created by Dennis Concepción Martín on 25/4/21. -// - -import SwiftUI - -struct ToolbarMenu: View { - @Binding var showRenameListAction: Bool - @Binding var showDeleteListAlert: Bool - - @FetchRequest(entity: WatchlistCompany.entity(), sortDescriptors: []) - var watchlistCompany: FetchedResults - - var body: some View { - Menu { - Section { - Button(action: { showRenameListAction = true }) { - Label("Rename list", systemImage: "square.and.pencil") - } - } - - // If there are only 1 watchlist -> It cannot be deleted - if Set(watchlistCompany.map { $0.watchlist }).count > 1 { - Section(header: Text("Secondary actions")) { - Button(action: { showDeleteListAlert = true }) { - Label("Delete list", systemImage: "trash") - } - } - } - } - label: { - Label("Options", systemImage: "ellipsis.circle") - .imageScale(.large) - } - } -} - -struct ToolbarMenu_Previews: PreviewProvider { - static var previews: some View { - ToolbarMenu(showRenameListAction: .constant(false), showDeleteListAlert: .constant(false)) - } -} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Global Helpers/Extensive List/ExtensiveList.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LazyBear/Views/Global Helpers/Extensive List/ExtensiveList.swift Fri Jun 11 11:37:42 2021 +0200 @@ -0,0 +1,136 @@ +// +// ExtensiveList.swift +// LazyBear +// +// Created by Dennis Concepción Martín on 14/4/21. +// + +import SwiftUI + +struct ExtensiveList: View { + var listName: String + var list: [String: QuoteModel]? + var intradayPrices: [String: [IntradayPriceModel]]? + var latestCurrencies: [String: CurrencyModel]? + var addOnDelete: Bool + + @Environment(\.presentationMode) private var presentationMode + @Environment(\.managedObjectContext) private var moc + @FetchRequest(entity: WatchlistCompany.entity(), sortDescriptors: []) + var watchlistCompany: FetchedResults + + @State private var isEditMode: EditMode = .inactive + @State private var showRenameListAction = false + @State private var showDeleteListAlert = false + @State private var showSearchView = false + + var body: some View { + NavigationView { + ZStack { + VStack { + if let list = list { + List { + ForEach(Array(list.keys.sorted()), id: \.self) { companySymbol in + StockItem(symbol: companySymbol, + company: list[companySymbol]!, + intradayPrices: intradayPrices?[companySymbol], + orientation: .horizontal, + hidePriceView: self.isEditMode == .active // Hide on EditMode + ) + + } + .onDelete(perform: addOnDelete ? deleteCompany: nil) + } + } + + if let latestCurrencies = latestCurrencies { + List(Array(latestCurrencies.keys.sorted()), id: \.self) { currencySymbol in + CurrencyListItem(currencySymbol: currencySymbol, currency: latestCurrencies[currencySymbol]!) + + } + } + } + + // Blur background + Color(.black) + .edgesIgnoringSafeArea(.all) + .opacity(showRenameListAction ? 0.2: 0) + .animation(.easeInOut) + .onTapGesture { showRenameListAction = false } + + // Show rename Action Sheet + TextfieldAlert(listName: listName, showRenameAction: $showRenameListAction, presentationMode: presentationMode) + .offset(y: showRenameListAction ? 0: 700) + .animation(.easeInOut) + } + // Show delete list alert + .alert(isPresented: $showDeleteListAlert) { + Alert( + title: Text("Are you sure you want to delete this list?"), + message: Text("This action can't be undo"), + primaryButton: .destructive(Text("Delete")) { deleteList() }, + secondaryButton: .cancel() + ) + } + .navigationTitle(listName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + if addOnDelete { + EditButton() + } else { + Button(action: { presentationMode.wrappedValue.dismiss() }) { + Image(systemName: "multiply") + .imageScale(.large) + } + } + } + ToolbarItem(placement: .navigationBarTrailing) { + if addOnDelete { + ToolbarMenu(showRenameListAction: $showRenameListAction, showDeleteListAlert: $showDeleteListAlert) + } + } + } + .environment(\.editMode, self.$isEditMode) // Always after Toolbar + } + } + + /* + Delete company from watchlist + */ + private func deleteCompany(at offsets: IndexSet) { + for index in offsets { + let company = watchlistCompany[index] + moc.delete(company) + } + do { + try moc.save() + print("Company deleted") + } catch { + print(error.localizedDescription) + } + } + + /* + Remove entire list if it's not the last one. It can't be zero watchlists + */ + private func deleteList() { + let selectedWatchlist = watchlistCompany.filter({ $0.watchlist == listName }) + for company in selectedWatchlist { + moc.delete(company) + } + do { + try moc.save() + print("List deleted") + presentationMode.wrappedValue.dismiss() // Dismiss view + } catch { + print(error.localizedDescription) + } + } +} + +struct ExtensiveList_Previews: PreviewProvider { + static var previews: some View { + ExtensiveList(listName: "List name", addOnDelete: false) + } +} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Global Helpers/Extensive List/Helpers/ToolbarMenu.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LazyBear/Views/Global Helpers/Extensive List/Helpers/ToolbarMenu.swift Fri Jun 11 11:37:42 2021 +0200 @@ -0,0 +1,44 @@ +// +// ToolbarMenu.swift +// LazyBear +// +// Created by Dennis Concepción Martín on 25/4/21. +// + +import SwiftUI + +struct ToolbarMenu: View { + @Binding var showRenameListAction: Bool + @Binding var showDeleteListAlert: Bool + + @FetchRequest(entity: WatchlistCompany.entity(), sortDescriptors: []) + var watchlistCompany: FetchedResults + + var body: some View { + Menu { + Section { + Button(action: { showRenameListAction = true }) { + Label("Rename list", systemImage: "square.and.pencil") + } + } + + if Set(watchlistCompany.map { $0.watchlist }).count > 1 { /// If there are only 1 watchlist -> It cannot be deleted + Section(header: Text("Secondary actions")) { + Button(action: { showDeleteListAlert = true }) { + Label("Delete list", systemImage: "trash") + } + } + } + } + label: { + Label("Options", systemImage: "ellipsis.circle") + .imageScale(.large) + } + } +} + +struct ToolbarMenu_Previews: PreviewProvider { + static var previews: some View { + ToolbarMenu(showRenameListAction: .constant(false), showDeleteListAlert: .constant(false)) + } +} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Global Helpers/RequestType.swift --- a/LazyBear/Views/Global Helpers/RequestType.swift Wed Jun 09 20:26:28 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -// -// RequestType.swift -// LazyBear -// -// Created by Dennis Concepción Martín on 20/5/21. -// - -import SwiftUI - -enum RequestType { - case initial - case streaming - case refresh -} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Global Helpers/StockRow.swift --- a/LazyBear/Views/Global Helpers/StockRow.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Global Helpers/StockRow.swift Fri Jun 11 11:37:42 2021 +0200 @@ -13,8 +13,9 @@ var list: [String: QuoteModel] var intradayPrices: [String: [IntradayPriceModel]]? var addOnDelete: Bool + + @State private var showExtensiveList = false - @State private var showExtensiveList = false @Environment(\.managedObjectContext) private var moc var body: some View { diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Home/Helpers/SectorItem.swift --- a/LazyBear/Views/Home/Helpers/SectorItem.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Home/Helpers/SectorItem.swift Fri Jun 11 11:37:42 2021 +0200 @@ -37,8 +37,8 @@ } } -//struct SectorItem_Previews: PreviewProvider { -// static var previews: some View { -// SectorItem(sector: SectorPerformanceModel(name: "Technology", performance: 0.04)) -// } -//} +struct SectorItem_Previews: PreviewProvider { + static var previews: some View { + SectorItem(sector: SectorPerformanceModel(name: "Technology", performance: 0.04)) + } +} diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Home/HomeView.swift --- a/LazyBear/Views/Home/HomeView.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Home/HomeView.swift Fri Jun 11 11:37:42 2021 +0200 @@ -9,10 +9,9 @@ struct HomeView: View { @ObservedObject var home = Home() + @State private var showTradingDates = false - - // Set recurrent price request - @State private var timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() + @State private var timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() /// Set recurrent price request static let taskDateFormat: DateFormatter = { let formatter = DateFormatter() @@ -32,17 +31,12 @@ } if let lists = home.data.lists { - if let gainers = lists.gainers { - StockRow(listName: "Gainers", list: gainers, intradayPrices: home.data.intradayPrices, addOnDelete: false) - .listRowInsets(EdgeInsets()) - } - if let losers = lists.losers { - StockRow(listName: "Losers", list: losers, intradayPrices: home.data.intradayPrices, addOnDelete: false) - .listRowInsets(EdgeInsets()) - } - if let mostActive = lists.mostactive { - StockRow(listName: "Most active", list: mostActive, intradayPrices: home.data.intradayPrices, addOnDelete: false) - .listRowInsets(EdgeInsets()) + let mirror = Mirror(reflecting: lists) + ForEach(Array(mirror.children), id: \.label) { child in + if let list = child.value as? [String : QuoteModel] { + StockRow(listName: "\(child.label!)", list: list, intradayPrices: home.data.intradayPrices, addOnDelete: false) + .listRowInsets(EdgeInsets()) + } } } if let latestCurrencies = home.data.latestCurrencies { @@ -50,8 +44,8 @@ .listRowInsets(EdgeInsets()) } } - .onAppear { self.timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() } // Start timer - .onReceive(timer) { _ in home.request("https://api.lazybear.app/home/type=streaming", .streaming) } // Receive timer notification + .onAppear { self.timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() } /// Restart timer + .onReceive(timer) { _ in home.request("https://api.lazybear.app/home/type=streaming", .streaming) } /// Receive timer notification .onDisappear { self.timer.upstream.connect().cancel() } // Stop timer .navigationTitle("\(dueDate, formatter: Self.taskDateFormat)") .navigationBarTitleDisplayMode(.inline) diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Home/TradingDates.swift --- a/LazyBear/Views/Home/TradingDates.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Home/TradingDates.swift Fri Jun 11 11:37:42 2021 +0200 @@ -37,6 +37,9 @@ } } + /* + Get array of dates to use in ForEach + */ private func getArrayOfDates() -> [Date] { // Get array of the string dates let stringDates = self.dates.map { $0.date } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Profile/Helpers/TextfieldAlert.swift --- a/LazyBear/Views/Profile/Helpers/TextfieldAlert.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Profile/Helpers/TextfieldAlert.swift Fri Jun 11 11:37:42 2021 +0200 @@ -41,7 +41,9 @@ Divider() - // Cancel and Done buttons + /* + Cancel and Done buttons + */ HStack { Spacer() Button(action: { @@ -72,6 +74,9 @@ ) } + /* + Rename watchlist name in Core Data + */ private func renameList(_ newListName: String) { let selectedWatchlist = watchlistCompany.filter({ $0.watchlist == listName }) for company in selectedWatchlist { @@ -80,16 +85,15 @@ do { try moc.save() print("List updated") - UIApplication.shared.endEditing() // Dismiss Keyboard - self.showRenameAction = false // Dismiss action rename sheet - self.$presentationMode.wrappedValue.dismiss() // Dismiss Modal View + UIApplication.shared.endEditing() /// Dismiss Keyboard + self.showRenameAction = false /// Dismiss action rename sheet + self.$presentationMode.wrappedValue.dismiss() /// Dismiss Modal View } catch { print(error.localizedDescription) } } } -// Dismiss Keyboard -extension UIApplication { +extension UIApplication { /// Dismiss Keyboard Extension func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Profile/Helpers/WatchlistCreator.swift --- a/LazyBear/Views/Profile/Helpers/WatchlistCreator.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Profile/Helpers/WatchlistCreator.swift Fri Jun 11 11:37:42 2021 +0200 @@ -56,8 +56,7 @@ Button("Cancel", action: { self.showCancelAlert = true }) } } - // Show delete list alert - .alert(isPresented: $showCancelAlert) { + .alert(isPresented: $showCancelAlert) { /// Show alert when the user tries to exit the view without saving the new watchlist Alert( title: Text("Your watchlist won't be saved"), message: Text("This action can't be undo"), @@ -66,8 +65,7 @@ ) } } - // Show alert when watchlist name is empty - .alert(isPresented: $watchlistNameIsEmpty) { + .alert(isPresented: $watchlistNameIsEmpty) { /// Show alert when the user tries to save the watchlist without a name Alert( title: Text("Give a name to your new watchlist"), message: Text("Try My portfolio, Favourites, ..."), @@ -82,7 +80,7 @@ /* Search company in array and get the index -> Delete company at - this index from the array + this index from the array (Core Data) */ private func remove(_ company: SearchResponse) { let index = watchlistCreatorClass.companies.firstIndex(of: company) diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Profile/Helpers/WatchlistCreatorList.swift --- a/LazyBear/Views/Profile/Helpers/WatchlistCreatorList.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Profile/Helpers/WatchlistCreatorList.swift Fri Jun 11 11:37:42 2021 +0200 @@ -10,6 +10,7 @@ struct WatchlistCreatorList: View { @ObservedObject var watchlistCreatorClass: WatchlistCreatorClass + @State private var searchedCompany = String() @State private var companies = [SearchResponse]() @@ -29,11 +30,7 @@ .navigationTitle("Search") .navigationBarTitleDisplayMode(.inline) .onChange(of: searchedCompany, perform: { searchedCompany in - if !searchedCompany.isEmpty { - // Encode string with spaces - let encodedSearchedCompany = searchedCompany.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) - request("https://api.lazybear.app/search/text=\(encodedSearchedCompany ?? "")") - } + encodeAndRequest(searchedCompany) }) .toolbar { @@ -44,12 +41,27 @@ } } + /* + Get companies from the API that matches the searched text + */ private func request(_ url: String) { let bazooka = Bazooka() bazooka.request(url: url, model: [SearchResponse].self) { response in self.companies = response } } + + /* + 1) Check if searchedCompany is empty + 2) Encode white spaces + 3) Request API + */ + private func encodeAndRequest(_ searchedCompany: String) { + if !searchedCompany.isEmpty { + let encodedSearchedCompany = searchedCompany.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + request("https://api.lazybear.app/search/text=\(encodedSearchedCompany ?? "")") + } + } } struct SearchCompanies_Previews: PreviewProvider { diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Profile/Helpers/WatchlistCreatorSearchBar.swift --- a/LazyBear/Views/Profile/Helpers/WatchlistCreatorSearchBar.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Profile/Helpers/WatchlistCreatorSearchBar.swift Fri Jun 11 11:37:42 2021 +0200 @@ -13,9 +13,9 @@ var body: some View { TextField("", text: $searchedCompany) - .introspectTextField { textField in - textField.becomeFirstResponder() - } + .introspectTextField { textField in + textField.becomeFirstResponder() + } .padding(10) .padding(.leading, 40) .overlay( @@ -30,6 +30,7 @@ .cornerRadius(10) ) .padding() + } } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Profile/ProfileView.swift --- a/LazyBear/Views/Profile/ProfileView.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Profile/ProfileView.swift Fri Jun 11 11:37:42 2021 +0200 @@ -15,28 +15,25 @@ var watchlistCompanies: FetchedResults @State private var showCreateNewWatchlist = false - - // Set recurrent price request - @State private var timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() + @State private var timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() /// Set recurrent price request var body: some View { if profile.showView { NavigationView { List { - // Get Watchlist names -> Create rows for each watchlist -> in each row, show companies - let watchlists = Set(watchlistCompanies.map { $0.watchlist }) // Set -> avoid duplicates names - + /* + Get Watchlist names -> Create rows for each watchlist -> in each row, show companies + */ + let watchlists = Set(watchlistCompanies.map { $0.watchlist }) /// Set -> avoid duplicates names ForEach(Array(watchlists).sorted(), id: \.self) { listName in let symbols = watchlistCompanies.filter({ $0.watchlist == listName }).map { $0.symbol } - if let companies = profile.data.quotes { - // Select from API requested companies only the ones withing the watchlist - let list = companies.filter({ symbols.contains($0.key) }) + let list = companies.filter({ symbols.contains($0.key) }) /// From API response select the companies within the specified watchlist StockRow(listName: listName, list: list, intradayPrices: profile.data.intradayPrices, addOnDelete: true) } } .listRowInsets(EdgeInsets()) - .onAppear { // Refresh API requested companies when Core Data changes + .onAppear { /// Request API again when Core Data changes to update the list refreshList() } } @@ -68,7 +65,7 @@ } /* - Get symbols in watchlists -> Prepare url -> Request + Get symbols in watchlists (Core Data) -> Prepare url -> Request */ private func prepareUrl(_ requestType: RequestType) { let symbols = watchlistCompanies.map { $0.symbol } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Search/CompanyList.swift --- a/LazyBear/Views/Search/CompanyList.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Search/CompanyList.swift Fri Jun 11 11:37:42 2021 +0200 @@ -23,6 +23,34 @@ @Environment(\.presentationMode) static var presentationMode static var previews: some View { - CompanyList(searchResult: [SearchResponse(currency: "USD", region: "US", securityName: "aaple inc", symbol: "aapl"), SearchResponse(currency: "USD", region: "US", securityName: "aaple inc", symbol: "aapl"), SearchResponse(currency: "USD", region: "US", securityName: "aaple inc", symbol: "aapl"), SearchResponse(currency: "USD", region: "US", securityName: "aaple inc", symbol: "aapl"), SearchResponse(currency: "USD", region: "US", securityName: "aaple inc", symbol: "aapl")]) + CompanyList(searchResult: + [ + SearchResponse( + currency: "USD", + region: "US", + securityName: "aaple inc", + symbol: "aapl"), + SearchResponse( + currency: "USD", + region: "US", + securityName: "aaple inc", + symbol: "aapl"), + SearchResponse( + currency: "USD", + region: "US", + securityName: "aaple inc", + symbol: "aapl"), + SearchResponse( + currency: "USD", + region: "US", + securityName: "aaple inc", + symbol: "aapl"), + SearchResponse( + currency: "USD", + region: "US", + securityName: "aaple inc", + symbol: "aapl") + ] + ) } } diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Search/Helpers/SearchedCompanyItem.swift --- a/LazyBear/Views/Search/Helpers/SearchedCompanyItem.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Search/Helpers/SearchedCompanyItem.swift Fri Jun 11 11:37:42 2021 +0200 @@ -14,12 +14,12 @@ @FetchRequest(entity: WatchlistCompany.entity(), sortDescriptors: []) var watchlistCompany: FetchedResults - @State private var showingActionSheet = false + @State private var showingWatchlistSelector = false var body: some View { - let watchlistSymbols = watchlistCompany.map { $0.symbol } HStack { - Button(action: { self.showingActionSheet = true }) { + Button(action: { self.showingWatchlistSelector = true }) { + let watchlistSymbols = watchlistCompany.map { $0.symbol } if watchlistSymbols.contains(company.symbol!) { Image(systemName: "star.fill") .foregroundColor(.yellow) @@ -49,12 +49,15 @@ Text(company.region!) } } - .actionSheet(isPresented: $showingActionSheet) { + .actionSheet(isPresented: $showingWatchlistSelector) { ActionSheet(title: Text("Add to watchlist"), message: Text("Select"), buttons: generateButtons()) } } - // Get watchlist names -> generate buttons + /* + Generate buttons for each watchlist to let the user selects to which watchlist + he wants to add the company + */ private func generateButtons() -> [ActionSheet.Button] { var actionButtons = [ActionSheet.Button]() let watchlists = Set(watchlistCompany.map { $0.watchlist }) @@ -72,7 +75,9 @@ return actionButtons } - // Add to watchlist + /* + When the user taps the watchlist -> save the company to CoreData + */ private func addCompany(_ symbol: String, _ name: String, _ watchlist: String) { let watchlistCompany = WatchlistCompany(context: moc) watchlistCompany.symbol = symbol diff -r 1662a41e2c1a -r 5f21f7c23c5e LazyBear/Views/Search/SearchView.swift --- a/LazyBear/Views/Search/SearchView.swift Wed Jun 09 20:26:28 2021 +0200 +++ b/LazyBear/Views/Search/SearchView.swift Fri Jun 11 11:37:42 2021 +0200 @@ -40,17 +40,25 @@ .navigationBarTitleDisplayMode(.inline) .navigationBarSearch($searchedText) .onChange(of: searchedText, perform: { searchedText in - if !searchedText.isEmpty { - // Encode string with spaces - let encodedSearchedText = searchedText.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) - search.request("https://api.lazybear.app/search/text=\(encodedSearchedText ?? "")") - } else { - search.showSearchList = false - } + encodeAndRequest(searchedText) }) } .navigationViewStyle(StackNavigationViewStyle()) } + + /* + 1) Check if searchedText is empty + 2) Encode white spaces + 3) Make API request + */ + private func encodeAndRequest(_ searchedText: String) { + if !searchedText.isEmpty { + let encodedSearchedText = searchedText.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + search.request("https://api.lazybear.app/search/text=\(encodedSearchedText ?? "")") + } else { + search.showSearchList = false + } + } } struct SearchView_Previews: PreviewProvider {