changeset 21:c3dda63f50ed v1.1

Added Core Data and UI changes - Implement Watchlist - Change conversion design - Improve UX
author Dennis Concepción Martín <dennisconcepcionmartin@gmail.com>
date Mon, 19 Jul 2021 19:27:12 +0100
parents f8aabe5b7251
children 3596690dda73
files Simoleon.xcodeproj/project.pbxproj Simoleon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved Simoleon.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate Simoleon/ContentView.swift Simoleon/Conversion.swift Simoleon/Favourites.swift Simoleon/Helpers/ConditionalWrapper.swift Simoleon/Helpers/ConversionBox.swift Simoleon/Helpers/CurrencyButton.swift Simoleon/Helpers/CurrencyRow.swift Simoleon/Helpers/CurrencySelector.swift Simoleon/Helpers/FavouriteButton.swift Simoleon/Helpers/ResignKeyboard.swift Simoleon/Helpers/Sidebar.swift Simoleon/Models/Favourite+CoreDataClass.swift Simoleon/Models/Favourite+CoreDataProperties.swift Simoleon/Persistence.swift Simoleon/Simoleon.xcdatamodeld/Simoleon.xcdatamodel/contents
diffstat 18 files changed, 404 insertions(+), 170 deletions(-) [+]
line wrap: on
line diff
--- a/Simoleon.xcodeproj/project.pbxproj	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon.xcodeproj/project.pbxproj	Mon Jul 19 19:27:12 2021 +0100
@@ -15,12 +15,16 @@
 		95AEBCA326A0900E00613729 /* CurrencyQuoteModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */; };
 		95B54F4426A4842C001DC0D8 /* Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4326A4842C001DC0D8 /* Conversion.swift */; };
 		95B54F4626A48852001DC0D8 /* CurrencySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4526A48852001DC0D8 /* CurrencySelector.swift */; };
-		95B54F4826A4954B001DC0D8 /* CurrencyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4726A4954B001DC0D8 /* CurrencyButton.swift */; };
 		95B54F4A26A4A450001DC0D8 /* ConversionBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4926A4A450001DC0D8 /* ConversionBox.swift */; };
-		95B54F4D26A4A64F001DC0D8 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 95B54F4C26A4A64F001DC0D8 /* Introspect */; };
 		95B54F4F26A4AC52001DC0D8 /* ContentViewPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4E26A4AC52001DC0D8 /* ContentViewPad.swift */; };
 		95B54F5126A4ACAC001DC0D8 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */; };
 		95C02C8B269B61680061DD6D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 95C02C8A269B61680061DD6D /* Alamofire */; };
+		95C5179126A5DC8E00BC2B24 /* ConditionalWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */; };
+		95C5179926A5EC9F00BC2B24 /* FavouriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179826A5EC9F00BC2B24 /* FavouriteButton.swift */; };
+		95C5179C26A5EFBE00BC2B24 /* Favourite+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179A26A5EFBE00BC2B24 /* Favourite+CoreDataClass.swift */; };
+		95C5179D26A5EFBE00BC2B24 /* Favourite+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179B26A5EFBE00BC2B24 /* Favourite+CoreDataProperties.swift */; };
+		95C5179F26A5F34200BC2B24 /* Favourites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179E26A5F34200BC2B24 /* Favourites.swift */; };
+		95C517A126A5F6C000BC2B24 /* ResignKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */; };
 		95C5B2282697752600941585 /* SimoleonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2272697752600941585 /* SimoleonApp.swift */; };
 		95C5B22C2697752700941585 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95C5B22B2697752700941585 /* Assets.xcassets */; };
 		95C5B22F2697752700941585 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95C5B22E2697752700941585 /* Preview Assets.xcassets */; };
@@ -59,10 +63,15 @@
 		95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyQuoteModel.swift; sourceTree = "<group>"; };
 		95B54F4326A4842C001DC0D8 /* Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conversion.swift; sourceTree = "<group>"; };
 		95B54F4526A48852001DC0D8 /* CurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelector.swift; sourceTree = "<group>"; };
-		95B54F4726A4954B001DC0D8 /* CurrencyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyButton.swift; sourceTree = "<group>"; };
 		95B54F4926A4A450001DC0D8 /* ConversionBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversionBox.swift; sourceTree = "<group>"; };
 		95B54F4E26A4AC52001DC0D8 /* ContentViewPad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewPad.swift; sourceTree = "<group>"; };
 		95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
+		95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalWrapper.swift; sourceTree = "<group>"; };
+		95C5179826A5EC9F00BC2B24 /* FavouriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteButton.swift; sourceTree = "<group>"; };
+		95C5179A26A5EFBE00BC2B24 /* Favourite+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favourite+CoreDataClass.swift"; sourceTree = "<group>"; };
+		95C5179B26A5EFBE00BC2B24 /* Favourite+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favourite+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		95C5179E26A5F34200BC2B24 /* Favourites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favourites.swift; sourceTree = "<group>"; };
+		95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignKeyboard.swift; sourceTree = "<group>"; };
 		95C5B2242697752600941585 /* Simoleon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Simoleon.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		95C5B2272697752600941585 /* SimoleonApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonApp.swift; sourceTree = "<group>"; };
 		95C5B22B2697752700941585 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -88,7 +97,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				95C02C8B269B61680061DD6D /* Alamofire in Frameworks */,
-				95B54F4D26A4A64F001DC0D8 /* Introspect in Frameworks */,
 				95E7643A269E0037008E9F31 /* CloudKit.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -113,6 +121,8 @@
 		95559331269B094A000FD726 /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				95C5179A26A5EFBE00BC2B24 /* Favourite+CoreDataClass.swift */,
+				95C5179B26A5EFBE00BC2B24 /* Favourite+CoreDataProperties.swift */,
 				95AEBC9A26A04A4200613729 /* CurrencyMetadataModel.swift */,
 				95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */,
 			);
@@ -165,6 +175,7 @@
 				95AEBC9426A03ECB00613729 /* ContentView.swift */,
 				95B54F4E26A4AC52001DC0D8 /* ContentViewPad.swift */,
 				95B54F4326A4842C001DC0D8 /* Conversion.swift */,
+				95C5179E26A5F34200BC2B24 /* Favourites.swift */,
 				95C5B22B2697752700941585 /* Assets.xcassets */,
 				95C5B2302697752700941585 /* Persistence.swift */,
 				95C5B2352697752700941585 /* Info.plist */,
@@ -216,11 +227,13 @@
 		95FE659A269AFB44008745DE /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
-				95B54F4726A4954B001DC0D8 /* CurrencyButton.swift */,
 				95B54F4526A48852001DC0D8 /* CurrencySelector.swift */,
 				95AEBC9C26A04D4600613729 /* CurrencyRow.swift */,
 				95B54F4926A4A450001DC0D8 /* ConversionBox.swift */,
 				95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */,
+				95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */,
+				95C5179826A5EC9F00BC2B24 /* FavouriteButton.swift */,
+				95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -243,7 +256,6 @@
 			name = Simoleon;
 			packageProductDependencies = (
 				95C02C8A269B61680061DD6D /* Alamofire */,
-				95B54F4C26A4A64F001DC0D8 /* Introspect */,
 			);
 			productName = Simoleon;
 			productReference = 95C5B2242697752600941585 /* Simoleon.app */;
@@ -318,7 +330,6 @@
 			mainGroup = 95C5B21B2697752600941585;
 			packageReferences = (
 				95C02C89269B61680061DD6D /* XCRemoteSwiftPackageReference "Alamofire" */,
-				95B54F4B26A4A64F001DC0D8 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
 			);
 			productRefGroup = 95C5B2252697752600941585 /* Products */;
 			projectDirPath = "";
@@ -365,18 +376,23 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				95C5179926A5EC9F00BC2B24 /* FavouriteButton.swift in Sources */,
+				95C5179C26A5EFBE00BC2B24 /* Favourite+CoreDataClass.swift in Sources */,
 				95C5B2312697752700941585 /* Persistence.swift in Sources */,
 				95AEBC9526A03ECB00613729 /* ContentView.swift in Sources */,
 				95AEBC9B26A04A4200613729 /* CurrencyMetadataModel.swift in Sources */,
 				9555933A269B0AB8000FD726 /* ParseJson.swift in Sources */,
+				95C5179D26A5EFBE00BC2B24 /* Favourite+CoreDataProperties.swift in Sources */,
+				95C5179F26A5F34200BC2B24 /* Favourites.swift in Sources */,
 				95C5B2282697752600941585 /* SimoleonApp.swift in Sources */,
 				95B54F4A26A4A450001DC0D8 /* ConversionBox.swift in Sources */,
+				95C517A126A5F6C000BC2B24 /* ResignKeyboard.swift in Sources */,
 				95AEBC9D26A04D4600613729 /* CurrencyRow.swift in Sources */,
 				95AEBCA326A0900E00613729 /* CurrencyQuoteModel.swift in Sources */,
-				95B54F4826A4954B001DC0D8 /* CurrencyButton.swift in Sources */,
 				95B54F4F26A4AC52001DC0D8 /* ContentViewPad.swift in Sources */,
 				95B54F4426A4842C001DC0D8 /* Conversion.swift in Sources */,
 				95C5B2342697752700941585 /* Simoleon.xcdatamodeld in Sources */,
+				95C5179126A5DC8E00BC2B24 /* ConditionalWrapper.swift in Sources */,
 				95B54F5126A4ACAC001DC0D8 /* Sidebar.swift in Sources */,
 				95B54F4626A48852001DC0D8 /* CurrencySelector.swift in Sources */,
 			);
@@ -547,7 +563,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.1;
+				MARKETING_VERSION = 1.2;
 				PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.Simoleon;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;
@@ -572,7 +588,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.1;
+				MARKETING_VERSION = 1.2;
 				PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.Simoleon;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;
@@ -706,14 +722,6 @@
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
-		95B54F4B26A4A64F001DC0D8 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git";
-			requirement = {
-				kind = upToNextMajorVersion;
-				minimumVersion = 0.1.3;
-			};
-		};
 		95C02C89269B61680061DD6D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/Alamofire/Alamofire.git";
@@ -725,11 +733,6 @@
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
-		95B54F4C26A4A64F001DC0D8 /* Introspect */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 95B54F4B26A4A64F001DC0D8 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
-			productName = Introspect;
-		};
 		95C02C8A269B61680061DD6D /* Alamofire */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 95C02C89269B61680061DD6D /* XCRemoteSwiftPackageReference "Alamofire" */;
--- a/Simoleon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved	Mon Jul 19 19:27:12 2021 +0100
@@ -9,15 +9,6 @@
           "revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
           "version": "5.4.3"
         }
-      },
-      {
-        "package": "Introspect",
-        "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
-        "state": {
-          "branch": null,
-          "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2",
-          "version": "0.1.3"
-        }
       }
     ]
   },
Binary file Simoleon.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed
--- a/Simoleon/ContentView.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/ContentView.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -8,11 +8,32 @@
 import SwiftUI
 
 struct ContentView: View {
+    @State private var tab: Tab = .convert
     var body: some View {
-        NavigationView {
+        TabView(selection: $tab) {
             Conversion()
+                .tabItem {
+                    Label("Convert", systemImage: "arrow.counterclockwise.circle")
+                }
+                .tag(Tab.convert)
+            
+            Favourites()
+                .tabItem {
+                    Label("Favourites", systemImage: "star")
+                }
+                .tag(Tab.favourites)
+            
+            Text("Settings")
+                .tabItem {
+                    Label("Settings", systemImage: "gear")
+                }
+                .tag(Tab.settings)
         }
     }
+    
+    private enum Tab {
+        case convert, favourites, settings
+    }
 }
 
 struct ContentView_Previews: PreviewProvider {
--- a/Simoleon/Conversion.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Conversion.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -9,14 +9,12 @@
 import Alamofire
 
 struct Conversion: View {
-    @State private var mainCurrency = "USD"
-    @State private var secondaryCurrency = "GBP"
+    @State private var currencyPair = "USD/GBP"
     @State private var amountToConvert = "1000"
     @State private var price: Double = 1.00
     @State private var showingConversion = false
     @State private var showingCurrencySelector = false
-    @State private var selectingMainCurrency = false
-    @State private var currencyPairNotFound = false
+    @State private var isEditing = false
     
     let currencyMetadata: [String: CurrencyMetadataModel] = parseJson("CurrencyMetadata.json")
     
@@ -24,57 +22,66 @@
         ScrollView(showsIndicators: false) {
             VStack(alignment: .leading) {
                 HStack {
-                    Button(action: { selectingMainCurrency = true; showingCurrencySelector = true }) {
-                        CurrencyButton(currency: $mainCurrency)
+                    Button(action: { showingCurrencySelector = true }) {
+                        RoundedRectangle(cornerRadius: 25)
+                            .foregroundColor(Color(.secondarySystemBackground))
+                            .frame(height: 75)
+                            .overlay(CurrencyRow(currencyPair: currencyPair).padding(.horizontal))
                     }
                     
-                    Button(action: { selectingMainCurrency = false; showingCurrencySelector = true }) {
-                        CurrencyButton(currency: $secondaryCurrency)
-                    }
+                    FavouriteButton(currencyPair: currencyPair)
                 }
                 
                 ConversionBox(
-                    mainCurrency: $mainCurrency,
-                    secondaryCurrency: $secondaryCurrency,
+                    currencyPair: $currencyPair,
                     amountToConvert: $amountToConvert,
                     price: $price,
                     showingConversion: $showingConversion,
                     showingCurrencySelector: $showingCurrencySelector,
-                    currencyPairNotFound: $currencyPairNotFound
+                    isEditing: $isEditing
                 )
             }
             .padding()
-            .onAppear { requestApi(mainCurrency, secondaryCurrency) }
+            .onAppear { request(currencyPair) }
             .onChange(of: showingCurrencySelector, perform: { showingCurrencySelector in
                 if !showingCurrencySelector {
-                    requestApi(mainCurrency, secondaryCurrency)
+                    request(currencyPair)
                 }
             })
             .sheet(isPresented: $showingCurrencySelector) {
-                CurrencySelector(
-                    mainCurrencySelected: $mainCurrency,
-                    secondaryCurrencySelected: $secondaryCurrency,
-                    showingCurrencySelector: $showingCurrencySelector,
-                    selectingMainCurrency: $selectingMainCurrency
-                )
+                CurrencySelector(currencyPair: $currencyPair, showingCurrencySelector: $showingCurrencySelector)
             }
         }
-        .navigationBarHidden(true)
+        .if(UIDevice.current.userInterfaceIdiom == .phone) { content in
+            NavigationView {
+                content
+                    .navigationTitle("Conversion")
+                    .toolbar {
+                        ToolbarItem(placement: .cancellationAction) {
+                            if isEditing {
+                                Button("Cancel", action: {
+                                    UIApplication.shared.dismissKeyboard()
+                                    isEditing = false
+                                })
+                            }
+                        }
+                    }
+            }
+        }
     }
     
-    private func requestApi(_ mainCurrency: String, _ secondaryCurrency: String) {
-        let url = "https://api.1forge.com/quotes?pairs=\(mainCurrency)/\(secondaryCurrency)&api_key=BFWeJQ3jJtqqpDv5ArNis59pAlFcQ4KF"
+    private func request(_ currencyPair: String) {
+        let url = "https://api.1forge.com/quotes?pairs=\(currencyPair)&api_key=BFWeJQ3jJtqqpDv5ArNis59pAlFcQ4KF"
         
         AF.request(url).responseDecodable(of: [CurrencyQuoteModel].self) { response in
             self.showingConversion = false
-            self.currencyPairNotFound = false
             
             if let currencyQuotes = response.value {
                 if let price = currencyQuotes.first?.price {
                     self.price = price
                     self.showingConversion =  true
                 } else {
-                    self.currencyPairNotFound = true
+//                    Handle error
                 }
             } else {
 //               Handle error
@@ -86,8 +93,6 @@
 
 struct Conversion_Previews: PreviewProvider {
     static var previews: some View {
-        NavigationView {
-            Conversion()
-        }
+        Conversion()
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Favourites.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -0,0 +1,56 @@
+//
+//  Favourites.swift
+//  Simoleon
+//
+//  Created by Dennis Concepci贸n Mart铆n on 19/07/2021.
+//
+
+import SwiftUI
+
+struct Favourites: View {
+    @Environment(\.managedObjectContext) private var viewContext
+    @FetchRequest(
+        sortDescriptors: [NSSortDescriptor(keyPath: \Favourite.currencyPair, ascending: true)],
+        animation: .default)
+    private var favourite: FetchedResults<Favourite>
+    
+    var body: some View {
+        List {
+            ForEach(favourite) { favourite in
+                CurrencyRow(currencyPair: favourite.currencyPair)
+            }
+            .onDelete(perform: removeFromFavourites)
+        }
+        .if(UIDevice.current.userInterfaceIdiom == .phone) { content in
+            NavigationView {
+                content
+                    .navigationTitle("Favourites")
+                    .toolbar {
+                        #if os(iOS)
+                        EditButton()
+                        #endif
+                    }
+            }
+        }
+    }
+    
+    private func removeFromFavourites(offsets: IndexSet) {
+        withAnimation {
+            offsets.map { favourite[$0] }.forEach(viewContext.delete)
+
+            do {
+                try viewContext.save()
+            } catch {
+                let nsError = error as NSError
+                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+            }
+        }
+    }
+}
+
+struct Favourites_Previews: PreviewProvider {
+    static var previews: some View {
+        Favourites()
+            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/ConditionalWrapper.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -0,0 +1,19 @@
+//
+//  ConditionalWrapper.swift
+//  Simoleon
+//
+//  Created by Dennis Concepci贸n Mart铆n on 19/07/2021.
+//
+
+import SwiftUI
+
+extension View {
+   @ViewBuilder
+   func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
+        if conditional {
+            content(self)
+        } else {
+            self
+        }
+    }
+}
--- a/Simoleon/Helpers/ConversionBox.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Helpers/ConversionBox.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -6,44 +6,47 @@
 //
 
 import SwiftUI
-import Introspect
 
 struct ConversionBox: View {
-    @Binding var mainCurrency: String
-    @Binding var secondaryCurrency: String
+    @Binding var currencyPair: String
     @Binding var amountToConvert: String
     @Binding var price: Double
     @Binding var showingConversion: Bool
     @Binding var showingCurrencySelector: Bool
-    @Binding var currencyPairNotFound: Bool
-    
-    @State private var showingCancelationButton = false
+    @Binding var isEditing: Bool
     
     let currencyMetadata: [String: CurrencyMetadataModel] = parseJson("CurrencyMetadata.json")
     
     var body: some View {
         VStack(alignment: .leading) {
-            Text("\(currencyMetadata[mainCurrency]!.name) (\(mainCurrency))")
+            let currencies = currencyPair.split(separator: "/")
+            Text("\(currencyMetadata[String(currencies[0])]!.name) (\(String(currencies[0])))")
                 .font(.callout)
                 .fontWeight(.semibold)
                 .padding(.top, 40)
             
             ZStack(alignment: .trailing) {
-                TextField("Enter amount", text: $amountToConvert)
-                    .keyboardType(.decimalPad)
-                    .font(Font.title.weight(.semibold))
-                    .lineLimit(1)
-                    .padding(.bottom, 10)
-                    .introspectTextField { textField in
-                        if !showingCurrencySelector {
-                            textField.becomeFirstResponder()
-                        }
-                    }
+                TextField("Enter amount", text: $amountToConvert) { startedEditing in
+                if startedEditing {
+                         withAnimation {
+                            isEditing = true
+                         }
+                     }
+                }
+                onCommit: {
+                     withAnimation {
+                        isEditing = false
+                     }
+                 }
+                .keyboardType(.decimalPad)
+                .font(Font.title.weight(.semibold))
+                .lineLimit(1)
+                .padding(.bottom, 10)
             }
             
             Divider()
             
-            Text("\(currencyMetadata[secondaryCurrency]!.name) (\(secondaryCurrency))")
+            Text("\(currencyMetadata[String(currencies[1])]!.name) (\(String(currencies[1])))")
                 .font(.callout)
                 .fontWeight(.semibold)
                 .padding(.top, 10)
@@ -54,13 +57,8 @@
                     .lineLimit(1)
                     .padding(.top, 5)
             } else {
-                if currencyPairNotFound {
-                    Text("The currency pair selected is not supported yet 馃槩")
-                        .padding(.top, 5)
-                } else {
-                    ProgressView()
-                        .padding(.top, 5)
-                }
+                ProgressView()
+                    .padding(.top, 5)
             }
         }
     }
@@ -80,6 +78,6 @@
 
 struct ConversionBox_Previews: PreviewProvider {
     static var previews: some View {
-        ConversionBox(mainCurrency: .constant("USD"), secondaryCurrency: .constant("GBP"), amountToConvert: .constant("1000"), price: .constant(1), showingConversion: .constant(true), showingCurrencySelector: .constant(false), currencyPairNotFound: .constant(false))
+        ConversionBox(currencyPair: .constant("USD/GBP"), amountToConvert: .constant("1000"), price: .constant(1), showingConversion: .constant(false), showingCurrencySelector: .constant(false), isEditing: .constant(false))
     }
 }
--- a/Simoleon/Helpers/CurrencyButton.swift	Mon Jul 19 10:12:23 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-//
-//  CurrencyButton.swift
-//  Simoleon
-//
-//  Created by Dennis Concepci贸n Mart铆n on 18/07/2021.
-//
-
-import SwiftUI
-
-struct CurrencyButton: View {
-    @Binding var currency: String
-    let currencyMetadata: [String: CurrencyMetadataModel] = parseJson("CurrencyMetadata.json")
-    
-    var body: some View {
-        RoundedRectangle(cornerRadius: 25)
-            .foregroundColor(Color(.secondarySystemBackground))
-            .frame(height: 75)
-            .overlay(
-                HStack {
-                    Image(currencyMetadata[currency]!.flag)
-                        .resizable()
-                        .aspectRatio(contentMode: .fill)
-                        .frame(width: 30, height: 30)
-                        .clipShape(Circle())
-                        .overlay(Circle().stroke(Color(.systemGray), lineWidth: 1))
-                    
-                    Text("\(currency)")
-                        .fontWeight(.semibold)
-                        .foregroundColor(Color("PlainButton"))
-                }
-            )
-    }
-}
-
-struct CurrencyButton_Previews: PreviewProvider {
-    static var previews: some View {
-        CurrencyButton(currency: .constant("USD"))
-    }
-}
--- a/Simoleon/Helpers/CurrencyRow.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Helpers/CurrencyRow.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -8,37 +8,40 @@
 import SwiftUI
 
 struct CurrencyRow: View {
-    var currency: String
+    var currencyPair: String
     let currencyMetadata: [String: CurrencyMetadataModel] = parseJson("CurrencyMetadata.json")
     
     var body: some View {
         HStack {
-            Image(currencyMetadata[currency]!.flag)
+            let currencies = currencyPair.split(separator: "/")
+            Image(currencyMetadata[String(currencies[0])]!.flag)
                 .resizable()
                 .aspectRatio(contentMode: .fill)
                 .frame(width: 30, height: 30)
                 .clipShape(Circle())
                 .overlay(Circle().stroke(Color(.systemGray), lineWidth: 1))
             
-            VStack(alignment: .leading) {
-                Text("\(currency)")
-                    .fontWeight(.semibold)
-                    .foregroundColor(Color("PlainButton"))
-                
-                Text("\(currencyMetadata[currency]!.name)")
-                    .font(.footnote)
-                    .fontWeight(.semibold)
-                    .foregroundColor(Color("PlainButton"))
-                    .opacity(0.5)
-                    .lineLimit(1)
-            }
-            .padding(.horizontal)
+            Image(currencyMetadata[String(currencies[1])]!.flag)
+                .resizable()
+                .aspectRatio(contentMode: .fill)
+                .frame(width: 30, height: 30)
+                .clipShape(Circle())
+                .overlay(Circle().stroke(Color(.systemGray), lineWidth: 1))
+                .offset(x: -20)
+                .padding(.trailing, -20)
+            
+            Text("From \(String(currencies[0])) to \(String(currencies[1]))")
+                .fontWeight(.semibold)
+                .foregroundColor(Color("PlainButton"))
+                .padding(.leading)
+            
+            Spacer()
         }
     }
 }
 
 struct CurrencyRow_Previews: PreviewProvider {
     static var previews: some View {
-        CurrencyRow(currency: "USD")
+        CurrencyRow(currencyPair: "USD/GBP")
     }
 }
--- a/Simoleon/Helpers/CurrencySelector.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Helpers/CurrencySelector.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -8,58 +8,78 @@
 import SwiftUI
 
 struct CurrencySelector: View {
-    @Binding var mainCurrencySelected: String
-    @Binding var secondaryCurrencySelected: String
+    @Binding var currencyPair: String
     @Binding var showingCurrencySelector: Bool
-    @Binding var selectingMainCurrency: Bool
+    @State private var searchCurrency = ""
+    @State private var searching = false
     
     var body: some View {
         NavigationView {
             Form {
-                ForEach(generateCurrencyList(), id: \.self) { currency in
-                    Button(action: { select(currency) }) {
-                        CurrencyRow(currency: currency)
+                TextField("Search ...", text: $searchCurrency) { startedEditing in
+                if startedEditing {
+                         withAnimation {
+                             searching = true
+                         }
+                     }
+                } onCommit: {
+                    withAnimation {
+                        searching = false
+                    }
+                }
+                
+                Section(header: Text("All currencies")) {
+                    ForEach(currencyPairs(), id: \.self) { currencyPair in
+                        Button(action: {
+                            self.currencyPair = currencyPair
+                            showingCurrencySelector = false
+                        }) {
+                            CurrencyRow(currencyPair: currencyPair)
+                        }
                     }
                 }
             }
+            .gesture(DragGesture()
+                 .onChanged({ _ in
+                     UIApplication.shared.dismissKeyboard()
+                 })
+             )
             .navigationTitle("Currencies")
             .navigationBarTitleDisplayMode(.inline)
             .toolbar {
                 ToolbarItem(placement: .confirmationAction) {
                     Button("OK", action: { showingCurrencySelector = false })
                 }
+                
+                ToolbarItem(placement: .cancellationAction) {
+                    if searching {
+                         Button("Cancel") {
+                            searchCurrency = ""
+                             withAnimation {
+                                searching = false
+                                UIApplication.shared.dismissKeyboard()
+                             }
+                         }
+                     }
+                }
             }
         }
     }
     
-    private func generateCurrencyList() -> [String] {
+    private func currencyPairs() -> [String] {
         let currencyPairs: [String] = parseJson("CurrencyPairs.json")
-        var currencies: [String] = []
-        
-        for currencyPair in currencyPairs {
-            let splittedCurrencies = currencyPair.split(separator: "/")
-            let mainCurrency = String(splittedCurrencies[0])
-            if !currencies.contains(mainCurrency) {
-                currencies.append(mainCurrency)
-            }
-        }
         
-        return currencies
-    }
-    
-    private func select(_ currency: String) {
-        if selectingMainCurrency {
-            self.mainCurrencySelected = currency
+        if searchCurrency.isEmpty {
+            return currencyPairs
         } else {
-            self.secondaryCurrencySelected = currency
+            return currencyPairs.filter { $0.contains(searchCurrency.uppercased()) }
         }
-        
-        showingCurrencySelector = false
     }
 }
 
+
 struct CurrencySelector_Previews: PreviewProvider {
     static var previews: some View {
-        CurrencySelector(mainCurrencySelected: .constant(""), secondaryCurrencySelected: .constant(""), showingCurrencySelector: .constant(false), selectingMainCurrency: .constant(true))
+        CurrencySelector(currencyPair: .constant("USD/GBP"), showingCurrencySelector: .constant(false))
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/FavouriteButton.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -0,0 +1,86 @@
+//
+//  FavouriteButton.swift
+//  Simoleon
+//
+//  Created by Dennis Concepci贸n Mart铆n on 19/07/2021.
+//
+
+import SwiftUI
+
+struct FavouriteButton: View {
+    var currencyPair: String
+    @Environment(\.managedObjectContext) private var viewContext
+    @FetchRequest(sortDescriptors: []) private var favourite: FetchedResults<Favourite>
+    
+    var body: some View {
+        Button(action: {
+            if isFavourite() {
+                removeFromFavourites()
+            } else {
+                addToFavourites()
+            }
+        }) {
+            RoundedRectangle(cornerRadius: 25)
+                .foregroundColor(Color(.secondarySystemBackground))
+                .frame(width: 75, height: 75)
+                .overlay(
+                    Image(systemName: generateStar())
+                        .font(.system(size: 28))
+                        .foregroundColor(Color(.systemYellow))
+                        
+                )
+        }
+    }
+    
+    private func isFavourite() -> Bool {
+        let favouriteCurrencyPairs = favourite.map { $0.currencyPair }
+        
+        if favouriteCurrencyPairs.contains(currencyPair) {
+            return true
+        } else {
+            return false
+        }
+    }
+    
+    private func generateStar() -> String {
+        if isFavourite() {
+            return "star.fill"
+        } else {
+            return "star"
+        }
+    }
+    
+    private func removeFromFavourites() {
+        withAnimation {
+            let favouriteObject = favourite.first(where: { $0.currencyPair == currencyPair })
+            viewContext.delete(favouriteObject ?? Favourite())
+            
+            do {
+                try viewContext.save()
+            } catch {
+                let nsError = error as NSError
+                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+            }
+        }
+    }
+    
+    private func addToFavourites() {
+        withAnimation {
+            let favourite = Favourite(context: viewContext)
+            favourite.currencyPair = currencyPair
+            
+            do {
+                try viewContext.save()
+            } catch {
+                let nsError = error as NSError
+                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+            }
+        }
+    }
+}
+
+struct FavouriteButton_Previews: PreviewProvider {
+    static var previews: some View {
+        FavouriteButton(currencyPair: "USD/GBP")
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/ResignKeyboard.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -0,0 +1,14 @@
+//
+//  ResignKeyboard.swift
+//  Simoleon
+//
+//  Created by Dennis Concepci贸n Mart铆n on 19/07/2021.
+//
+
+import SwiftUI
+
+extension UIApplication {
+    func dismissKeyboard() {
+        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+    }
+}
--- a/Simoleon/Helpers/Sidebar.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Helpers/Sidebar.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -11,7 +11,15 @@
     var body: some View {
         List {
             NavigationLink(destination: Conversion()) {
-                Label("Convert", systemImage: "glass")
+                Label("Convert", systemImage: "arrow.counterclockwise.circle")
+            }
+            
+            NavigationLink(destination: Text("Favourites")) {
+                Label("Favourites", systemImage: "star")
+            }
+            
+            NavigationLink(destination: Text("Settings")) {
+                Label("Settings", systemImage: "gear")
             }
         }
         .listStyle(SidebarListStyle())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Models/Favourite+CoreDataClass.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -0,0 +1,15 @@
+//
+//  Favourite+CoreDataClass.swift
+//  Simoleon
+//
+//  Created by Dennis Concepci贸n Mart铆n on 19/07/2021.
+//
+//
+
+import Foundation
+import CoreData
+
+@objc(Favourite)
+public class Favourite: NSManagedObject {
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Models/Favourite+CoreDataProperties.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -0,0 +1,25 @@
+//
+//  Favourite+CoreDataProperties.swift
+//  Simoleon
+//
+//  Created by Dennis Concepci贸n Mart铆n on 19/07/2021.
+//
+//
+
+import Foundation
+import CoreData
+
+
+extension Favourite {
+
+    @nonobjc public class func fetchRequest() -> NSFetchRequest<Favourite> {
+        return NSFetchRequest<Favourite>(entityName: "Favourite")
+    }
+
+    @NSManaged public var currencyPair: String
+
+}
+
+extension Favourite : Identifiable {
+
+}
--- a/Simoleon/Persistence.swift	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Persistence.swift	Mon Jul 19 19:27:12 2021 +0100
@@ -17,6 +17,11 @@
             let newItem = Item(context: viewContext)
             newItem.timestamp = Date()
         }
+        
+        for _ in 0..<10 {
+            let favourite = Favourite(context: viewContext)
+            favourite.currencyPair = "USD/GBP"
+        }
         do {
             try viewContext.save()
         } catch {
--- a/Simoleon/Simoleon.xcdatamodeld/Simoleon.xcdatamodel/contents	Mon Jul 19 10:12:23 2021 +0100
+++ b/Simoleon/Simoleon.xcdatamodeld/Simoleon.xcdatamodel/contents	Mon Jul 19 19:27:12 2021 +0100
@@ -1,9 +1,13 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="true" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20F71" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
+    <entity name="Favourite" representedClassName="Favourite" syncable="YES">
+        <attribute name="currencyPair" optional="YES" attributeType="String"/>
+    </entity>
     <entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
     </entity>
     <elements>
         <element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
+        <element name="Favourite" positionX="-63" positionY="-9" width="128" height="44"/>
     </elements>
 </model>
\ No newline at end of file