From ce71a5bde6a75aa241f011d97c6422e8c20540cd Mon Sep 17 00:00:00 2001 From: Vova Galchenko Date: Sun, 24 Dec 2023 13:33:17 -0800 Subject: [PATCH 1/4] All Swift from the ingredient list view down * General project changes: * Changed the target iOS version to 16. Our analytics shows that the oldest version of iOS used by our users in the recent past is 16.2. * Our project seemed to have no consistent structure with regards to XCode groups and directory structure. I aligned both XCode groups and directory structure with the following: - UI: layout of views. - Design Language: the design conventions used throughout the app. If we ever want to reskin the app, it should be possible to do so by merely changing things inside this folder. - Model: describes the data that powers the app. - Misc Utils: utilities used broadly throughout the app, such as facilities for assertion checking. Both Swift and ObjC sourcecode lives in this hierarchy, but I put ObjC code within groups/directories named `ObjC` within the hierarchy. * The old UIKit-based `CSIngredientListVC` written in ObjC has been replaced with SwiftUI-based `IngredientListView`. Although our usecase is seemingly basic, I've found it difficult to do _exactly_ what we want. I assume this might be much easier for you guys, so I sprinkled a bunch of O&A questions in there. Almost everything downstream of IngredientListView is now in Swift. The sole exception that I can think of is CSGlassView. * The model has almost fully migrated to Swift. Structs are used to represent ingredient groups, ingredients and other aspects of the model. IngredientsStore is the sole class in the data model and represents the entrypoint into the persistence layer. A couple of ObjC model classes remain to support some legacy ObjC UI code: * `CSIngredient` - `Ingredient` struct is bridged to `CSIngredient` by use of a dictionary. * `CSUnit` - used across both ObjC and Swift code. * Colors have moved into our assetcatalog for smoother support of dark mode or other reskinning. `CSColor` enum was created to facilitate access to the assetcatalog. Eventually, we should get to the point where colors are accessed exclusively through `CSColor`. * All text styling has been centralized under the `CSTextStyle` struct. Facilities have been introduced to apply `CSTextStyle` to SwiftUI's `Text` as well as UIKit's `UILabel`, `UIButton` and `UITextField`. There is no styling of text outside of `CSTextStyle`, though this isn't currently automatically enforced. --- .../CakeTests/DoubleStringUtilsTests.swift | 20 +- CookSmart/CookSmart.xcodeproj/project.pbxproj | 252 +++++++------ CookSmart/CookSmart/CSConversionVC.m | 274 -------------- .../CookSmart/CSFilteredIngredientGroup.h | 17 - .../CookSmart/CSFilteredIngredientGroup.m | 43 --- CookSmart/CookSmart/CSGlassView.m | 99 ----- CookSmart/CookSmart/CSIngredientGroup.h | 28 -- CookSmart/CookSmart/CSIngredientGroup.m | 132 ------- .../CookSmart/CSIngredientGroupInternals.h | 20 - CookSmart/CookSmart/CSIngredientListVC.h | 27 -- CookSmart/CookSmart/CSIngredientListVC.m | 345 ----------------- .../CookSmart/CSIngredientListViewCell.h | 16 - .../CookSmart/CSIngredientListViewCell.m | 43 --- CookSmart/CookSmart/CSIngredients.h | 51 --- CookSmart/CookSmart/CSIngredients.m | 348 ------------------ .../CookSmart/CSRecentsIngredientGroup.h | 16 - .../CookSmart/CSRecentsIngredientGroup.m | 54 --- .../CSScaleView+ViewRepresentable.swift | 24 -- CookSmart/CookSmart/CookSmart-Prefix.pch | 2 +- CookSmart/CookSmart/Core/Button.swift | 37 -- CookSmart/CookSmart/Core/Colors.swift | 31 -- .../CookSmart/Core/Double+StringUtils.swift | 44 --- CookSmart/CookSmart/Core/Fonts.swift | 35 -- CookSmart/CookSmart/Core/Fraction.swift | 53 --- CookSmart/CookSmart/Core/Label.swift | 49 --- CookSmart/CookSmart/Core/StyledView.swift | 21 -- .../AccentColor.colorset/Contents.json | 38 ++ .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/152.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/180.png | Bin .../AppIcon.appiconset/20.png | Bin .../AppIcon.appiconset/29.png | Bin .../AppIcon.appiconset/40.png | Bin .../AppIcon.appiconset/58.png | Bin .../AppIcon.appiconset/60.png | Bin .../AppIcon.appiconset/76.png | Bin .../AppIcon.appiconset/80.png | Bin .../AppIcon.appiconset/87.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../BackgroundColor.colorset/Contents.json | 38 ++ .../Close.imageset/Contents.json | 0 .../Close.imageset/icon_076.png | Bin .../ContentTextColor.colorset/Contents.json | 38 ++ .../Assets.xcassets/Contents.json | 6 + .../Contents.json | 38 ++ .../CookSmart/Design Language/CSColor.swift | 30 ++ .../Design Language/CSTextStyle.swift | 108 ++++++ .../CookSmart/Design Language/CakeColor.swift | 24 ++ .../CookSmart/Images.xcassets/Contents.json | 6 - CookSmart/CookSmart/Ingredients.plist | 168 ++++----- .../CookSmart/Misc Utils/Assertions.swift | 32 ++ .../Misc Utils/Double+StringUtils.swift | 88 +++++ CookSmart/CookSmart/Model/Density.swift | 46 +++ CookSmart/CookSmart/Model/Ingredient.swift | 82 +++++ .../IngredientGroup/IngredientGroup.swift | 22 ++ .../RecentsIngredientGroup.swift | 27 ++ .../StoredIngredientGroup.swift | 44 +++ .../CookSmart/Model/IngredientsStore.swift | 230 ++++++++++++ .../CookSmart/{ => Model/ObjC}/CSIngredient.h | 4 +- .../CookSmart/{ => Model/ObjC}/CSIngredient.m | 29 +- .../CookSmart/Model/ObjC/CSSharedConstants.h | 17 + .../CookSmart/Model/ObjC/CSSharedConstants.m | 16 + .../CookSmart/{ => Model/ObjC/Units}/CSUnit.h | 2 + .../CookSmart/{ => Model/ObjC/Units}/CSUnit.m | 0 .../{ => Model/ObjC/Units}/CSUnitCollection.h | 0 .../{ => Model/ObjC/Units}/CSUnitCollection.m | 0 .../CookSmart/Scale View/ScalesView.swift | 141 ------- CookSmart/CookSmart/ScaleView.swift | 42 --- CookSmart/CookSmart/ScaleViewController.swift | 147 -------- CookSmart/CookSmart/SceneDelegate.swift | 2 + .../EditIngredientViewController.swift | 132 +++---- .../CookSmart/UI/IngredientListView.swift | 265 +++++++++++++ .../CookSmart/{ => UI/ObjC}/CSConversionVC.h | 5 +- CookSmart/CookSmart/UI/ObjC/CSConversionVC.m | 262 +++++++++++++ .../{ => UI/ObjC}/CSConversionVC.xib | 0 .../{ => UI/Scales}/CenterLineView.swift | 4 +- .../{ => UI/Scales}/GradientView.swift | 5 +- .../{ => UI/Scales/ObjC}/CSGlassView.h | 2 + .../CookSmart/UI/Scales/ObjC/CSGlassView.m | 143 +++++++ .../{ => UI/Scales/ObjC}/CSGradientView.h | 0 .../{ => UI/Scales/ObjC}/CSGradientView.m | 0 .../{ => UI/Scales/ObjC}/CSScaleVC.h | 0 .../{ => UI/Scales/ObjC}/CSScaleVC.m | 0 .../{ => UI/Scales/ObjC}/CSScaleVC.xib | 70 ++-- .../{ => UI/Scales/ObjC}/CSScaleVCInternals.h | 0 .../{ => UI/Scales/ObjC}/CSScaleView.h | 0 .../{ => UI/Scales/ObjC}/CSScaleView.m | 3 +- .../Scales}/ScaleScrollView.swift | 124 ++----- .../{Scale View => UI/Scales}/ScaleTile.swift | 4 +- .../UI/Scales/ScaleViewController.swift | 202 ++++++++++ .../CookSmart/UI/Scales/ScalesView.swift | 185 ++++++++++ .../{ => UI/Scales}/UnitPickerView.swift | 19 +- .../{Core => UI}/UIView+Constraints.swift | 0 CookSmart/CookSmart/UnitPickerDelegate.swift | 14 - CookSmart/CookSmart/cake-Bridging-Header.h | 5 +- 97 files changed, 2355 insertions(+), 2635 deletions(-) delete mode 100644 CookSmart/CookSmart/CSConversionVC.m delete mode 100644 CookSmart/CookSmart/CSFilteredIngredientGroup.h delete mode 100644 CookSmart/CookSmart/CSFilteredIngredientGroup.m delete mode 100644 CookSmart/CookSmart/CSGlassView.m delete mode 100644 CookSmart/CookSmart/CSIngredientGroup.h delete mode 100644 CookSmart/CookSmart/CSIngredientGroup.m delete mode 100644 CookSmart/CookSmart/CSIngredientGroupInternals.h delete mode 100644 CookSmart/CookSmart/CSIngredientListVC.h delete mode 100644 CookSmart/CookSmart/CSIngredientListVC.m delete mode 100644 CookSmart/CookSmart/CSIngredientListViewCell.h delete mode 100644 CookSmart/CookSmart/CSIngredientListViewCell.m delete mode 100644 CookSmart/CookSmart/CSIngredients.h delete mode 100644 CookSmart/CookSmart/CSIngredients.m delete mode 100644 CookSmart/CookSmart/CSRecentsIngredientGroup.h delete mode 100644 CookSmart/CookSmart/CSRecentsIngredientGroup.m delete mode 100644 CookSmart/CookSmart/CSScaleView+ViewRepresentable.swift delete mode 100644 CookSmart/CookSmart/Core/Button.swift delete mode 100644 CookSmart/CookSmart/Core/Colors.swift delete mode 100644 CookSmart/CookSmart/Core/Double+StringUtils.swift delete mode 100644 CookSmart/CookSmart/Core/Fonts.swift delete mode 100644 CookSmart/CookSmart/Core/Fraction.swift delete mode 100644 CookSmart/CookSmart/Core/Label.swift delete mode 100644 CookSmart/CookSmart/Core/StyledView.swift create mode 100644 CookSmart/CookSmart/Design Language/Assets.xcassets/AccentColor.colorset/Contents.json rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/1024.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/120.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/152.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/167.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/180.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/20.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/29.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/40.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/58.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/60.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/76.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/80.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/87.png (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/AppIcon.appiconset/Contents.json (100%) create mode 100644 CookSmart/CookSmart/Design Language/Assets.xcassets/BackgroundColor.colorset/Contents.json rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/Close.imageset/Contents.json (100%) rename CookSmart/CookSmart/{Images.xcassets => Design Language/Assets.xcassets}/Close.imageset/icon_076.png (100%) create mode 100644 CookSmart/CookSmart/Design Language/Assets.xcassets/ContentTextColor.colorset/Contents.json create mode 100644 CookSmart/CookSmart/Design Language/Assets.xcassets/Contents.json create mode 100644 CookSmart/CookSmart/Design Language/Assets.xcassets/SubheadingTextColor.colorset/Contents.json create mode 100644 CookSmart/CookSmart/Design Language/CSColor.swift create mode 100644 CookSmart/CookSmart/Design Language/CSTextStyle.swift create mode 100644 CookSmart/CookSmart/Design Language/CakeColor.swift delete mode 100644 CookSmart/CookSmart/Images.xcassets/Contents.json create mode 100644 CookSmart/CookSmart/Misc Utils/Assertions.swift create mode 100644 CookSmart/CookSmart/Misc Utils/Double+StringUtils.swift create mode 100644 CookSmart/CookSmart/Model/Density.swift create mode 100644 CookSmart/CookSmart/Model/Ingredient.swift create mode 100644 CookSmart/CookSmart/Model/IngredientGroup/IngredientGroup.swift create mode 100644 CookSmart/CookSmart/Model/IngredientGroup/RecentsIngredientGroup.swift create mode 100644 CookSmart/CookSmart/Model/IngredientGroup/StoredIngredientGroup.swift create mode 100644 CookSmart/CookSmart/Model/IngredientsStore.swift rename CookSmart/CookSmart/{ => Model/ObjC}/CSIngredient.h (92%) rename CookSmart/CookSmart/{ => Model/ObjC}/CSIngredient.m (61%) create mode 100644 CookSmart/CookSmart/Model/ObjC/CSSharedConstants.h create mode 100644 CookSmart/CookSmart/Model/ObjC/CSSharedConstants.m rename CookSmart/CookSmart/{ => Model/ObjC/Units}/CSUnit.h (84%) rename CookSmart/CookSmart/{ => Model/ObjC/Units}/CSUnit.m (100%) rename CookSmart/CookSmart/{ => Model/ObjC/Units}/CSUnitCollection.h (100%) rename CookSmart/CookSmart/{ => Model/ObjC/Units}/CSUnitCollection.m (100%) delete mode 100644 CookSmart/CookSmart/Scale View/ScalesView.swift delete mode 100644 CookSmart/CookSmart/ScaleView.swift delete mode 100644 CookSmart/CookSmart/ScaleViewController.swift rename CookSmart/CookSmart/{Edit Ingredient => UI}/EditIngredientViewController.swift (52%) create mode 100644 CookSmart/CookSmart/UI/IngredientListView.swift rename CookSmart/CookSmart/{ => UI/ObjC}/CSConversionVC.h (66%) create mode 100644 CookSmart/CookSmart/UI/ObjC/CSConversionVC.m rename CookSmart/CookSmart/{ => UI/ObjC}/CSConversionVC.xib (100%) rename CookSmart/CookSmart/{ => UI/Scales}/CenterLineView.swift (93%) rename CookSmart/CookSmart/{ => UI/Scales}/GradientView.swift (90%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSGlassView.h (83%) create mode 100644 CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.m rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSGradientView.h (100%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSGradientView.m (100%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSScaleVC.h (100%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSScaleVC.m (100%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSScaleVC.xib (81%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSScaleVCInternals.h (100%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSScaleView.h (100%) rename CookSmart/CookSmart/{ => UI/Scales/ObjC}/CSScaleView.m (98%) rename CookSmart/CookSmart/{Scale View => UI/Scales}/ScaleScrollView.swift (65%) rename CookSmart/CookSmart/{Scale View => UI/Scales}/ScaleTile.swift (98%) create mode 100644 CookSmart/CookSmart/UI/Scales/ScaleViewController.swift create mode 100644 CookSmart/CookSmart/UI/Scales/ScalesView.swift rename CookSmart/CookSmart/{ => UI/Scales}/UnitPickerView.swift (93%) rename CookSmart/CookSmart/{Core => UI}/UIView+Constraints.swift (100%) delete mode 100644 CookSmart/CookSmart/UnitPickerDelegate.swift diff --git a/CookSmart/CakeTests/DoubleStringUtilsTests.swift b/CookSmart/CakeTests/DoubleStringUtilsTests.swift index 272e1f7..225d4ef 100644 --- a/CookSmart/CakeTests/DoubleStringUtilsTests.swift +++ b/CookSmart/CakeTests/DoubleStringUtilsTests.swift @@ -40,61 +40,61 @@ class DoubleSringUtilsTests: XCTestCase { func test_vulgarFractionString_aboveThreshold_roundsUp() { let rawValue = 66.7 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "67") } func test_vulgarFractionString_aboveThreshold_roundsDown() { let rawValue = 66.445 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "66") } func test_vulgarFractionString_aboveThreshold() { let rawValue = 66.445 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "66") } func test_vulgarFractionString_belowThreshold_half() { let rawValue = 49.45 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "49½") } func test_vulgarFractionString_belowThreshold_zero() { let rawValue = 0.05 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "0") } func test_vulgarFractionString_belowThreshold_eighth() { let rawValue = 0.1 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "⅛") } func test_vulgarFractionString_belowThreshold_quarter() { let rawValue = 1.20 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "1¼") } func test_vulgarFractionString_belowThreshold_third() { let rawValue = 1.35 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "1⅓") } func test_vulgarFractionString_belowThreshold_one() { let rawValue = 0.95 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "1") } func test_vulgarFractionString_belowThreshold_two() { let rawValue = 1.95 - let result = rawValue.vulgarFractionString + let result = rawValue.humanReabableString XCTAssertEqual(result, "2") } } diff --git a/CookSmart/CookSmart.xcodeproj/project.pbxproj b/CookSmart/CookSmart.xcodeproj/project.pbxproj index 6092d25..1b5ede4 100644 --- a/CookSmart/CookSmart.xcodeproj/project.pbxproj +++ b/CookSmart/CookSmart.xcodeproj/project.pbxproj @@ -7,35 +7,35 @@ objects = { /* Begin PBXBuildFile section */ - 392D8645242FD0FF002064D6 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392D8644242FD0FF002064D6 /* Colors.swift */; }; 396E363D2439684E00C2815B /* CenterLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396E363C2439684E00C2815B /* CenterLineView.swift */; }; 396E363F24396D5900C2815B /* UnitPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396E363E24396D5900C2815B /* UnitPickerView.swift */; }; 396E3643243976A000C2815B /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396E3642243976A000C2815B /* GradientView.swift */; }; - 396E3647243A3BDC00C2815B /* UnitPickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396E3646243A3BDC00C2815B /* UnitPickerDelegate.swift */; }; 39C3CE582442775E00359EDF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C3CE562442775E00359EDF /* AppDelegate.swift */; }; 39C3CE5A2442776D00359EDF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C3CE592442776D00359EDF /* SceneDelegate.swift */; }; 39C3CE642442A1C000359EDF /* EditIngredientViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C3CE632442A1C000359EDF /* EditIngredientViewController.swift */; }; 39C3CE82244BE22100359EDF /* ScalesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C3CE81244BE22100359EDF /* ScalesView.swift */; }; - 773C97E2244BE5D900208351 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773C97E0244BE5D900208351 /* Button.swift */; }; - 773C97E5244BF0BF00208351 /* StyledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773C97E4244BF0BF00208351 /* StyledView.swift */; }; - 773C97E8244BF14A00208351 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773C97E7244BF14A00208351 /* Label.swift */; }; 77479AEA242FB9C8000CFB0E /* ScaleTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77479AE9242FB9C8000CFB0E /* ScaleTile.swift */; }; - 77479AF924303D86000CFB0E /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77479AF824303D86000CFB0E /* Fonts.swift */; }; 774EBD6524426CC2002A59AC /* Double+StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774EBD6424426CC2002A59AC /* Double+StringUtils.swift */; }; 774EBD77244271DC002A59AC /* DoubleStringUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774EBD67244270C8002A59AC /* DoubleStringUtilsTests.swift */; }; - 774EBD792442A1CE002A59AC /* Fraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774EBD782442A1CE002A59AC /* Fraction.swift */; }; 774EBD7D2442E475002A59AC /* ScaleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774EBD7C2442E475002A59AC /* ScaleViewController.swift */; }; - 774EBD802442EC26002A59AC /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774EBD7F2442EC26002A59AC /* UIView+Constraints.swift */; }; 77F41932243950CB0055053E /* ScaleScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F41931243950CB0055053E /* ScaleScrollView.swift */; }; - 80433B2E1BB26C1A006B7A85 /* CSRecentsIngredientGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 80433B2D1BB26C1A006B7A85 /* CSRecentsIngredientGroup.m */; }; 80433B361BB2731B006B7A85 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 80433B351BB2731B006B7A85 /* Launch Screen.storyboard */; }; - 80592C0518924B010082D4E1 /* CSIngredients.m in Sources */ = {isa = PBXBuildFile; fileRef = 80592C0418924B010082D4E1 /* CSIngredients.m */; }; + 80475EF62B12AB00009644DF /* IngredientListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80475EF52B12AB00009644DF /* IngredientListView.swift */; }; + 80475EFF2B12E105009644DF /* Ingredient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80475EFE2B12E105009644DF /* Ingredient.swift */; }; + 80475F012B12E257009644DF /* StoredIngredientGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80475F002B12E257009644DF /* StoredIngredientGroup.swift */; }; + 80475F032B12E33B009644DF /* IngredientsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80475F022B12E33B009644DF /* IngredientsStore.swift */; }; + 80475F052B130C41009644DF /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80475F042B130C41009644DF /* Assertions.swift */; }; + 805231B52B38F397009C473A /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805231B42B38F397009C473A /* UIView+Constraints.swift */; }; + 805541E62B16BD5300529F3B /* IngredientGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805541E52B16BD5300529F3B /* IngredientGroup.swift */; }; + 805541E82B16C82C00529F3B /* RecentsIngredientGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805541E72B16C82C00529F3B /* RecentsIngredientGroup.swift */; }; + 805541F02B19780200529F3B /* CSSharedConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 805541EF2B19780200529F3B /* CSSharedConstants.m */; }; + 805541F52B1AC45700529F3B /* Density.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805541F42B1AC45700529F3B /* Density.swift */; }; + 805541FD2B1C0DC000529F3B /* CSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805541FC2B1C0DC000529F3B /* CSColor.swift */; }; 80592C0818924B0F0082D4E1 /* CSIngredient.m in Sources */ = {isa = PBXBuildFile; fileRef = 80592C0718924B0F0082D4E1 /* CSIngredient.m */; }; - 80592C0B1892541D0082D4E1 /* CSIngredientGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 80592C0A1892541D0082D4E1 /* CSIngredientGroup.m */; }; 80592C0E189268D60082D4E1 /* CSScaleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 80592C0D189268D60082D4E1 /* CSScaleView.m */; }; 80592C1718939E030082D4E1 /* CSGradientView.m in Sources */ = {isa = PBXBuildFile; fileRef = 80592C1618939E030082D4E1 /* CSGradientView.m */; }; + 805B8B0F2B1C71BE00FF0BBF /* CSTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805B8B0E2B1C71BE00FF0BBF /* CSTextStyle.swift */; }; 80629C5E18D4AED600106131 /* libVGAnalytics.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 80629C5B18D4AE8A00106131 /* libVGAnalytics.a */; }; - 807C0B6C1BB3DD4D007D2C03 /* CSIngredientListViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 807C0B6B1BB3DD4D007D2C03 /* CSIngredientListViewCell.m */; }; 80A614DE18BF3D4100A205BB /* CSGlassView.m in Sources */ = {isa = PBXBuildFile; fileRef = 80A614DD18BF3D4100A205BB /* CSGlassView.m */; }; 80CD77B72B1158A10042B93D /* libz.1.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 80629C6718D4B3BF00106131 /* libz.1.tbd */; }; 931AF96818C2B8AF0018AA8B /* WeightUnits.plist in Resources */ = {isa = PBXBuildFile; fileRef = 931AF96718C2B8AF0018AA8B /* WeightUnits.plist */; }; @@ -43,7 +43,6 @@ 931AF96D18C2BAE50018AA8B /* CSUnitCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = 931AF96C18C2BAE50018AA8B /* CSUnitCollection.m */; }; 9358BD6E1891CAAA00A99D51 /* CSConversionVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 9358BD6C1891CAAA00A99D51 /* CSConversionVC.m */; }; 9358BD6F1891CAAA00A99D51 /* CSConversionVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9358BD6D1891CAAA00A99D51 /* CSConversionVC.xib */; }; - 936809701891059700655B89 /* CSIngredientListVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 9368096F1891059700655B89 /* CSIngredientListVC.m */; }; 93D09934189766080014DB3E /* CSUnit.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D09933189766080014DB3E /* CSUnit.m */; }; 93F6CCA818AED6050045400D /* CSScaleVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 93F6CCA618AED6050045400D /* CSScaleVC.m */; }; 93F6CCA918AED6050045400D /* CSScaleVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 93F6CCA718AED6050045400D /* CSScaleVC.xib */; }; @@ -51,9 +50,8 @@ B7BCA293180524A800CF7588 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7BCA292180524A800CF7588 /* CoreGraphics.framework */; }; B7BCA295180524A800CF7588 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7BCA294180524A800CF7588 /* UIKit.framework */; }; B7BCA29B180524A800CF7588 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B7BCA299180524A800CF7588 /* InfoPlist.strings */; }; - B7BCA2A3180524A800CF7588 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7BCA2A2180524A800CF7588 /* Images.xcassets */; }; + B7BCA2A3180524A800CF7588 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7BCA2A2180524A800CF7588 /* Assets.xcassets */; }; B7BCA2C01805284A00CF7588 /* Ingredients.plist in Resources */ = {isa = PBXBuildFile; fileRef = B7BCA2BF1805284A00CF7588 /* Ingredients.plist */; }; - DC82F26218AC25B9004FD733 /* CSFilteredIngredientGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = DC82F26118AC25B9004FD733 /* CSFilteredIngredientGroup.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -88,46 +86,43 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 392D8644242FD0FF002064D6 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 396E363C2439684E00C2815B /* CenterLineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterLineView.swift; sourceTree = ""; }; 396E363E24396D5900C2815B /* UnitPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitPickerView.swift; sourceTree = ""; }; 396E3642243976A000C2815B /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; - 396E3646243A3BDC00C2815B /* UnitPickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitPickerDelegate.swift; sourceTree = ""; }; 39C3CE562442775E00359EDF /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 39C3CE592442776D00359EDF /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 39C3CE632442A1C000359EDF /* EditIngredientViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditIngredientViewController.swift; sourceTree = ""; }; 39C3CE81244BE22100359EDF /* ScalesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalesView.swift; sourceTree = ""; }; - 773C97E0244BE5D900208351 /* Button.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; - 773C97E4244BF0BF00208351 /* StyledView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledView.swift; sourceTree = ""; }; - 773C97E7244BF14A00208351 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; 77479AE8242FB9C8000CFB0E /* cake-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cake-Bridging-Header.h"; sourceTree = ""; }; 77479AE9242FB9C8000CFB0E /* ScaleTile.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ScaleTile.swift; sourceTree = ""; }; - 77479AF824303D86000CFB0E /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; 774EBD6424426CC2002A59AC /* Double+StringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+StringUtils.swift"; sourceTree = ""; }; 774EBD67244270C8002A59AC /* DoubleStringUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleStringUtilsTests.swift; sourceTree = ""; }; 774EBD6D244271D6002A59AC /* CakeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CakeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 774EBD71244271D6002A59AC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 774EBD782442A1CE002A59AC /* Fraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fraction.swift; sourceTree = ""; }; 774EBD7C2442E475002A59AC /* ScaleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleViewController.swift; sourceTree = ""; }; - 774EBD7F2442EC26002A59AC /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; 77F41931243950CB0055053E /* ScaleScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleScrollView.swift; sourceTree = ""; }; - 80433B2C1BB26C1A006B7A85 /* CSRecentsIngredientGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSRecentsIngredientGroup.h; sourceTree = ""; }; - 80433B2D1BB26C1A006B7A85 /* CSRecentsIngredientGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSRecentsIngredientGroup.m; sourceTree = ""; }; 80433B351BB2731B006B7A85 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; - 80592C0318924B010082D4E1 /* CSIngredients.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSIngredients.h; sourceTree = ""; }; - 80592C0418924B010082D4E1 /* CSIngredients.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSIngredients.m; sourceTree = ""; }; + 80475EF52B12AB00009644DF /* IngredientListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IngredientListView.swift; sourceTree = ""; }; + 80475EFE2B12E105009644DF /* Ingredient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ingredient.swift; sourceTree = ""; }; + 80475F002B12E257009644DF /* StoredIngredientGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredIngredientGroup.swift; sourceTree = ""; }; + 80475F022B12E33B009644DF /* IngredientsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IngredientsStore.swift; sourceTree = ""; }; + 80475F042B130C41009644DF /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = ""; }; + 805231B42B38F397009C473A /* UIView+Constraints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; + 805541E52B16BD5300529F3B /* IngredientGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IngredientGroup.swift; sourceTree = ""; }; + 805541E72B16C82C00529F3B /* RecentsIngredientGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsIngredientGroup.swift; sourceTree = ""; }; + 805541EE2B19780200529F3B /* CSSharedConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSSharedConstants.h; sourceTree = ""; }; + 805541EF2B19780200529F3B /* CSSharedConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CSSharedConstants.m; sourceTree = ""; }; + 805541F42B1AC45700529F3B /* Density.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Density.swift; sourceTree = ""; }; + 805541FC2B1C0DC000529F3B /* CSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSColor.swift; sourceTree = ""; }; 80592C0618924B0F0082D4E1 /* CSIngredient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSIngredient.h; sourceTree = ""; }; 80592C0718924B0F0082D4E1 /* CSIngredient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSIngredient.m; sourceTree = ""; }; - 80592C091892541D0082D4E1 /* CSIngredientGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSIngredientGroup.h; sourceTree = ""; }; - 80592C0A1892541D0082D4E1 /* CSIngredientGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSIngredientGroup.m; sourceTree = ""; }; 80592C0C189268D60082D4E1 /* CSScaleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSScaleView.h; sourceTree = ""; }; 80592C0D189268D60082D4E1 /* CSScaleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSScaleView.m; sourceTree = ""; }; 80592C1518939E030082D4E1 /* CSGradientView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSGradientView.h; sourceTree = ""; }; 80592C1618939E030082D4E1 /* CSGradientView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSGradientView.m; sourceTree = ""; }; + 805B8B0E2B1C71BE00FF0BBF /* CSTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSTextStyle.swift; sourceTree = ""; }; 80629C5518D4AE8A00106131 /* VGAnalytics.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = VGAnalytics.xcodeproj; path = "../analytics/iOS Analytics/VGAnalytics.xcodeproj"; sourceTree = ""; }; 80629C6718D4B3BF00106131 /* libz.1.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.1.tbd; path = ../iPhoneOS.sdk/usr/lib/libz.1.tbd; sourceTree = SDKROOT; }; - 807C0B6A1BB3DD4D007D2C03 /* CSIngredientListViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSIngredientListViewCell.h; sourceTree = ""; }; - 807C0B6B1BB3DD4D007D2C03 /* CSIngredientListViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSIngredientListViewCell.m; sourceTree = ""; }; 8087757D18C24D1F00A94031 /* CSScaleVCInternals.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSScaleVCInternals.h; sourceTree = ""; }; 80A614DC18BF3D4100A205BB /* CSGlassView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSGlassView.h; sourceTree = ""; }; 80A614DD18BF3D4100A205BB /* CSGlassView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSGlassView.m; sourceTree = ""; }; @@ -138,8 +133,6 @@ 9358BD6B1891CAAA00A99D51 /* CSConversionVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSConversionVC.h; sourceTree = ""; }; 9358BD6C1891CAAA00A99D51 /* CSConversionVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSConversionVC.m; sourceTree = ""; }; 9358BD6D1891CAAA00A99D51 /* CSConversionVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CSConversionVC.xib; sourceTree = ""; }; - 9368096E1891059700655B89 /* CSIngredientListVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSIngredientListVC.h; sourceTree = ""; }; - 9368096F1891059700655B89 /* CSIngredientListVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSIngredientListVC.m; sourceTree = ""; }; 93D09932189766080014DB3E /* CSUnit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSUnit.h; sourceTree = ""; }; 93D09933189766080014DB3E /* CSUnit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSUnit.m; sourceTree = ""; }; 93F6CCA518AED6050045400D /* CSScaleVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSScaleVC.h; sourceTree = ""; }; @@ -152,11 +145,8 @@ B7BCA298180524A800CF7588 /* CookSmart-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CookSmart-Info.plist"; sourceTree = ""; }; B7BCA29A180524A800CF7588 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; B7BCA29E180524A800CF7588 /* CookSmart-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CookSmart-Prefix.pch"; sourceTree = ""; }; - B7BCA2A2180524A800CF7588 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + B7BCA2A2180524A800CF7588 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B7BCA2BF1805284A00CF7588 /* Ingredients.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Ingredients.plist; sourceTree = ""; }; - DC82F26018AC25B9004FD733 /* CSFilteredIngredientGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSFilteredIngredientGroup.h; sourceTree = ""; }; - DC82F26118AC25B9004FD733 /* CSFilteredIngredientGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSFilteredIngredientGroup.m; sourceTree = ""; }; - DC82F26318AC2642004FD733 /* CSIngredientGroupInternals.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSIngredientGroupInternals.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -182,37 +172,28 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 392D8642242FD0CA002064D6 /* Core */ = { + 392D8642242FD0CA002064D6 /* Misc Utils */ = { isa = PBXGroup; children = ( - 773C97E0244BE5D900208351 /* Button.swift */, - 392D8644242FD0FF002064D6 /* Colors.swift */, - 77479AF824303D86000CFB0E /* Fonts.swift */, 774EBD6424426CC2002A59AC /* Double+StringUtils.swift */, - 774EBD782442A1CE002A59AC /* Fraction.swift */, - 774EBD7F2442EC26002A59AC /* UIView+Constraints.swift */, - 773C97E4244BF0BF00208351 /* StyledView.swift */, - 773C97E7244BF14A00208351 /* Label.swift */, + 80475F042B130C41009644DF /* Assertions.swift */, ); - path = Core; + path = "Misc Utils"; sourceTree = ""; }; - 39C3CE612442A1A300359EDF /* Edit Ingredient */ = { - isa = PBXGroup; - children = ( - 39C3CE632442A1C000359EDF /* EditIngredientViewController.swift */, - ); - path = "Edit Ingredient"; - sourceTree = ""; - }; - 39C3CE7F244BE1B100359EDF /* Scale View */ = { + 39C3CE7F244BE1B100359EDF /* Scales */ = { isa = PBXGroup; children = ( + 805231B12B38D566009C473A /* ObjC */, + 396E363E24396D5900C2815B /* UnitPickerView.swift */, + 396E3642243976A000C2815B /* GradientView.swift */, + 396E363C2439684E00C2815B /* CenterLineView.swift */, + 774EBD7C2442E475002A59AC /* ScaleViewController.swift */, 77479AE9242FB9C8000CFB0E /* ScaleTile.swift */, 77F41931243950CB0055053E /* ScaleScrollView.swift */, 39C3CE81244BE22100359EDF /* ScalesView.swift */, ); - path = "Scale View"; + path = Scales; sourceTree = ""; }; 774EBD6E244271D6002A59AC /* CakeTests */ = { @@ -224,84 +205,107 @@ path = CakeTests; sourceTree = ""; }; - 8000355C18B3CBF50032BF91 /* Scales */ = { + 80475EF32B12AAC9009644DF /* UI */ = { + isa = PBXGroup; + children = ( + 805231B42B38F397009C473A /* UIView+Constraints.swift */, + 805231B22B38D6B5009C473A /* ObjC */, + 80475EF52B12AB00009644DF /* IngredientListView.swift */, + 39C3CE632442A1C000359EDF /* EditIngredientViewController.swift */, + 39C3CE7F244BE1B100359EDF /* Scales */, + ); + path = UI; + sourceTree = ""; + }; + 80475EFD2B12E01A009644DF /* Model */ = { isa = PBXGroup; children = ( + 805231B02B38D4E6009C473A /* ObjC */, + 805541E92B18709F00529F3B /* IngredientGroup */, + 80475EFE2B12E105009644DF /* Ingredient.swift */, + 80475F022B12E33B009644DF /* IngredientsStore.swift */, + 805541F42B1AC45700529F3B /* Density.swift */, + ); + path = Model; + sourceTree = ""; + }; + 805231B02B38D4E6009C473A /* ObjC */ = { + isa = PBXGroup; + children = ( + 805541EE2B19780200529F3B /* CSSharedConstants.h */, + 805541EF2B19780200529F3B /* CSSharedConstants.m */, + 805231B32B38DCB7009C473A /* Units */, + 80592C0618924B0F0082D4E1 /* CSIngredient.h */, + 80592C0718924B0F0082D4E1 /* CSIngredient.m */, + ); + path = ObjC; + sourceTree = ""; + }; + 805231B12B38D566009C473A /* ObjC */ = { + isa = PBXGroup; + children = ( + 80592C0C189268D60082D4E1 /* CSScaleView.h */, + 80592C0D189268D60082D4E1 /* CSScaleView.m */, + 8087757D18C24D1F00A94031 /* CSScaleVCInternals.h */, 80A614DC18BF3D4100A205BB /* CSGlassView.h */, 80A614DD18BF3D4100A205BB /* CSGlassView.m */, 93F6CCA518AED6050045400D /* CSScaleVC.h */, 93F6CCA618AED6050045400D /* CSScaleVC.m */, 93F6CCA718AED6050045400D /* CSScaleVC.xib */, - 396E363E24396D5900C2815B /* UnitPickerView.swift */, - 396E3646243A3BDC00C2815B /* UnitPickerDelegate.swift */, 80592C1518939E030082D4E1 /* CSGradientView.h */, 80592C1618939E030082D4E1 /* CSGradientView.m */, - 396E3642243976A000C2815B /* GradientView.swift */, - 80592C0F18926AF30082D4E1 /* ScaleView */, - 8087757D18C24D1F00A94031 /* CSScaleVCInternals.h */, - 396E363C2439684E00C2815B /* CenterLineView.swift */, - 774EBD7C2442E475002A59AC /* ScaleViewController.swift */, ); - name = Scales; + path = ObjC; sourceTree = ""; }; - 80592C0218924AEA0082D4E1 /* Model */ = { + 805231B22B38D6B5009C473A /* ObjC */ = { isa = PBXGroup; children = ( - 93D0993B18977D320014DB3E /* Units */, - 80592C0318924B010082D4E1 /* CSIngredients.h */, - 80592C0418924B010082D4E1 /* CSIngredients.m */, - 80592C0618924B0F0082D4E1 /* CSIngredient.h */, - 80592C0718924B0F0082D4E1 /* CSIngredient.m */, - 80592C091892541D0082D4E1 /* CSIngredientGroup.h */, - 80592C0A1892541D0082D4E1 /* CSIngredientGroup.m */, - 80433B2C1BB26C1A006B7A85 /* CSRecentsIngredientGroup.h */, - 80433B2D1BB26C1A006B7A85 /* CSRecentsIngredientGroup.m */, - DC82F26018AC25B9004FD733 /* CSFilteredIngredientGroup.h */, - DC82F26118AC25B9004FD733 /* CSFilteredIngredientGroup.m */, - DC82F26318AC2642004FD733 /* CSIngredientGroupInternals.h */, - ); - name = Model; + 9358BD6B1891CAAA00A99D51 /* CSConversionVC.h */, + 9358BD6C1891CAAA00A99D51 /* CSConversionVC.m */, + 9358BD6D1891CAAA00A99D51 /* CSConversionVC.xib */, + ); + path = ObjC; sourceTree = ""; }; - 80592C0F18926AF30082D4E1 /* ScaleView */ = { + 805231B32B38DCB7009C473A /* Units */ = { isa = PBXGroup; children = ( - 80592C0C189268D60082D4E1 /* CSScaleView.h */, - 80592C0D189268D60082D4E1 /* CSScaleView.m */, + 93D09932189766080014DB3E /* CSUnit.h */, + 93D09933189766080014DB3E /* CSUnit.m */, + 931AF96B18C2BAE50018AA8B /* CSUnitCollection.h */, + 931AF96C18C2BAE50018AA8B /* CSUnitCollection.m */, ); - name = ScaleView; + path = Units; sourceTree = ""; }; - 80629C5618D4AE8A00106131 /* Products */ = { + 805541E92B18709F00529F3B /* IngredientGroup */ = { isa = PBXGroup; children = ( - 80629C5B18D4AE8A00106131 /* libVGAnalytics.a */, - 80629C5D18D4AE8A00106131 /* VGAnalyticsTests.xctest */, + 80475F002B12E257009644DF /* StoredIngredientGroup.swift */, + 805541E52B16BD5300529F3B /* IngredientGroup.swift */, + 805541E72B16C82C00529F3B /* RecentsIngredientGroup.swift */, ); - name = Products; + path = IngredientGroup; sourceTree = ""; }; - 807C0B681BB3DD04007D2C03 /* Ingredient List UI */ = { + 805541FE2B1C0FD200529F3B /* Design Language */ = { isa = PBXGroup; children = ( - 9368096E1891059700655B89 /* CSIngredientListVC.h */, - 9368096F1891059700655B89 /* CSIngredientListVC.m */, - 807C0B6A1BB3DD4D007D2C03 /* CSIngredientListViewCell.h */, - 807C0B6B1BB3DD4D007D2C03 /* CSIngredientListViewCell.m */, + 805541FC2B1C0DC000529F3B /* CSColor.swift */, + B7BCA2A2180524A800CF7588 /* Assets.xcassets */, + 805B8B0E2B1C71BE00FF0BBF /* CSTextStyle.swift */, ); - name = "Ingredient List UI"; + path = "Design Language"; sourceTree = ""; }; - 93D0993B18977D320014DB3E /* Units */ = { + 80629C5618D4AE8A00106131 /* Products */ = { isa = PBXGroup; children = ( - 93D09932189766080014DB3E /* CSUnit.h */, - 93D09933189766080014DB3E /* CSUnit.m */, - 931AF96B18C2BAE50018AA8B /* CSUnitCollection.h */, - 931AF96C18C2BAE50018AA8B /* CSUnitCollection.m */, + 80629C5B18D4AE8A00106131 /* libVGAnalytics.a */, + 80629C5D18D4AE8A00106131 /* VGAnalyticsTests.xctest */, ); - name = Units; + name = Products; sourceTree = ""; }; B7BCA284180524A800CF7588 = { @@ -341,19 +345,13 @@ B7BCA296180524A800CF7588 /* CookSmart */ = { isa = PBXGroup; children = ( - 39C3CE7F244BE1B100359EDF /* Scale View */, + 80475EF32B12AAC9009644DF /* UI */, + 80475EFD2B12E01A009644DF /* Model */, + 805541FE2B1C0FD200529F3B /* Design Language */, + 392D8642242FD0CA002064D6 /* Misc Utils */, 39C3CE562442775E00359EDF /* AppDelegate.swift */, 39C3CE592442776D00359EDF /* SceneDelegate.swift */, - 392D8642242FD0CA002064D6 /* Core */, - 39C3CE612442A1A300359EDF /* Edit Ingredient */, 80433B351BB2731B006B7A85 /* Launch Screen.storyboard */, - 9358BD6B1891CAAA00A99D51 /* CSConversionVC.h */, - 9358BD6C1891CAAA00A99D51 /* CSConversionVC.m */, - 9358BD6D1891CAAA00A99D51 /* CSConversionVC.xib */, - 8000355C18B3CBF50032BF91 /* Scales */, - 80592C0218924AEA0082D4E1 /* Model */, - 807C0B681BB3DD04007D2C03 /* Ingredient List UI */, - B7BCA2A2180524A800CF7588 /* Images.xcassets */, B7BCA297180524A800CF7588 /* Supporting Files */, ); path = CookSmart; @@ -499,7 +497,7 @@ 931AF96A18C2B9C90018AA8B /* VolumeUnits.plist in Resources */, B7BCA29B180524A800CF7588 /* InfoPlist.strings in Resources */, B7BCA2C01805284A00CF7588 /* Ingredients.plist in Resources */, - B7BCA2A3180524A800CF7588 /* Images.xcassets in Resources */, + B7BCA2A3180524A800CF7588 /* Assets.xcassets in Resources */, 80433B361BB2731B006B7A85 /* Launch Screen.storyboard in Resources */, 93F6CCA918AED6050045400D /* CSScaleVC.xib in Resources */, ); @@ -597,37 +595,35 @@ files = ( 774EBD6524426CC2002A59AC /* Double+StringUtils.swift in Sources */, 396E363F24396D5900C2815B /* UnitPickerView.swift in Sources */, + 80475F012B12E257009644DF /* StoredIngredientGroup.swift in Sources */, + 805541E62B16BD5300529F3B /* IngredientGroup.swift in Sources */, + 80475EFF2B12E105009644DF /* Ingredient.swift in Sources */, + 805541FD2B1C0DC000529F3B /* CSColor.swift in Sources */, 77479AEA242FB9C8000CFB0E /* ScaleTile.swift in Sources */, - 773C97E2244BE5D900208351 /* Button.swift in Sources */, - 774EBD802442EC26002A59AC /* UIView+Constraints.swift in Sources */, - 392D8645242FD0FF002064D6 /* Colors.swift in Sources */, + 80475EF62B12AB00009644DF /* IngredientListView.swift in Sources */, 80A614DE18BF3D4100A205BB /* CSGlassView.m in Sources */, - 774EBD792442A1CE002A59AC /* Fraction.swift in Sources */, + 80475F052B130C41009644DF /* Assertions.swift in Sources */, 93F6CCA818AED6050045400D /* CSScaleVC.m in Sources */, + 805B8B0F2B1C71BE00FF0BBF /* CSTextStyle.swift in Sources */, 931AF96D18C2BAE50018AA8B /* CSUnitCollection.m in Sources */, 774EBD7D2442E475002A59AC /* ScaleViewController.swift in Sources */, + 805231B52B38F397009C473A /* UIView+Constraints.swift in Sources */, + 805541F52B1AC45700529F3B /* Density.swift in Sources */, + 80475F032B12E33B009644DF /* IngredientsStore.swift in Sources */, 39C3CE582442775E00359EDF /* AppDelegate.swift in Sources */, 80592C0818924B0F0082D4E1 /* CSIngredient.m in Sources */, 80592C0E189268D60082D4E1 /* CSScaleView.m in Sources */, 39C3CE5A2442776D00359EDF /* SceneDelegate.swift in Sources */, - 80592C0B1892541D0082D4E1 /* CSIngredientGroup.m in Sources */, - 77479AF924303D86000CFB0E /* Fonts.swift in Sources */, 80592C1718939E030082D4E1 /* CSGradientView.m in Sources */, 39C3CE642442A1C000359EDF /* EditIngredientViewController.swift in Sources */, 9358BD6E1891CAAA00A99D51 /* CSConversionVC.m in Sources */, - 80592C0518924B010082D4E1 /* CSIngredients.m in Sources */, 77F41932243950CB0055053E /* ScaleScrollView.swift in Sources */, 39C3CE82244BE22100359EDF /* ScalesView.swift in Sources */, - 807C0B6C1BB3DD4D007D2C03 /* CSIngredientListViewCell.m in Sources */, - DC82F26218AC25B9004FD733 /* CSFilteredIngredientGroup.m in Sources */, - 396E3647243A3BDC00C2815B /* UnitPickerDelegate.swift in Sources */, - 773C97E8244BF14A00208351 /* Label.swift in Sources */, 93D09934189766080014DB3E /* CSUnit.m in Sources */, - 773C97E5244BF0BF00208351 /* StyledView.swift in Sources */, - 80433B2E1BB26C1A006B7A85 /* CSRecentsIngredientGroup.m in Sources */, + 805541F02B19780200529F3B /* CSSharedConstants.m in Sources */, 396E3643243976A000C2815B /* GradientView.swift in Sources */, 396E363D2439684E00C2815B /* CenterLineView.swift in Sources */, - 936809701891059700655B89 /* CSIngredientListVC.m in Sources */, + 805541E82B16C82C00529F3B /* RecentsIngredientGroup.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -674,7 +670,6 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = CakeTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -709,7 +704,6 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = CakeTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -781,7 +775,7 @@ "\"$(TARGET_BUILD_DIR)/usr/local/lib/include\"", "\"$(OBJROOT)/UninstalledProducts/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; ONLY_ACTIVE_ARCH = YES; PROVISIONING_PROFILE = "11064EA0-77EA-4B3B-BB70-727CC87F5A87"; SDKROOT = iphoneos; @@ -836,7 +830,7 @@ "\"$(TARGET_BUILD_DIR)/usr/local/lib/include\"", "\"$(OBJROOT)/UninstalledProducts/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; PROVISIONING_PROFILE = "11064EA0-77EA-4B3B-BB70-727CC87F5A87"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/CookSmart/CookSmart/CSConversionVC.m b/CookSmart/CookSmart/CSConversionVC.m deleted file mode 100644 index 5e21a19..0000000 --- a/CookSmart/CookSmart/CSConversionVC.m +++ /dev/null @@ -1,274 +0,0 @@ -// -// CSConversionVC.m -// CookSmart -// -// Created by Olga Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSConversionVC.h" -#import "CSIngredients.h" -#import "CSIngredientListVC.h" -#import "CSIngredientGroup.h" -#import "CSIngredient.h" -#import "CSScaleView.h" -#import "CSUnit.h" -#import "CSUnitCollection.h" -#import "CSScaleVC.h" -#import "cake-Swift.h" - -#define CHOOSE_UNITS_TEXT @"Choose Units" - -@interface CSConversionVC () - -@property (nonatomic, readwrite, assign) NSUInteger ingredientIndex; -@property (weak, nonatomic) IBOutlet UIScrollView *ingredientPickerScrollView; - -@property (strong, nonatomic) IBOutlet CSScaleVC* scaleVC; - -@end - -@implementation CSConversionVC - -- (id)initWithIngredientGroupIndex:(NSUInteger)ingredientGroupIndex ingredientIndex:(NSUInteger)ingredientIndex -{ - self = [super initWithNibName:@"CSConversionVC" bundle:nil]; - if (self) - { - self.ingredientIndex = [[CSIngredients sharedInstance] flattenedIngredientIndexForGroupIndex:ingredientGroupIndex ingredientIndex:ingredientIndex]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(ingredientDeleted:) - name:INGREDIENT_DELETE_NOTIFICATION_NAME - object:nil]; - } - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - View Lifecycle Management - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - [self addChildViewController:self.scaleVC]; - [self.view addSubview:self.scaleVC.view]; - self.scaleVC.view.translatesAutoresizingMaskIntoConstraints = NO; - self.scaleVC.delegate = self; - NSLayoutConstraint* bottom = [NSLayoutConstraint constraintWithItem:self.scaleVC.view - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeBottom - multiplier:1.0 - constant:0]; - NSLayoutConstraint* left = [NSLayoutConstraint constraintWithItem:self.scaleVC.view - attribute:NSLayoutAttributeLeft - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeLeft - multiplier:1.0 - constant:0]; - NSLayoutConstraint* right = [NSLayoutConstraint constraintWithItem:self.scaleVC.view - attribute:NSLayoutAttributeRight - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeRight - multiplier:1.0 - constant:0]; - NSLayoutConstraint* top = [NSLayoutConstraint constraintWithItem:self.scaleVC.view - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.ingredientPickerScrollView - attribute:NSLayoutAttributeBottom - multiplier:1.0 - constant:10]; - [self.view addConstraints:@[bottom, left, right, top]]; - self.ingredientPickerScrollView.scrollsToTop = NO; - self.ingredientPickerScrollView.showsHorizontalScrollIndicator = NO; - self.ingredientPickerScrollView.showsVerticalScrollIndicator = NO; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - // When we first appear, always select the very first ingredient – the one most recently selected. - [self selectIngredientAtIndex:self.ingredientIndex]; - logViewChange(@"conversion", [self.scaleVC analyticsAttributes]); -} - -- (void)viewDidDisappear:(BOOL)animated -{ - [super viewDidDisappear:animated]; - [self markCurrentIngredientAccess]; -} - -- (void)markCurrentIngredientAccess -{ - if (self.isViewLoaded && self.view.window != nil) { - CSIngredient *ingredient = [[CSIngredients sharedInstance] ingredientAtFlattenedIngredientIndex:self.ingredientIndex]; - [ingredient markAccess]; - } -} - -#pragma mark - Ingredient Picker - -- (NSString *)nameForIngredientAtXOrigin:(CGFloat)xOrigin -{ - NSUInteger indexOfIngredient = (NSUInteger) (xOrigin/self.ingredientPickerScrollView.bounds.size.width); - NSString *nameOfIngredient = nil; - if (indexOfIngredient < [[CSIngredients sharedInstance] flattenedCountOfIngredients]) - { - nameOfIngredient = [[[CSIngredients sharedInstance] ingredientAtFlattenedIngredientIndex:indexOfIngredient] name]; - } - return nameOfIngredient; -} - -- (void)refreshIngredientNameUI -{ - self.ingredientPickerScrollView.contentSize = CGSizeMake(self.ingredientPickerScrollView.bounds.size.width*[[CSIngredients sharedInstance] flattenedCountOfIngredients], - self.ingredientPickerScrollView.bounds.size.height); - for (UIView *subview in self.ingredientPickerScrollView.subviews) - { - [subview removeFromSuperview]; - } - CGFloat initialXOffset = self.ingredientPickerScrollView.bounds.size.width*self.ingredientIndex; - for (CGFloat xOrigin = initialXOffset; xOrigin <= initialXOffset + 2*self.ingredientPickerScrollView.bounds.size.width; xOrigin += self.ingredientPickerScrollView.bounds.size.width) - { - UIButton *ingredientButton = [UIButton buttonWithType:UIButtonTypeSystem]; - ingredientButton.frame = CGRectMake(xOrigin, 0, self.ingredientPickerScrollView.bounds.size.width, self.ingredientPickerScrollView.bounds.size.height); - [ingredientButton setTitle:[self nameForIngredientAtXOrigin:xOrigin] forState:UIControlStateNormal]; - ingredientButton.titleLabel.font = [UIFont fontWithName:@"AvenirNext-Medium" size:MAJOR_BUTTON_FONT_SIZE]; - [ingredientButton setTitleColor:RED_LINE_COLOR forState:UIControlStateNormal]; - [ingredientButton setTitleColor:[UIColor blackColor] forState:UIControlStateDisabled]; - [ingredientButton addTarget:self action:@selector(handleIngredientTap:) forControlEvents:UIControlEventTouchUpInside]; - [self.ingredientPickerScrollView addSubview:ingredientButton]; - } - self.ingredientPickerScrollView.contentOffset = CGPointMake(initialXOffset, 0); -} - -- (void)handleIngredientTap:(id)sender -{ - CSIngredientListVC* ingrListVC = [[CSIngredientListVC alloc] initWithDelegate:self]; - UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:ingrListVC]; - nav.modalPresentationStyle = UIModalPresentationOverFullScreen; - [self presentViewController:nav animated:YES completion:nil]; -} - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView -{ - CSAssert(scrollView == self.ingredientPickerScrollView, @"conversion_vc_wrong_scrollview_delegate", - @"CSConversionVC doesn't expect to be delegate of a scrollview other than its ingredientPickerScrollView"); - CGFloat minVisibleX = self.ingredientPickerScrollView.contentOffset.x; - CGFloat maxVisibleX = minVisibleX + self.ingredientPickerScrollView.bounds.size.width; - NSUInteger numSubviews = self.ingredientPickerScrollView.subviews.count; - CGFloat contentSize = self.ingredientPickerScrollView.contentSize.width; - for (UIButton *button in self.ingredientPickerScrollView.subviews) - { - if (button.frame.origin.x + button.bounds.size.width < minVisibleX && - button.frame.origin.x + (numSubviews + 1)*button.bounds.size.width <= contentSize && - button.frame.origin.x + numSubviews*button.bounds.size.width) - { - button.frame = CGRectMake(button.frame.origin.x + numSubviews*button.bounds.size.width, 0, button.frame.size.width, button.frame.size.height); - [button setTitle:[self nameForIngredientAtXOrigin:button.frame.origin.x] forState:UIControlStateNormal]; - } - if (button.frame.origin.x > maxVisibleX && - button.frame.origin.x - numSubviews*button.bounds.size.width >= 0 && - button.frame.origin.x + (1 - numSubviews)*button.bounds.size.width >= minVisibleX) - { - button.frame = CGRectMake(button.frame.origin.x - numSubviews*button.bounds.size.width, 0, button.frame.size.width, button.frame.size.height); - [button setTitle:[self nameForIngredientAtXOrigin:button.frame.origin.x] forState:UIControlStateNormal]; - } - } - - CGFloat distanceToSnap = remainder(self.ingredientPickerScrollView.contentOffset.x, self.ingredientPickerScrollView.bounds.size.width); - CGFloat distanceToMiddle = (self.ingredientPickerScrollView.bounds.size.width/2) - fabs(distanceToSnap); - CGFloat scaleViewAlpha = distanceToMiddle/(self.ingredientPickerScrollView.bounds.size.width/2); - [self.scaleVC setScalesAlpha:scaleViewAlpha]; - - NSUInteger projectedIndex = (int)round(self.ingredientPickerScrollView.contentOffset.x/self.ingredientPickerScrollView.bounds.size.width); - projectedIndex = MIN(MAX(0, projectedIndex), self.ingredientPickerScrollView.contentSize.width/self.ingredientPickerScrollView.bounds.size.width - 1); - if (projectedIndex != self.ingredientIndex) - { - self.ingredientIndex = projectedIndex; - [self refreshScalesWithCurrentIngredient]; - [self markCurrentIngredientAccess]; - logUserAction(@"ingredient_switch", [self.scaleVC analyticsAttributes]); - } -} - -#pragma mark - CSIngredientListVCDelegate - -- (void)ingredientListVC:(CSIngredientListVC *)listVC selectedIngredientGroup:(NSUInteger)ingredientGroupIndex ingredientIndex:(NSUInteger)index -{ - CSIngredientGroup *ingredientGroup = [[CSIngredients sharedInstance] ingredientGroupAtIndex:ingredientGroupIndex]; - CSIngredient *ingredient = [ingredientGroup ingredientAtIndex:index]; - logUserAction(@"ingredient_select", @{ - @"ingredient_group_name" : ingredientGroup.name, - @"ingredient_name" : ingredient.name, - @"ingredient_density" : [NSNumber numberWithFloat:ingredient.density], - }); - [self selectIngredientAtIndex:[[CSIngredients sharedInstance] flattenedIngredientIndexForGroupIndex:ingredientGroupIndex ingredientIndex:index]]; -} - -- (void)selectIngredientAtIndex:(NSUInteger)ingredientIndex -{ - self.ingredientIndex = ingredientIndex; - [self refreshIngredientNameUI]; - [self refreshScalesWithCurrentIngredient]; - [self markCurrentIngredientAccess]; -} - -- (void)refreshScalesWithCurrentIngredient -{ - self.scaleVC.ingredient = [[CSIngredients sharedInstance] ingredientAtFlattenedIngredientIndex:self.ingredientIndex]; -} - -#pragma mark - Notifications - -- (void)ingredientDeleted:(NSNotification *)notification -{ - // When an ingredient is deleted, our index into the ingredient group might change. - // In the future we might want to put a better solution for this, but for now, we'll - // just select the very first ingredient of the very first ingredient group and be done - // with it. - [self selectIngredientAtIndex:0]; -} - -#pragma mark - scaleVC delegate methods - -- (void)scaleVCDidBeginChangingUnits:(CSScaleVC*)scaleVC -{ - [self iterateOverIngredientButtons:^(UIButton *ingredientButton) { - ingredientButton.enabled = NO; - [ingredientButton setTitle:CHOOSE_UNITS_TEXT forState:UIControlStateNormal]; - }]; - [self.ingredientPickerScrollView setScrollEnabled:NO]; -} - -- (void)scaleVCDidFinishChangingUnits:(CSScaleVC *)scaleVC -{ - [self iterateOverIngredientButtons:^(UIButton *ingredientButton) { - ingredientButton.enabled = YES; - [ingredientButton setTitle:[self nameForIngredientAtXOrigin:ingredientButton.frame.origin.x] forState:UIControlStateNormal]; - }]; - [self.ingredientPickerScrollView setScrollEnabled:YES]; -} - -- (void)iterateOverIngredientButtons:(void (^)(UIButton *))work -{ - for (UIButton *ingredientButton in self.ingredientPickerScrollView.subviews) - { - if ([ingredientButton isKindOfClass:[UIButton class]]) - { - work(ingredientButton); - } - } -} - -@end diff --git a/CookSmart/CookSmart/CSFilteredIngredientGroup.h b/CookSmart/CookSmart/CSFilteredIngredientGroup.h deleted file mode 100644 index a5f19db..0000000 --- a/CookSmart/CookSmart/CSFilteredIngredientGroup.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// CSFilteredIngredientGroup.h -// CookSmart -// -// Created by Vova Galchenko on 2/12/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSIngredientGroup.h" - -@interface CSFilteredIngredientGroup : CSIngredientGroup - -+ (CSFilteredIngredientGroup *)filteredIngredientGroupWithIngredients:(NSArray *)ingredients name:(NSString *)groupName originalIngredientGroup:(CSIngredientGroup *)originalIngredientGroup; - -@property (nonatomic, readonly) CSIngredientGroup *originalIngredientGroup; - -@end diff --git a/CookSmart/CookSmart/CSFilteredIngredientGroup.m b/CookSmart/CookSmart/CSFilteredIngredientGroup.m deleted file mode 100644 index 86d6cfb..0000000 --- a/CookSmart/CookSmart/CSFilteredIngredientGroup.m +++ /dev/null @@ -1,43 +0,0 @@ -// -// CSFilteredIngredientGroup.m -// CookSmart -// -// Created by Vova Galchenko on 2/12/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSFilteredIngredientGroup.h" -#import "CSIngredientGroupInternals.h" - -@interface CSFilteredIngredientGroup() - -@property (nonatomic, readwrite, strong) CSIngredientGroup *originalIngredientGroup; - -@end - -@implementation CSFilteredIngredientGroup - -- (id)initWithIngredients:(NSArray *)ingredients name:(NSString *)groupName originalIngredientGroup:(CSIngredientGroup *)originalIngredientGroup -{ - if (self = [super init]) - { - self.ingredients = [NSMutableArray arrayWithArray:ingredients]; - self.name = groupName; - self.originalIngredientGroup = originalIngredientGroup; - self.synthetic = NO; - } - return self; -} - -+ (CSFilteredIngredientGroup *)filteredIngredientGroupWithIngredients:(NSArray *)ingredients name:(NSString *)groupName originalIngredientGroup:(CSIngredientGroup *)originalIngredientGroup -{ - return [[self alloc] initWithIngredients:ingredients name:groupName originalIngredientGroup:originalIngredientGroup]; -} - -- (void)deleteIngredient:(CSIngredient *)ingredient -{ - [super deleteIngredient:ingredient]; - [self.originalIngredientGroup deleteIngredient:ingredient]; -} - -@end diff --git a/CookSmart/CookSmart/CSGlassView.m b/CookSmart/CookSmart/CSGlassView.m deleted file mode 100644 index 2359220..0000000 --- a/CookSmart/CookSmart/CSGlassView.m +++ /dev/null @@ -1,99 +0,0 @@ -// -// CSMagnifyingView.m -// CookSmart -// -// Created by Vova Galchenko on 2/27/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSGlassView.h" - -#define SHADOW_SIZE 5 -#define MAGNIFYING_FACTOR 1.1 - -@interface CSGlassView() - -@property (nonatomic, weak) UIView *magnifiedView; -@property (nonatomic, weak) UIView *glassening; -@property (nonatomic, weak) CADisplayLink *displayLink; - -@end - -@implementation CSGlassView - -- (id)initWithCoder:(NSCoder *)aDecoder -{ - self = [super initWithCoder:aDecoder]; - if (self) { - self.opaque = YES; - self.layer.shadowColor = [[UIColor blackColor] CGColor]; - self.layer.shadowOffset = CGSizeMake(0, SHADOW_SIZE); - self.layer.shadowOpacity = 0.075; - - UIView *glassening = [[UIView alloc] init]; - [self addSubview:self.glassening]; - self.glassening = glassening; - self.glassening.opaque = NO; - self.glassening.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.5 alpha:0.025]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(observeSceneLifecycle:) - name:UISceneWillEnterForegroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(observeSceneLifecycle:) - name:UISceneDidEnterBackgroundNotification - object:nil]; - } - return self; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - self.glassening.frame = self.bounds; -} - -- (void)observeSceneLifecycle: (NSNotification *)sceneLifecycleChangeNotification -{ - if (((UIScene *)[sceneLifecycleChangeNotification object]) == self.window.windowScene) { - if ([[sceneLifecycleChangeNotification name] isEqualToString:UISceneDidEnterBackgroundNotification]) { - [self.displayLink invalidate]; - } else if ([[sceneLifecycleChangeNotification name] isEqualToString:UISceneWillEnterForegroundNotification]) { - CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(refreshMagnifiedView)]; - [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - self.displayLink = displayLink; - } - } -} - - -- (void)refreshMagnifiedView -{ - [self.magnifiedView removeFromSuperview]; - CGPoint imageUnderGlassOrigin = [self.viewToMagnify convertPoint:CGPointMake(0, 0) fromView:self]; - CGFloat widthIncrease = (MAGNIFYING_FACTOR - 1)*self.bounds.size.width; - CGFloat heightIncrease = (MAGNIFYING_FACTOR - 1)*self.bounds.size.height; - - UIView *magnifiedView = [self.viewToMagnify resizableSnapshotViewFromRect:CGRectMake( - imageUnderGlassOrigin.x + widthIncrease/2, - imageUnderGlassOrigin.y + heightIncrease/2, - self.bounds.size.width - widthIncrease, - self.bounds.size.height - heightIncrease - ) - afterScreenUpdates:NO - withCapInsets:UIEdgeInsetsZero]; - - - magnifiedView.frame = self.bounds; - [self insertSubview:magnifiedView atIndex:0]; - self.magnifiedView = magnifiedView; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - // Pass through all events - return NO; -} - -@end diff --git a/CookSmart/CookSmart/CSIngredientGroup.h b/CookSmart/CookSmart/CSIngredientGroup.h deleted file mode 100644 index 64cd105..0000000 --- a/CookSmart/CookSmart/CSIngredientGroup.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// CSIngredientGroup.h -// CookSmart -// -// Created by Vova Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import - -@class CSIngredient; - -@interface CSIngredientGroup : NSObject - -+ (CSIngredientGroup *)ingredientGroupWithDictionary:(NSDictionary *)groupDictionary; -- (CSIngredient *)ingredientAtIndex:(NSUInteger)ingredientIndex; -- (NSUInteger)indexOfIngredient:(CSIngredient *)ingredient; -- (NSUInteger)countOfIngredients; - -- (void)deleteIngredient:(CSIngredient *)ingredient; -- (void)addIngredient:(CSIngredient*)ingredient; -- (void)replaceIngredientAtIndex:(NSUInteger)index withIngredient:(CSIngredient*)ingredient; -- (NSDictionary *)dictionary; - -@property (nonatomic, readonly) NSString *name; -@property (nonatomic, readonly, getter = isSynthetic) BOOL synthetic; - -@end diff --git a/CookSmart/CookSmart/CSIngredientGroup.m b/CookSmart/CookSmart/CSIngredientGroup.m deleted file mode 100644 index f09b80d..0000000 --- a/CookSmart/CookSmart/CSIngredientGroup.m +++ /dev/null @@ -1,132 +0,0 @@ -// -// CSIngredientGroup.m -// CookSmart -// -// Created by Vova Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSIngredientGroup.h" -#import "CSIngredient.h" -#import "CSIngredientGroupInternals.h" - -@interface CSIngredientGroup () -{ - unsigned long _version; -} - -@end - -@implementation CSIngredientGroup - -- (id)initWithDictionary:(NSDictionary *)groupDictionary -{ - if (self = [super init]) - { - NSArray *dictionaryKeys = [groupDictionary allKeys]; - CSAssert(dictionaryKeys.count == 1, @"ingredient_group_dictionary_consistency", @"A group dictionary should contain exactly one key: the name of the group."); - self.name = dictionaryKeys[0]; - NSMutableArray *tmpIngredients = [NSMutableArray array]; - for (NSDictionary *ingredientDictionary in [groupDictionary objectForKey:self.name]) - { - [tmpIngredients addObject:[CSIngredient ingredientWithDictionary:ingredientDictionary]]; - } - self.ingredients = tmpIngredients; - self.synthetic = NO; - } - return self; -} - -+ (CSIngredientGroup *)ingredientGroupWithDictionary:(NSDictionary *)groupDictionary -{ - return [[self alloc] initWithDictionary:groupDictionary]; -} - -- (CSIngredient *)ingredientAtIndex:(NSUInteger)ingredientIndex -{ - return [self.ingredients objectAtIndex:ingredientIndex]; -} - -- (NSUInteger)indexOfIngredient:(CSIngredient *)ingredient -{ - return [self.ingredients indexOfObject:ingredient]; -} - -- (NSUInteger)countOfIngredients -{ - return self.ingredients.count; -} - -- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)len -{ - NSUInteger count = 0; - unsigned long countOfItemsAlreadyEnumerated = state->state; - - if(countOfItemsAlreadyEnumerated == 0) - { - // We are not tracking mutations, so we'll set state->mutationsPtr to point - // into one of our extra values, since these values are not otherwise used - // by the protocol. - // If your class was mutable, you may choose to use an internal variable that - // is updated when the class is mutated. - // state->mutationsPtr MUST NOT be NULL and SHOULD NOT be set to self. - state->mutationsPtr = &_version; - } - - if(countOfItemsAlreadyEnumerated < [self.ingredients count]) - { - state->itemsPtr = buffer; - while((countOfItemsAlreadyEnumerated < [self.ingredients count]) && (count < len)) - { - // Add the item for the next index to stackbuf. - // - // If you choose not to use ARC, you do not need to retain+autorelease the - // objects placed into stackbuf. It is the caller's responsibility to ensure we - // are not deallocated during enumeration. - buffer[count] = self.ingredients[countOfItemsAlreadyEnumerated]; - countOfItemsAlreadyEnumerated++; - - // We must return how many items are in state->itemsPtr. - count++; - } - } - else - count = 0; - - // Update state->state with the new value of countOfItemsAlreadyEnumerated so that it is - // preserved for the next invocation. - state->state = countOfItemsAlreadyEnumerated; - return count; -} - -- (void)deleteIngredient:(CSIngredient *)ingredient -{ - _version++; - [self.ingredients removeObject:ingredient]; -} - -- (void)addIngredient:(CSIngredient *)ingredient -{ - _version++; - [self.ingredients addObject:ingredient]; -} - -- (void)replaceIngredientAtIndex:(NSUInteger)index withIngredient:(CSIngredient*)ingredient -{ - _version++; - [self.ingredients replaceObjectAtIndex:index withObject:ingredient]; -} - -- (NSDictionary *)dictionary -{ - NSMutableArray *ingredients = [NSMutableArray arrayWithCapacity:self.ingredients.count]; - for (CSIngredient *ingredient in self.ingredients) - { - [ingredients addObject:[ingredient dictionary]]; - } - return @{ - self.name : ingredients - }; -} - -@end diff --git a/CookSmart/CookSmart/CSIngredientGroupInternals.h b/CookSmart/CookSmart/CSIngredientGroupInternals.h deleted file mode 100644 index c8819a7..0000000 --- a/CookSmart/CookSmart/CSIngredientGroupInternals.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// CSIngredientGroupInternals.h -// CookSmart -// -// Created by Vova Galchenko on 2/12/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#ifndef CookSmart_CSIngredientGroupInternals_h -#define CookSmart_CSIngredientGroupInternals_h - -@interface CSIngredientGroup() - -@property (nonatomic, readwrite, strong) NSString *name; -@property (nonatomic, readwrite, strong) NSMutableArray *ingredients; -@property (nonatomic, readwrite, assign, getter = isSynthetic) BOOL synthetic; - -@end - -#endif diff --git a/CookSmart/CookSmart/CSIngredientListVC.h b/CookSmart/CookSmart/CSIngredientListVC.h deleted file mode 100644 index 7f8cf28..0000000 --- a/CookSmart/CookSmart/CSIngredientListVC.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// CSIngredientListVC.h -// CookSmart -// -// Created by Olga Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import - -@class CSIngredientGroup; -@protocol CSIngredientListVCDelegate; - -@interface CSIngredientListVC : UITableViewController - -- (id)initWithDelegate:(id)delegate; -- (void)detailButtonTapped:(id)sender; - -@end - -@protocol CSIngredientListVCDelegate - -- (void)ingredientListVC:(CSIngredientListVC *)listVC - selectedIngredientGroup:(NSUInteger)ingredientGroupIndex - ingredientIndex:(NSUInteger)index; - -@end \ No newline at end of file diff --git a/CookSmart/CookSmart/CSIngredientListVC.m b/CookSmart/CookSmart/CSIngredientListVC.m deleted file mode 100644 index 9a644bd..0000000 --- a/CookSmart/CookSmart/CSIngredientListVC.m +++ /dev/null @@ -1,345 +0,0 @@ -// -// CSIngredientListVC.m -// CookSmart -// -// Created by Olga Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSIngredientListVC.h" -#import "CSIngredients.h" -#import "CSFilteredIngredientGroup.h" -#import "CSIngredient.h" -#import "CSIngredientListViewCell.h" -#import "CSRecentsIngredientGroup.h" -#import "cake-Swift.h" - -@interface CSIngredientListVC () - -@property (nonatomic, readwrite, weak) iddelegate; -@property (nonatomic, strong) UISearchBar* searchBar; -@property (nonatomic, strong) UIButton* resetToDefaults; -@property (nonatomic, strong) CSIngredients* filteredIngredients; - -@end - -@implementation CSIngredientListVC - -static NSString* CellIdentifier = @"Cell"; -static const NSUInteger ResetToDefaultsHeight = 40; - -- (id)initWithDelegate:(id)delegate -{ - self = [super initWithStyle:UITableViewStylePlain]; - if (self) - { - self.delegate = delegate; - self.title = @"Ingredients"; - } - return self; -} - -- (void)dealloc -{ - self.searchBar.delegate = nil; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - [self.tableView registerClass:[CSIngredientListViewCell class] forCellReuseIdentifier:CellIdentifier]; - - UIBarButtonItem* closeItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"Close"] style:UIBarButtonItemStylePlain target:self action:@selector(closeIngrList:)]; - self.navigationItem.leftBarButtonItem = closeItem; - [self.navigationItem.leftBarButtonItem setTintColor:RED_LINE_COLOR]; - - UIBarButtonItem* add = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(editIngredient:)]; - add.tintColor = RED_LINE_COLOR; - self.navigationItem.rightBarButtonItem = add; - - UILabel *titleView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)]; - titleView.text = self.title; - titleView.font = [UIFont fontWithName:@"AvenirNext-Medium" size:20]; - titleView.textAlignment = NSTextAlignmentCenter; - self.navigationItem.titleView = titleView; - - self.searchBar = [[UISearchBar alloc] init]; - self.searchBar.searchBarStyle = UISearchBarStyleMinimal; - [self.searchBar sizeToFit]; - self.searchBar.delegate = self; - self.tableView.tableHeaderView = self.searchBar; - - self.resetToDefaults = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, ResetToDefaultsHeight)]; - [self.resetToDefaults addTarget:self action:@selector(resetButtonAction:) forControlEvents:UIControlEventTouchUpInside]; - [self.resetToDefaults setTitle:@"Reset to Defaults" forState:UIControlStateNormal]; - [self.resetToDefaults setTitleColor:BACKGROUND_COLOR forState:UIControlStateNormal]; - [self.resetToDefaults.titleLabel setFont:[UIFont fontWithName:@"AvenirNext-Medium" size:17]]; - self.resetToDefaults.backgroundColor = RED_LINE_COLOR; - [self showHideResetToDefaults]; - - [self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y + self.searchBar.frame.size.height)]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - [self refreshData]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - logViewChange(@"ingredient_list", nil); -} - -#pragma mark - Scroll view delegate - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView -{ - [self.searchBar resignFirstResponder]; -} - -#pragma mark - Table view data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - // Return the number of sections. - return [[self ingredientsToSupplyData] countOfIngredientGroups]; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - // Return the number of rows in the section. - return [[[self ingredientsToSupplyData] ingredientGroupAtIndex:section] countOfIngredients]; -} - -- (NSString*)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - return [[[self ingredientsToSupplyData] ingredientGroupAtIndex:section] name]; -} - -- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section -{ - UILabel *headerLabel = [[UILabel alloc] init]; - headerLabel.text = [@" " stringByAppendingString:[[[self ingredientsToSupplyData] ingredientGroupAtIndex:section] name]]; - headerLabel.font = [UIFont fontWithName:@"AvenirNext-DemiBold" size:15]; - headerLabel.backgroundColor = BACKGROUND_COLOR; - return headerLabel; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - CSIngredientListViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; - CSIngredient *ingredient = [[[self ingredientsToSupplyData] ingredientGroupAtIndex:indexPath.section] ingredientAtIndex:indexPath.row]; - [cell configureForListVC:self ingredient:ingredient]; - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - CSIngredientGroup *selectedIngredientGroup = [[self ingredientsToSupplyData] ingredientGroupAtIndex:indexPath.section]; - CSIngredient *selectedIngredient = [selectedIngredientGroup ingredientAtIndex:indexPath.row]; - if ([selectedIngredientGroup respondsToSelector:@selector(originalIngredientGroup)]) - { - selectedIngredientGroup = [selectedIngredientGroup performSelector:@selector(originalIngredientGroup) withObject:nil]; - } - [self.delegate ingredientListVC:self - selectedIngredientGroup:[[CSIngredients sharedInstance] indexOfIngredientGroup:selectedIngredientGroup] - ingredientIndex:[selectedIngredientGroup indexOfIngredient:selectedIngredient]]; - - [self dismissViewControllerAnimated:YES completion:nil]; -} - -- (void)detailButtonTapped:(id)sender -{ - NSUInteger flattenedIngredientIndex = [(UIButton *)sender tag]; - if (flattenedIngredientIndex != NSNotFound) - [self editIngredient:[[CSIngredients sharedInstance] ingredientAtFlattenedIngredientIndex:flattenedIngredientIndex]]; - else - CSAssertFail(@"detail_button_unfound_ingredient", @"Wasn't able to find the flattened ingredient index for this button."); -} - -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSUInteger numGroups = [[CSIngredients sharedInstance] countOfIngredientGroups]; - CSIngredientGroup *ingrGroup = [[CSIngredients sharedInstance] ingredientGroupAtIndex:indexPath.section]; - // Don't allow deletions from synthetic groups. - return !ingrGroup.isSynthetic && - // Don't let people delete the final ingredient for now. - // It's not clear what we want the UX to be when there aren't any ingredients. - // For now, this is such an edge case that we'll just not support it. - ((numGroups > 1) || (numGroups == 1 && [[[CSIngredients sharedInstance] ingredientGroupAtIndex:0] countOfIngredients] > 1)); -} - -- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (editingStyle == UITableViewCellEditingStyleDelete) - { - CSIngredients *ingredients = [self ingredientsToSupplyData]; - CSRecentsIngredientGroup *beforeChangeRecents = ingredients.recents; - CSIngredientGroup *ingredientGroup = [ingredients ingredientGroupAtIndex:indexPath.section]; - CSIngredient *ingredientToDelete = [ingredientGroup ingredientAtIndex:indexPath.row]; - BOOL needToDeleteFromRecents = beforeChangeRecents && [beforeChangeRecents indexOfIngredient:ingredientToDelete] != NSNotFound; - BOOL deleteSuccess = [ingredients deleteIngredientAtGroupIndex:indexPath.section ingredientIndex:indexPath.row]; - if (deleteSuccess) - { - logUserAction(@"ingredient_delete", [ingredientToDelete dictionaryForAnalytics]); - - [tableView beginUpdates]; - if (ingredientGroup.countOfIngredients == 0) - { - [tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationAutomatic]; - } - else - { - [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; - } - - if (needToDeleteFromRecents) - { - CSRecentsIngredientGroup *afterChangeRecents = ingredients.recents; - if (afterChangeRecents == nil) - { - [tableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic]; - } - else - { - [tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:[beforeChangeRecents indexOfIngredient:ingredientToDelete] inSection:0]] - withRowAnimation:UITableViewRowAnimationAutomatic]; - } - } - [tableView endUpdates]; - } - else - { - logIssue(@"ingredient_delete_fail", [ingredientToDelete dictionaryForAnalytics]); - } - } -} - -- (void)editIngredient:(id)sender -{ - UIViewController *editVC; - if (sender == self.navigationItem.rightBarButtonItem) - { - editVC = [[EditIngredientViewController alloc] initWithIngredient: nil]; - } - else if ([sender isKindOfClass:[CSIngredient class]]) - { - CSIngredient *ingredientToEdit = (CSIngredient *)sender; - editVC = [[EditIngredientViewController alloc] initWithIngredient:ingredientToEdit]; - } - else - { - CSAssertFail(@"edit_ingredient_sender", @"The sender of the editIngredient: message should be rightBarButtonItem."); - } - - [self.navigationController pushViewController:editVC animated:YES]; -} - -#pragma mark - search bar delegate -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - logUserAction(@"ingredient_filter", @{@"search_text" : searchText}); - - NSMutableArray* filteredGroupsArray = [NSMutableArray array]; - - for (CSIngredientGroup* group in [CSIngredients sharedInstance]) - { - if ([group isSynthetic]) { - continue; - } - NSMutableArray* ingredients = [NSMutableArray array]; - for (CSIngredient* ingr in group) - { - if ([ingr.name rangeOfString:searchText options:NSCaseInsensitiveSearch].location != NSNotFound) - { - [ingredients addObject:ingr]; - } - } - if (ingredients.count > 0) - { - CSFilteredIngredientGroup *filteredIngredientGroup = [CSFilteredIngredientGroup filteredIngredientGroupWithIngredients:ingredients name:group.name originalIngredientGroup:group]; - [filteredGroupsArray addObject:filteredIngredientGroup]; - } - } - - self.filteredIngredients = [[CSIngredients alloc] initWithIngredientGroups:filteredGroupsArray synthesizeGroups:NO]; - [self refreshData]; -} - -- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar -{ - logUserAction(@"ingredient_filter_cancel", @{@"search_text" : searchBar.text}); -} - -#pragma mark - reset to defaults -- (void)refreshData -{ - [[CSIngredients sharedInstance] refreshRecents]; - [self.tableView reloadData]; - [self showHideResetToDefaults]; -} - -- (void)showHideResetToDefaults -{ - if ([[NSFileManager defaultManager] contentsEqualAtPath:pathToIngredientsOnDisk() andPath:pathToIngredientsInBundle()]) - self.tableView.tableFooterView = nil; - else - self.tableView.tableFooterView = self.resetToDefaults; -} - -- (void)resetButtonAction:(id)sender -{ - UIAlertController* alertController = [UIAlertController - alertControllerWithTitle:@"Are you sure?" - message:@"Resetting to defaults will remove all your added and edited ingredients." - preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction* cancelAction = [UIAlertAction - actionWithTitle:@"Cancel" - style:UIAlertActionStyleCancel - handler:nil]; - UIAlertAction* resetAction = [UIAlertAction - actionWithTitle:@"Reset" - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * _Nonnull action) { - self.tableView.tableFooterView = nil; - - [[CSIngredients sharedInstance] deleteAllSavedIngredients]; - [self refreshData]; - NSIndexPath* firstCellPath = [NSIndexPath indexPathForRow:0 inSection:0]; - [self.tableView scrollToRowAtIndexPath:firstCellPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; - }]; - [alertController addAction:cancelAction]; - [alertController addAction:resetAction]; - [self presentViewController:alertController animated:YES completion:nil]; -} - -#pragma mark - dismiss self -- (void)closeIngrList:(id)sender -{ - // When closing the ingredient list, always select the very first item – the one most recently looked at. - [self.delegate ingredientListVC:self - selectedIngredientGroup:0 - ingredientIndex:0]; - [self dismissViewControllerAnimated:YES completion:nil]; -} - -#pragma mark - Misc Helpers - -- (CSIngredients *)ingredientsToSupplyData -{ - CSIngredients *ingredients = nil; - if ([self.searchBar.text isEqualToString:@""] || !self.searchBar.text) - { - ingredients = [CSIngredients sharedInstance]; - } - else - { - ingredients = self.filteredIngredients; - } - - return ingredients; -} - -@end diff --git a/CookSmart/CookSmart/CSIngredientListViewCell.h b/CookSmart/CookSmart/CSIngredientListViewCell.h deleted file mode 100644 index 72b815d..0000000 --- a/CookSmart/CookSmart/CSIngredientListViewCell.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// CSIngredientListViewCell.h -// CookSmart -// -// Created by Vova Galchenko on 9/24/15. -// Copyright © 2015 Olga Galchenko. All rights reserved. -// - -#import -@class CSIngredientListVC, CSIngredient; - -@interface CSIngredientListViewCell : UITableViewCell - -- (void)configureForListVC:(CSIngredientListVC *)listVC ingredient:(CSIngredient *)ingredient; - -@end diff --git a/CookSmart/CookSmart/CSIngredientListViewCell.m b/CookSmart/CookSmart/CSIngredientListViewCell.m deleted file mode 100644 index bbb4409..0000000 --- a/CookSmart/CookSmart/CSIngredientListViewCell.m +++ /dev/null @@ -1,43 +0,0 @@ -// -// CSIngredientListViewCell.m -// CookSmart -// -// Created by Vova Galchenko on 9/24/15. -// Copyright © 2015 Olga Galchenko. All rights reserved. -// - -#import "CSIngredientListViewCell.h" -#import "CSIngredientListVC.h" -#import "CSIngredient.h" -#import "CSIngredients.h" - -@interface CSIngredientListViewCell() - -@property (nonatomic, weak, readwrite) UIButton *detailButton; - -@end - -@implementation CSIngredientListViewCell - -- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) - { - UIButton *detailButton = [UIButton buttonWithType:UIButtonTypeInfoDark]; - [detailButton setTintColor:RED_LINE_COLOR]; - self.accessoryView = detailButton; - self.detailButton = detailButton; - self.textLabel.font = [UIFont fontWithName:@"AvenirNext-Regular" size:17]; - } - return self; -} - -- (void)configureForListVC:(CSIngredientListVC *)listVC ingredient:(CSIngredient *)ingredient -{ - self.textLabel.text = [ingredient name]; - self.detailButton.tag = [[CSIngredients sharedInstance] flattenedIndexForIngredient:ingredient]; - [self.detailButton removeTarget:nil action:nil forControlEvents:UIControlEventTouchUpInside]; - [self.detailButton addTarget:listVC action:@selector(detailButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; -} - -@end diff --git a/CookSmart/CookSmart/CSIngredients.h b/CookSmart/CookSmart/CSIngredients.h deleted file mode 100644 index 0355e79..0000000 --- a/CookSmart/CookSmart/CSIngredients.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// CSIngredients.h -// CookSmart -// -// Created by Vova Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import - -#define INGREDIENT_DELETE_NOTIFICATION_NAME @"ingredient_delete_notification" - -@class CSIngredient; -@class CSIngredientGroup; -@class CSRecentsIngredientGroup; - -static inline NSString *pathToIngredientsOnDisk(void) -{ - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSCAssert([paths count] > 0, @"Unable to get the path to the documents directory."); - NSString *documentsDirectory = paths[0]; - return [documentsDirectory stringByAppendingPathComponent:@"ingredients.plist"]; -} - -static inline NSString *pathToIngredientsInBundle(void) -{ - return [[NSBundle mainBundle] pathForResource:@"Ingredients" ofType:@"plist"]; -} - -@interface CSIngredients : NSObject - -- (id)initWithIngredientGroups:(NSArray *)ingredientGroups synthesizeGroups:(BOOL)shouldSynthesize; -+ (CSIngredients *)sharedInstance; -- (void)refreshRecents; -- (CSRecentsIngredientGroup *)recents; -- (CSIngredientGroup *)ingredientGroupAtIndex:(NSUInteger)index; -- (CSIngredient*)ingredientAtGroupIndex:(NSUInteger)groupIndex andIngredientIndex:(NSUInteger)index; -- (NSUInteger)countOfIngredientGroups; -- (NSUInteger)flattenedIndexForIngredient:(CSIngredient *)passedInIngredient; -- (CSIngredient *)ingredientAtFlattenedIngredientIndex:(NSUInteger)flattenedIngredientIndex; -- (NSUInteger)flattenedIngredientIndexForGroupIndex:(NSUInteger)groupIndex ingredientIndex:(NSUInteger)index; -- (NSUInteger)flattenedCountOfIngredients; -- (NSUInteger)indexOfIngredientGroup:(CSIngredientGroup *)group; - -- (BOOL)deleteIngredientAtGroupIndex:(NSUInteger)groupIndex ingredientIndex:(NSUInteger)ingredientIndex; -- (BOOL)addIngredient:(CSIngredient*)newIngr; -- (BOOL)persist; - -- (void)deleteAllSavedIngredients; - -@end diff --git a/CookSmart/CookSmart/CSIngredients.m b/CookSmart/CookSmart/CSIngredients.m deleted file mode 100644 index 01de09a..0000000 --- a/CookSmart/CookSmart/CSIngredients.m +++ /dev/null @@ -1,348 +0,0 @@ - // -// CSIngredients.m -// CookSmart -// -// Created by Vova Galchenko on 1/23/14. -// Copyright (c) 2014 Olga Galchenko. All rights reserved. -// - -#import "CSIngredients.h" -#import "CSIngredientGroup.h" -#import "CSFilteredIngredientGroup.h" -#import "CSRecentsIngredientGroup.h" -#import "CSIngredient.h" - -#define CUSTOM_GROUP_NAME @"Custom" - -@interface CSIngredients() -{ - unsigned long _version; -} - -@property (nonatomic, readwrite, strong) NSMutableArray *ingredientGroups; - -@end - -@implementation CSIngredients - -static CSIngredients *sharedInstance; - -+ (void)initialize -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - BOOL ingredientsIsDir = YES; - if ([[NSFileManager defaultManager] fileExistsAtPath:pathToIngredientsOnDisk() isDirectory:&ingredientsIsDir]) - { - CSAssert(!ingredientsIsDir, @"ingredients_file_is_directory", @"The ingredients file's place is taken by a directory."); - } - else - { - // This is the first time we're launching the app. - // Let's move the ingredients file from the app bundle to our sandbox. - [CSIngredients copyIngredientsFromBundle]; - } - sharedInstance = [[self alloc] initWithPlistOnDisk]; - }); -} - -+ (void)copyIngredientsFromBundle -{ - NSError *copyError = nil; - [[NSFileManager defaultManager] copyItemAtPath:pathToIngredientsInBundle() - toPath:pathToIngredientsOnDisk() - error:©Error]; - CSAssert(copyError == nil, @"ingredients_file_copy", @"Error occurred while copying the ingredients file to the sandbox."); -} - -- (id)initWithPlistOnDisk -{ - NSArray *rawIngredientGroupsArray = [NSArray arrayWithContentsOfFile:pathToIngredientsOnDisk()]; - NSMutableArray *tmpIngredientGroupsArray = [NSMutableArray arrayWithCapacity:[rawIngredientGroupsArray count]]; - - for (NSDictionary *ingredientGroupDict in rawIngredientGroupsArray) - { - [tmpIngredientGroupsArray addObject:[CSIngredientGroup ingredientGroupWithDictionary:ingredientGroupDict]]; - } - return [self initWithIngredientGroups:[NSArray arrayWithArray:tmpIngredientGroupsArray] synthesizeGroups:YES]; -} - -- (id)initWithIngredientGroups:(NSArray *)ingredientGroups synthesizeGroups:(BOOL)shouldSynthesize -{ - if (self = [super init]) - { - NSMutableArray *groups = [NSMutableArray arrayWithArray:ingredientGroups]; - if (shouldSynthesize) - { - CSRecentsIngredientGroup *recents = [CSRecentsIngredientGroup recentsGroupWithIngredients:[self ingredientsFromGroups:groups]]; - if ([recents countOfIngredients] > 0) [groups insertObject:recents atIndex:0]; - } - self.ingredientGroups = groups; - } - return self; -} - -+ (CSIngredients *)sharedInstance -{ - CSAssert(sharedInstance != nil, @"ingredients_singleton_guard", @"Something went wrong with the singleton."); - return sharedInstance; -} - -- (void)refreshRecents -{ - _version++; - NSArray *nonSyntheticGroups = [self.ingredientGroups filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(CSIngredientGroup * _Nonnull evaluatedObject, NSDictionary * _Nullable bindings) { - return !evaluatedObject.isSynthetic; - }]]; - CSRecentsIngredientGroup *recents = [CSRecentsIngredientGroup recentsGroupWithIngredients:[self ingredientsFromGroups:nonSyntheticGroups]]; - if (recents.countOfIngredients > 0 && [nonSyntheticGroups count] < [self.ingredientGroups count]) - { - CSAssert([self.ingredientGroups count] > 0 && [[self.ingredientGroups objectAtIndex:0] isKindOfClass:[CSRecentsIngredientGroup class]], - @"recents_group_required", @"The first ingredient group must always be "); - [self.ingredientGroups replaceObjectAtIndex:0 - withObject:recents]; - [self persist]; - } - else if (recents.countOfIngredients > 0) - { - [self.ingredientGroups insertObject:recents atIndex:0]; - [self persist]; - } - else if (recents.countOfIngredients == 0 && ((CSIngredientGroup *) self.ingredientGroups[0]).isSynthetic) { - [self.ingredientGroups removeObjectAtIndex:0]; - [self persist]; - } -} - -- (CSRecentsIngredientGroup *)recents -{ - CSIngredientGroup *group = self.ingredientGroups[0]; - if (![group isKindOfClass:[CSRecentsIngredientGroup class]]) { - group = nil; - } - return (CSRecentsIngredientGroup *)group; -} - -- (NSArray *)ingredientsFromGroups:(NSArray *)groups -{ - NSMutableArray *allIngredients = [NSMutableArray array]; - for (CSIngredientGroup *group in groups) - { - for (int i = 0; i < group.countOfIngredients; i++) - { - [allIngredients addObject:[group ingredientAtIndex:i]]; - } - } - return allIngredients; -} - -- (CSIngredientGroup *)ingredientGroupAtIndex:(NSUInteger)index -{ - CSIngredientGroup *group = [self.ingredientGroups objectAtIndex:index]; - return group; -} - -- (CSIngredientGroup*)lastIngredientGroup -{ - CSIngredientGroup* group = [self.ingredientGroups lastObject]; - return group; -} - -- (CSIngredientGroup *)customIngredientGroup -{ - if (![[self lastIngredientGroup].name isEqualToString:CUSTOM_GROUP_NAME]) - { - //need to create it - [self.ingredientGroups addObject:[CSIngredientGroup ingredientGroupWithDictionary:@{CUSTOM_GROUP_NAME:@[]}]]; - } - return [self lastIngredientGroup]; -} - -- (CSIngredient*)ingredientAtGroupIndex:(NSUInteger)groupIndex andIngredientIndex:(NSUInteger)index -{ - CSIngredient* returnIngr = nil; - if (groupIndex < [self countOfIngredientGroups]) - { - CSIngredientGroup* group = [self ingredientGroupAtIndex:groupIndex]; - if (index < [group countOfIngredients]) - returnIngr = [group ingredientAtIndex:index]; - } - - return returnIngr; -} - -- (NSUInteger)indexOfIngredientGroup:(CSIngredientGroup *)group -{ - NSUInteger index = NSNotFound; - for (int i = 0; i < self.countOfIngredientGroups; i++) - { - if ([self ingredientGroupAtIndex:i] == group) - { - index = i; - } - } - return index; -} - -- (NSUInteger)flattenedIndexForIngredient:(CSIngredient *)passedInIngredient -{ - NSUInteger result = 0; - for (CSIngredientGroup *group in self) - { - for (CSIngredient *ingredient in group) - { - if ([ingredient isEqualToIngredient:passedInIngredient]) - { - return result; - } - result++; - } - } - return NSNotFound; -} - -- (CSIngredient *)ingredientAtFlattenedIngredientIndex:(NSUInteger)flattenedIngredientIndex -{ - NSInteger ingredientIndex = flattenedIngredientIndex; - NSUInteger groupIndex = 0; - while (ingredientIndex > ((NSInteger)[[self ingredientGroupAtIndex:groupIndex] countOfIngredients] - 1)) - { - ingredientIndex -= [[self ingredientGroupAtIndex:groupIndex] countOfIngredients]; - groupIndex++; - } - return [[self ingredientGroupAtIndex:groupIndex] ingredientAtIndex:ingredientIndex]; -} - -- (NSUInteger)flattenedIngredientIndexForGroupIndex:(NSUInteger)groupIndex ingredientIndex:(NSUInteger)index -{ - NSUInteger flattenedIngredientIndex = index; - for (NSUInteger i = 0; i < groupIndex; i++) - { - flattenedIngredientIndex += [[self ingredientGroupAtIndex:i] countOfIngredients]; - } - return flattenedIngredientIndex; -} - -- (NSUInteger)flattenedCountOfIngredients -{ - NSUInteger numIngredients = 0; - for (CSIngredientGroup *group in self) - { - numIngredients += [group countOfIngredients]; - } - return numIngredients; -} - -- (NSUInteger)countOfIngredientGroups -{ - return self.ingredientGroups.count; -} - -- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)len -{ - NSUInteger count = 0; - unsigned long countOfItemsAlreadyEnumerated = state->state; - - if(countOfItemsAlreadyEnumerated == 0) - { - // We are not tracking mutations, so we'll set state->mutationsPtr to point - // into one of our extra values, since these values are not otherwise used - // by the protocol. - // If your class was mutable, you may choose to use an internal variable that - // is updated when the class is mutated. - // state->mutationsPtr MUST NOT be NULL and SHOULD NOT be set to self. - state->mutationsPtr = &_version; - } - - if(countOfItemsAlreadyEnumerated < [self.ingredientGroups count]) - { - state->itemsPtr = buffer; - while((countOfItemsAlreadyEnumerated < [self.ingredientGroups count]) && (count < len)) - { - // Add the item for the next index to stackbuf. - // - // If you choose not to use ARC, you do not need to retain+autorelease the - // objects placed into stackbuf. It is the caller's responsibility to ensure we - // are not deallocated during enumeration. - buffer[count] = self.ingredientGroups[countOfItemsAlreadyEnumerated]; - countOfItemsAlreadyEnumerated++; - - // We must return how many items are in state->itemsPtr. - count++; - } - } - else - count = 0; - - // Update state->state with the new value of countOfItemsAlreadyEnumerated so that it is - // preserved for the next invocation. - state->state = countOfItemsAlreadyEnumerated; - return count; -} - -- (BOOL)deleteIngredientAtGroupIndex:(NSUInteger)groupIndex ingredientIndex:(NSUInteger)ingredientIndex -{ - _version++; //mutation protection for fast enumeration - - CSIngredientGroup *ingrGroup = [self ingredientGroupAtIndex:groupIndex]; - CSIngredient *ingredient = [ingrGroup ingredientAtIndex:ingredientIndex]; - [ingrGroup deleteIngredient:ingredient]; - if ([ingrGroup countOfIngredients] <= 0) - { - [self.ingredientGroups removeObject:ingrGroup]; - } - if ([ingrGroup respondsToSelector:@selector(originalIngredientGroup)] && - (ingrGroup = [ingrGroup performSelector:@selector(originalIngredientGroup) withObject:nil]) && - [ingrGroup countOfIngredients] <= 0) - { - [sharedInstance.ingredientGroups removeObject:ingrGroup]; - } - [self refreshRecents]; - [[NSNotificationCenter defaultCenter] postNotificationName:INGREDIENT_DELETE_NOTIFICATION_NAME object:ingredient]; - return [sharedInstance persist]; -} - -- (BOOL)addIngredient:(CSIngredient*)newIngr -{ - _version++; - - CSIngredientGroup* ingrGroup = [self customIngredientGroup]; - [ingrGroup addIngredient:newIngr]; - - return [sharedInstance persist]; -} - -- (BOOL)persist -{ - NSMutableArray *groupsToSerialize = [NSMutableArray arrayWithCapacity:self.ingredientGroups.count]; - for (CSIngredientGroup *group in self.ingredientGroups) - { - if (!group.isSynthetic) - { - [groupsToSerialize addObject:[group dictionary]]; - } - } - BOOL success = [groupsToSerialize writeToFile:pathToIngredientsOnDisk() atomically:YES]; - if (!success) - { - NSLog(@"FAILED TO WRITE FILE: %s", strerror(errno)); - } - return success; -} - -- (void)deleteAllSavedIngredients -{ - sharedInstance = nil; - - NSError* err; - [[NSFileManager defaultManager] removeItemAtPath:pathToIngredientsOnDisk() error:&err]; - - CSAssert(err == nil, @"csingredients_remove_ingredients_saved_on_disk", @"Resetting ingredients to defaults"); - - [CSIngredients copyIngredientsFromBundle]; - sharedInstance = [[CSIngredients alloc] initWithPlistOnDisk]; - - [[NSNotificationCenter defaultCenter] postNotificationName:INGREDIENT_DELETE_NOTIFICATION_NAME object:nil]; -} - -@end diff --git a/CookSmart/CookSmart/CSRecentsIngredientGroup.h b/CookSmart/CookSmart/CSRecentsIngredientGroup.h deleted file mode 100644 index 36e5d5b..0000000 --- a/CookSmart/CookSmart/CSRecentsIngredientGroup.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// CSRecentsIngredientGroup.h -// CookSmart -// -// Created by Vova Galchenko on 9/22/15. -// Copyright © 2015 Olga Galchenko. All rights reserved. -// - -#import "CSIngredientGroup.h" -#import "CSIngredients.h" - -@interface CSRecentsIngredientGroup : CSIngredientGroup - -+ (CSRecentsIngredientGroup *)recentsGroupWithIngredients:(NSArray *)allIngredients; - -@end diff --git a/CookSmart/CookSmart/CSRecentsIngredientGroup.m b/CookSmart/CookSmart/CSRecentsIngredientGroup.m deleted file mode 100644 index ec1dbf4..0000000 --- a/CookSmart/CookSmart/CSRecentsIngredientGroup.m +++ /dev/null @@ -1,54 +0,0 @@ -// -// CSRecentsIngredientGroup.m -// CookSmart -// -// Created by Vova Galchenko on 9/22/15. -// Copyright © 2015 Olga Galchenko. All rights reserved. -// - -#import "CSRecentsIngredientGroup.h" -#import "CSIngredientGroupInternals.h" -#import "CSIngredient.h" - -#define MAX_NUM_RECENTS 5 - -@implementation CSRecentsIngredientGroup - -- (id)initWithIngredients:(NSArray *)allIngredients -{ - if (self = [super init]) - { - self.name = @"Recents"; - NSArray *accessedIngredients = [allIngredients filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(CSIngredient * _Nonnull evaluatedIngr, NSDictionary * _Nullable bindings) { - return evaluatedIngr.lastAccessDate != nil; - }]]; - NSArray *sortedAccessedIngredients = [accessedIngredients sortedArrayUsingComparator:^NSComparisonResult(CSIngredient * _Nonnull ingr1, CSIngredient * _Nonnull ingr2) { - return [ingr2.lastAccessDate compare:ingr1.lastAccessDate]; - }]; - NSArray *topAccessedIngredients = [sortedAccessedIngredients subarrayWithRange:NSMakeRange(0, MIN(MAX_NUM_RECENTS, sortedAccessedIngredients.count))]; - self.ingredients = [NSMutableArray arrayWithArray:topAccessedIngredients]; - self.synthetic = YES; - } - return self; -} - -+ (CSRecentsIngredientGroup *)recentsGroupWithIngredients:(NSArray *)allIngredients; -{ - return [[self alloc] initWithIngredients: allIngredients]; -} - -- (void)deleteIngredient:(CSIngredient *)ingredient -{ - CSAssertFail(@"recent_ingr_delete", @"Manipulating recent ingredients is not allowed. Attempted deletion of %@", ingredient); -} -- (void)addIngredient:(CSIngredient*)ingredient -{ - CSAssertFail(@"recent_ingr_add", @"Manipulating recent ingredients is not allowed. Attempted addition of %@", ingredient); -} - -- (void)replaceIngredientAtIndex:(NSUInteger)index withIngredient:(CSIngredient*)ingredient -{ - CSAssertFail(@"recent_ingr_replace", @"Manipulating recent ingredients is not allowed. Attempted replacement."); -} - -@end diff --git a/CookSmart/CookSmart/CSScaleView+ViewRepresentable.swift b/CookSmart/CookSmart/CSScaleView+ViewRepresentable.swift deleted file mode 100644 index 0631cc3..0000000 --- a/CookSmart/CookSmart/CSScaleView+ViewRepresentable.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CSScaleView+ViewRepresentable.swift -// cake -// -// Created by Olga Galchenko on 4/12/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import SwiftUI - -struct ScaleScrollViewRepresentable: UIViewRepresentable { - func makeUIView(context: Context) -> ScaleScrollView { - ScaleScrollView() - } - - func updateUIView(_ scaleView: ScaleScrollView, context: Context) {} -} - -struct ScaleScrollView_Previews: PreviewProvider { - static var previews: some View { - ScaleScrollViewRepresentable() - .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) - } -} diff --git a/CookSmart/CookSmart/CookSmart-Prefix.pch b/CookSmart/CookSmart/CookSmart-Prefix.pch index 594f0f0..c743db2 100644 --- a/CookSmart/CookSmart/CookSmart-Prefix.pch +++ b/CookSmart/CookSmart/CookSmart-Prefix.pch @@ -51,4 +51,4 @@ static inline void CSAssertFail(NSString *assertName, NSString *descriptionForma va_start(args, descriptionFormat); CSAssert_v(NO, assertName, descriptionFormat, args); va_end(args); -} \ No newline at end of file +} diff --git a/CookSmart/CookSmart/Core/Button.swift b/CookSmart/CookSmart/Core/Button.swift deleted file mode 100644 index 5194030..0000000 --- a/CookSmart/CookSmart/Core/Button.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Button.swift -// cake -// -// Created by Olga Galchenko on 4/18/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import UIKit - -final class Button: UIButton, StyledView { - init(style: ButtonStyle = .standard) { - super.init(frame: .zero) - - translatesAutoresizingMaskIntoConstraints = false - applyStyle(style) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func applyStyle(_ style: ButtonStyle) { - setTitleColor(style.titleColor, for: .normal) - titleLabel?.font = style.font - } -} - -struct ButtonStyle { - var titleColor: UIColor = Color.redLineColor - var font: UIFont = AvenirFont.regular.of(size: 17) - - static var standard: ButtonStyle { - ButtonStyle() - } -} diff --git a/CookSmart/CookSmart/Core/Colors.swift b/CookSmart/CookSmart/Core/Colors.swift deleted file mode 100644 index 479dfd9..0000000 --- a/CookSmart/CookSmart/Core/Colors.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Colors.swift -// cake -// -// Created by Alex King on 3/28/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import UIKit - -enum Color { - static let redLineColor = Color.color( - light: UIColor(red: 187.0 / 255.0, green: 1.0 / 255.0, blue: 3.0 / 255.0, alpha: 1.0), - dark: UIColor(red: 187.0 / 255.0, green: 1.0 / 255.0, blue: 3.0 / 255.0, alpha: 1.0) - ) - - static let background = Color.color( - light: UIColor(red: 245.0 / 255.0, green: 245.0 / 255.0, blue: 245.0 / 255.0, alpha: 1.0), - dark: UIColor(red: 10.0 / 255.0, green: 10.0 / 255.0, blue: 10.0 / 255.0, alpha: 1.0) - ) - - private static func color(light: UIColor, dark: UIColor) -> UIColor { - UIColor { traitCollection in - switch traitCollection.userInterfaceStyle { - case .dark: return dark - case .light, .unspecified: return light - @unknown default: return light - } - } - } -} diff --git a/CookSmart/CookSmart/Core/Double+StringUtils.swift b/CookSmart/CookSmart/Core/Double+StringUtils.swift deleted file mode 100644 index 703aa3a..0000000 --- a/CookSmart/CookSmart/Core/Double+StringUtils.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Double+StringUtils.swift -// cake -// -// Created by Olga Galchenko on 4/11/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import Foundation - -private let fractionThreshold: Double = 50.0 - -extension Double { - private var whole: Int { Int(modf(self).0) } - private var decimal: Self { modf(self).1 } - - private var roundedWhole: Int { - guard self < fractionThreshold else { - return Int(rounded(FloatingPointRoundingRule.toNearestOrEven)) - } - - return whole - } - - private var decimalFraction: Fraction { - guard self < fractionThreshold else { return .zero } - return Fraction(value: decimal) - } - - var roundedValue: Double { - Double(roundedWhole) + decimalFraction.rawValue - } - - var vulgarFractionString: String { - let rounded = roundedValue - let whole = rounded.whole - let fraction = rounded.decimalFraction - - if whole == 0, fraction != .zero { - return fraction.string - } - return String(whole) + fraction.string - } -} diff --git a/CookSmart/CookSmart/Core/Fonts.swift b/CookSmart/CookSmart/Core/Fonts.swift deleted file mode 100644 index 936985c..0000000 --- a/CookSmart/CookSmart/Core/Fonts.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Fonts.swift -// cake -// -// Created by Olga Galchenko on 3/28/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import SwiftUI -import UIKit - -public enum AvenirFont: String { - case condensedMedium = "AvenirNextCondensed-Medium" - case regular = "AvenirNext-Regular" - case medium = "AvenirNext-Medium" - - public func of(size: CGFloat) -> UIFont { - UIFont(name: rawValue, size: size) ?? UIFont.systemFont(ofSize: size) - } -} - -struct CustomFont: ViewModifier { - var weight: AvenirFont - var size: CGFloat - - func body(content: Content) -> some View { - content.font(.custom(weight.rawValue, size: size)) - } -} - -extension View { - func font(weight: AvenirFont, size: CGFloat) -> some View { - modifier(CustomFont(weight: weight, size: size)) - } -} diff --git a/CookSmart/CookSmart/Core/Fraction.swift b/CookSmart/CookSmart/Core/Fraction.swift deleted file mode 100644 index aabd195..0000000 --- a/CookSmart/CookSmart/Core/Fraction.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Fraction.swift -// cake -// -// Created by Olga Galchenko on 4/11/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import Foundation - -enum Fraction: Double, CaseIterable { - case zero = 0 - case eighth = 0.125 - case quarter = 0.250 - case third = 0.333 - case threeEighths = 0.375 - case half = 0.500 - case fiveEighths = 0.625 - case twoThirds = 0.666 - case threeQuarters = 0.750 - case sevenEighths = 0.875 - case one = 1.0 - - init(value: Double) { - var closestFraction: Fraction = .zero - var smallestDifference = 1.0 - - for fraction in Fraction.allCases { - let difference = fabs(fraction.rawValue - value) - if difference < smallestDifference { - smallestDifference = difference - closestFraction = fraction - } - } - - self = closestFraction - } - - var string: String { - switch self { - case .eighth: return "⅛" - case .quarter: return "¼" - case .third: return "⅓" - case .threeEighths: return "⅜" - case .half: return "½" - case .fiveEighths: return "⅝" - case .twoThirds: return "⅔" - case .threeQuarters: return "¾" - case .sevenEighths: return "⅞" - case .zero, .one: return "" - } - } -} diff --git a/CookSmart/CookSmart/Core/Label.swift b/CookSmart/CookSmart/Core/Label.swift deleted file mode 100644 index 5dd265c..0000000 --- a/CookSmart/CookSmart/Core/Label.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Label.swift -// cake -// -// Created by Olga Galchenko on 4/18/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import UIKit - -final class Label: UILabel, StyledView { - init(style: LabelStyle = .standard) { - super.init(frame: .zero) - - translatesAutoresizingMaskIntoConstraints = false - applyStyle(style) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func applyStyle(_ style: LabelStyle) { - textColor = style.titleColor - font = style.font - } -} - -struct LabelStyle { - var titleColor: UIColor = .label - var font: UIFont = AvenirFont.regular.of(size: 17) - - static var standard: LabelStyle { - LabelStyle() - } - - static var tiny: LabelStyle { - var style = standard - style.font = AvenirFont.condensedMedium.of(size: 12) - return style - } - - static var medium: LabelStyle { - var style = standard - style.font = AvenirFont.regular.of(size: 15) - return style - } -} diff --git a/CookSmart/CookSmart/Core/StyledView.swift b/CookSmart/CookSmart/Core/StyledView.swift deleted file mode 100644 index 4fac7a7..0000000 --- a/CookSmart/CookSmart/Core/StyledView.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// StyledView.swift -// cake -// -// Created by Olga Galchenko on 4/18/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import UIKit - -protocol StyledView: AnyObject { - associatedtype Style - - init(style: Style) -} - -extension StyledView { - public static func make(style: Self.Style) -> Self { - Self(style: style) - } -} diff --git a/CookSmart/CookSmart/Design Language/Assets.xcassets/AccentColor.colorset/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..73a92b8 --- /dev/null +++ b/CookSmart/CookSmart/Design Language/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "3", + "green" : "1", + "red" : "187" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "3", + "green" : "1", + "red" : "187" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/1024.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/1024.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/120.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/120.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/152.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/152.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/167.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/167.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/180.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/180.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/20.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/20.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/29.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/29.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/40.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/40.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/58.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/58.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/60.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/60.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/76.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/76.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/80.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/80.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/87.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/87.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/AppIcon.appiconset/Contents.json rename to CookSmart/CookSmart/Design Language/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/CookSmart/CookSmart/Design Language/Assets.xcassets/BackgroundColor.colorset/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/BackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..69ab891 --- /dev/null +++ b/CookSmart/CookSmart/Design Language/Assets.xcassets/BackgroundColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "245", + "green" : "245", + "red" : "245" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "10", + "green" : "10", + "red" : "10" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CookSmart/CookSmart/Images.xcassets/Close.imageset/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/Close.imageset/Contents.json similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/Close.imageset/Contents.json rename to CookSmart/CookSmart/Design Language/Assets.xcassets/Close.imageset/Contents.json diff --git a/CookSmart/CookSmart/Images.xcassets/Close.imageset/icon_076.png b/CookSmart/CookSmart/Design Language/Assets.xcassets/Close.imageset/icon_076.png similarity index 100% rename from CookSmart/CookSmart/Images.xcassets/Close.imageset/icon_076.png rename to CookSmart/CookSmart/Design Language/Assets.xcassets/Close.imageset/icon_076.png diff --git a/CookSmart/CookSmart/Design Language/Assets.xcassets/ContentTextColor.colorset/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/ContentTextColor.colorset/Contents.json new file mode 100644 index 0000000..9fdb5a8 --- /dev/null +++ b/CookSmart/CookSmart/Design Language/Assets.xcassets/ContentTextColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CookSmart/CookSmart/Design Language/Assets.xcassets/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/CookSmart/CookSmart/Design Language/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CookSmart/CookSmart/Design Language/Assets.xcassets/SubheadingTextColor.colorset/Contents.json b/CookSmart/CookSmart/Design Language/Assets.xcassets/SubheadingTextColor.colorset/Contents.json new file mode 100644 index 0000000..a001870 --- /dev/null +++ b/CookSmart/CookSmart/Design Language/Assets.xcassets/SubheadingTextColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "142", + "green" : "138", + "red" : "138" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "147", + "green" : "141", + "red" : "141" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CookSmart/CookSmart/Design Language/CSColor.swift b/CookSmart/CookSmart/Design Language/CSColor.swift new file mode 100644 index 0000000..d0f840d --- /dev/null +++ b/CookSmart/CookSmart/Design Language/CSColor.swift @@ -0,0 +1,30 @@ +// +// CSColor.swift +// cake +// +// Created by Vova Galchenko on 12/2/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +import SwiftUI + +// TODO: There should be no reference to any color except through this enum. Enforce this at build time. +enum CSColor: String { + case background = "BackgroundColor" + case accent = "AccentColor" + case subheadingText = "SubheadingTextColor" + case contentText = "ContentTextColor" + case clear = "Clear" + + func asUIColor() -> UIColor { + if rawValue == "Clear" { + UIColor.clear + } else { + UIColor(named: rawValue)! + } + } + + func asSwiftUIColor() -> Color { + Color(asUIColor()) + } +} diff --git a/CookSmart/CookSmart/Design Language/CSTextStyle.swift b/CookSmart/CookSmart/Design Language/CSTextStyle.swift new file mode 100644 index 0000000..ca34e9e --- /dev/null +++ b/CookSmart/CookSmart/Design Language/CSTextStyle.swift @@ -0,0 +1,108 @@ +// +// CSTextStyle.swift +// cake +// +// Created by Vova Galchenko on 12/3/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +import SwiftUI +import UIKit + +// TODO: Enforce at build time that no text styling is done outside of CSTextStyle +struct CSTextStyle { + let font: UIFont + let color: CSColor + let backgroundColor: CSColor + + init(font: UIFont, color: CSColor, backgroundColor: CSColor = .clear) { + self.font = font + self.color = color + self.backgroundColor = backgroundColor + } + + static let heading = CSTextStyle(font: CSFont.heading, color: .contentText) + static let subheading = CSTextStyle(font: CSFont.subheading, color: .subheadingText) + static let minorContent = CSTextStyle(font: CSFont.minorContent, color: .contentText) + static let supportingContent = CSTextStyle(font: CSFont.supportingContent, color: .contentText) + static let coreContent = CSTextStyle(font: CSFont.coreContent, color: .contentText) + static let plainButton = CSTextStyle(font: CSFont.coreContent, color: .accent) + static let actionButton = CSTextStyle(font: CSFont.emphasizedContent, color: .accent) +} + +struct CSTextStyleViewModifier: ViewModifier { + let csTextStyle: CSTextStyle + + func body(content: Content) -> some View { + content + .font(Font(csTextStyle.font)) + .foregroundStyle(csTextStyle.color.asSwiftUIColor()) + .backgroundStyle(csTextStyle.backgroundColor.asSwiftUIColor()) + } +} + +extension Text { + func csTextStyle(_ style: CSTextStyle) -> some View { + modifier(CSTextStyleViewModifier(csTextStyle: style)) + } +} + +extension Button { + func csTextStyle(_ style: CSTextStyle) -> some View { + modifier(CSTextStyleViewModifier(csTextStyle: style)) + } +} + +extension UITextField { + func applyTextStyle(_ textStyle: CSTextStyle) { + font = textStyle.font + textColor = textStyle.color.asUIColor() + } +} + +extension UIButton { + convenience init(style: CSTextStyle) { + self.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + applyTextStyle(style) + } + + func applyTextStyle(_ style: CSTextStyle) { + setTitleColor(style.color.asUIColor(), for: .normal) + titleLabel?.font = style.font + } +} + +extension UILabel { + convenience init(style: CSTextStyle) { + self.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + applyStyle(style) + } + + func applyStyle(_ style: CSTextStyle) { + textColor = style.color.asUIColor() + font = style.font + } +} + +private enum CSFont { + // TODO: We should be using UIFontMetrics to automatically scale fonts depending on user settings + static let heading = AvenirFont.demiBold.of(size: 20) + static let subheading = AvenirFont.demiBold.of(size: 15) + static let minorContent = AvenirFont.condensedMedium.of(size: 12) + static let supportingContent = AvenirFont.regular.of(size: 15) + static let coreContent = AvenirFont.regular.of(size: 17) + static let emphasizedContent = AvenirFont.demiBold.of(size: 17) +} + +private enum AvenirFont: String { + case condensedMedium = "AvenirNextCondensed-Medium" + case regular = "AvenirNext-Regular" + case medium = "AvenirNext-Medium" + case demiBold = "AvenirNext-DemiBold" + + public func of(size: CGFloat) -> UIFont { + UIFont(name: rawValue, size: size) ?? UIFont.systemFont(ofSize: size) + } +} diff --git a/CookSmart/CookSmart/Design Language/CakeColor.swift b/CookSmart/CookSmart/Design Language/CakeColor.swift new file mode 100644 index 0000000..b8f09cf --- /dev/null +++ b/CookSmart/CookSmart/Design Language/CakeColor.swift @@ -0,0 +1,24 @@ +// +// CakeColor.swift +// cake +// +// Created by Vova Galchenko on 12/2/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +import SwiftUI + +// TODO: There should be no reference to any color except through this enum + +enum CSColor: String { + case background = "BackgroundColor" + case accent = "AccentColor" + + func asUIColor() -> UIColor { + UIColor(named: rawValue)! + } + + func asSwiftUIColor() -> Color { + Color(asUIColor()) + } +} diff --git a/CookSmart/CookSmart/Images.xcassets/Contents.json b/CookSmart/CookSmart/Images.xcassets/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/CookSmart/CookSmart/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CookSmart/CookSmart/Ingredients.plist b/CookSmart/CookSmart/Ingredients.plist index 1beba8f..ce60bb6 100644 --- a/CookSmart/CookSmart/Ingredients.plist +++ b/CookSmart/CookSmart/Ingredients.plist @@ -2,34 +2,28 @@ - + Flours - + Name All-Purpose Flour Density 125 - - Name - Almond Flour - Density - 95.6796 - - + Name Barley Flour Density 113.398 - + Name Bread Flour Density 120.485 - + Name Buckwheat Flour Density @@ -41,31 +35,31 @@ Density 116.25 - + Name Oat Flour Density 92.136 - + Name Quinoa Flour Density 92.136 - + Name Brown Rice Flour Density 152.379 - + Name White Rice Flour Density 141.748 - + Name Self Rising Flour Density @@ -100,13 +94,13 @@ Density 198 - + Name Pearl Sugar Density 200 - + Name Muscovado Sugar Density @@ -120,34 +114,34 @@ - + Oil and Shortening - + Name Butter Density 227 - + Name Canola Oil Density 219.5 - + Name Coconut Oil Density 216 - + Name Lard Density 205 - + Name Olive Oil Density @@ -159,7 +153,7 @@ Density 222 - + Name Vegetable Shortening Density @@ -167,63 +161,63 @@ - + Syrups - + Name Corn Syrup Density 311.845 - - Name - Honey - Density - 340 - - - Name - Maple Syrup - Density - 311.845 - - - Name - Molasses - Density - 340.194 - - + + Name + Honey + Density + 340 + + + Name + Maple Syrup + Density + 311.845 + + + Name + Molasses + Density + 340.194 + + - + Dry Goods - + Name Baking Powder Density 240 - + Name Bread Crumbs Density 113.398 - + Name Bread Crumbs, Panko Density 49.6117 - + Name Buttermilk Powder Density 198.447 - + Name Chocolate, chopped/chips Density @@ -235,23 +229,23 @@ Density 85 - + Name Cornmeal Density 138.204 - + Name Cornstarch Density 113.398 - + Name Oats, rolled Density - 99.2233 + 99.22329999999999 Name @@ -259,13 +253,13 @@ Density 269.32 - + Name Poppy Seeds Density 155.922 - + Name Raisins Density @@ -273,94 +267,94 @@ - + Dairy - + Name Buttermilk Density 242 - + Name Cream Cheese Density 226.796 - + Name Egg Whites, large Density 240 - + Name Egg Yolks, large Density 300 - + Name Feta Cheese Density 113.398 - + Name Heavy/Whipping Cream Density 232 - + Name Milk (whole, 2%, 1%, skim) Density 242 - + Name Milk, evaporated Density 255.146 - + Name Milk, sweetened condensed Density 311.845 - + Name Parmesan Cheese, grated Density - 99.2233 + 99.22329999999999 - + Name Ricotta Cheese Density 226.796 - + Name Semi-Soft Cheese, grated Density 113.398 - + Name Sour Cream Density 242 - + Name Yogurt Density 245 - + Name Yogurt, greek Density @@ -368,58 +362,58 @@ - + Produce - + Name Bananas, mashed Density 226.796 - + Name Berries, frozen Density 141.748 - + Name Blueberries Density 170.097 - + Name Carrots, grated Density - 99.2233 + 99.22329999999999 - + Name Mushrooms, sliced Density - 77.9612 + 77.96120000000001 - + Name Onions, diced Density 141.748 - + Name Pumpkin, pureed Density 269.32 - + Name Raspberries Density 120.485 - + Name Strawberries, sliced Density diff --git a/CookSmart/CookSmart/Misc Utils/Assertions.swift b/CookSmart/CookSmart/Misc Utils/Assertions.swift new file mode 100644 index 0000000..1eee17e --- /dev/null +++ b/CookSmart/CookSmart/Misc Utils/Assertions.swift @@ -0,0 +1,32 @@ +// +// Assertions.swift +// cake +// +// Created by Vova Galchenko on 11/25/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +func assertAndLogOnFailure( + _ assertion: Bool, + _ message: @autoclosure () -> String, + file: StaticString = #file, + line: UInt = #line +) { + guard assertion else { + logAndFailAssertion(message(), file: file, line: line) + } +} + +func logAndFailAssertion( + _ message: @autoclosure () -> String, + _ error: Error? = nil, + file: StaticString = #file, + line: UInt = #line +) -> Never { + logIssue("assert_fail", [ + "src_location": "\(file):\(line)", + "assert_msg": message(), + "error_description": error?.localizedDescription ?? "none", + ]) + preconditionFailure(message(), file: file, line: line) +} diff --git a/CookSmart/CookSmart/Misc Utils/Double+StringUtils.swift b/CookSmart/CookSmart/Misc Utils/Double+StringUtils.swift new file mode 100644 index 0000000..d9d79af --- /dev/null +++ b/CookSmart/CookSmart/Misc Utils/Double+StringUtils.swift @@ -0,0 +1,88 @@ +// +// Double+StringUtils.swift +// cake +// +// Created by Olga Galchenko on 4/11/20. +// Copyright © 2020 Olga Galchenko. All rights reserved. +// + +import Foundation + +private let fractionThreshold: Double = 50.0 + +extension Double { + private var whole: Int { Int(modf(self).0) } + private var decimal: Self { modf(self).1 } + + private var roundedWhole: Int { + guard self < fractionThreshold else { + return Int(rounded(FloatingPointRoundingRule.toNearestOrEven)) + } + + return whole + } + + private var decimalFraction: HumanReadableFraction { + guard self < fractionThreshold else { return .zero } + return HumanReadableFraction(value: decimal) + } + + var roundedValue: Double { + Double(roundedWhole) + decimalFraction.rawValue + } + + var humanReabableString: String { + let rounded = roundedValue + let whole = rounded.whole + let fraction = rounded.decimalFraction + + if whole == 0, fraction != .zero { + return fraction.string + } + return String(whole) + fraction.string + } +} + +private enum HumanReadableFraction: Double, CaseIterable { + case zero = 0 + case eighth = 0.125 + case quarter = 0.250 + case third = 0.3333333333333333 + case threeEighths = 0.375 + case half = 0.500 + case fiveEighths = 0.625 + case twoThirds = 0.6666666666666666 + case threeQuarters = 0.750 + case sevenEighths = 0.875 + case one = 1.0 + + init(value: Double) { + var closestFraction: HumanReadableFraction = .zero + var smallestDifference = 1.0 + + for fraction in HumanReadableFraction.allCases { + let difference = fabs(fraction.rawValue - value) + if difference < smallestDifference { + smallestDifference = difference + closestFraction = fraction + } + } + + self = closestFraction + } + + var string: String { + switch self { + case .eighth: return "⅛" + case .quarter: return "¼" + case .third: return "⅓" + case .threeEighths: return "⅜" + case .half: return "½" + case .fiveEighths: return "⅝" + case .twoThirds: return "⅔" + case .threeQuarters: return "¾" + case .sevenEighths: return "⅞" + case .zero, .one: return "" + } + } +} diff --git a/CookSmart/CookSmart/Model/Density.swift b/CookSmart/CookSmart/Model/Density.swift new file mode 100644 index 0000000..bad589f --- /dev/null +++ b/CookSmart/CookSmart/Model/Density.swift @@ -0,0 +1,46 @@ +// +// Density.swift +// cake +// +// Created by Vova Galchenko on 12/1/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +import Foundation + +struct Density: Codable, Equatable { + private let density: Double + var isValid: Bool { + !density.isNaN && density.isFinite && !density.isZero + } + + var analyticsRepresentation: String { + isValid ? "\(density)" : "invalid" + } + + // TODO: This should be removed after CSUnit-related models are easy-to-access enums + var canonical: Double { density } + + // TODO: Once CSUnit-related models are replaced with enums, we should be exlusively using the other initializer + init(inGramsPerCup canonicalDensity: Double) { + density = canonicalDensity + } + + init(_ magnitude: Float, in weightUnit: CSUnit, per volumeUnit: CSUnit) { + density = Double(magnitude * (volumeUnit.conversionFactor / weightUnit.conversionFactor)) + } + + init(from decoder: Decoder) throws { + let topLevelContainer = try decoder.singleValueContainer() + density = try topLevelContainer.decode(Double.self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(density) + } + + func `in`(_ weightUnit: CSUnit, per volumeUnit: CSUnit) -> Double { + density * Double(weightUnit.conversionFactor / volumeUnit.conversionFactor) + } +} diff --git a/CookSmart/CookSmart/Model/Ingredient.swift b/CookSmart/CookSmart/Model/Ingredient.swift new file mode 100644 index 0000000..4e75638 --- /dev/null +++ b/CookSmart/CookSmart/Model/Ingredient.swift @@ -0,0 +1,82 @@ +// +// Ingredient.swift +// cake +// +// Created by Vova Galchenko on 11/25/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +struct Ingredient: Codable, Identifiable, Equatable { + let id: UUID + let name: String + let density: Density + var lastAccessDate: Date? + + init(id: UUID, name: String, density: Density, lastAccessDate: Date? = nil) { + self.id = id + self.name = name + self.density = density + self.lastAccessDate = lastAccessDate + } + + init(name: String, density: Density, lastAccessDate: Date? = nil) { + assertAndLogOnFailure( + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1", + "This initializer is only here for test purposes" + ) + id = UUID() + self.name = name + self.density = density + self.lastAccessDate = lastAccessDate + } + + enum CodingKeys: String, CodingKey { + case id + case name + case density + case lastAccessDate + + // TODO: get rid of this once @objc interop is no longer necessary + var rawValue: String { + switch self { + case .id: return "Id" + case .name: return IngredientKeyName + case .density: return IngredientKeyDensity + case .lastAccessDate: return IngredientKeyLastAccessDate + } + } + } + + // Custom decoder to handle the case where the id isn't present. + // This is going to be in case of apps with old data. + // For now, I'm also not going to bother creating ids in the app bundle either. + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = if let decodedId = try container.decodeIfPresent(UUID.self, forKey: .id) { + decodedId + } else { + UUID() + } + name = try container.decode(String.self, forKey: .name) + density = try container.decode(Density.self, forKey: .density) + lastAccessDate = try container.decodeIfPresent(Date.self, forKey: .lastAccessDate) + } + + func matches(_ searchString: String) -> Bool { + name.range(of: searchString, options: .caseInsensitive) != nil + } + + // TODO: Remove this objc interop utility when it's no longer necessary + func toDictionary() -> [String: Any] { + let baseDict = [ + CodingKeys.name.stringValue: name, + CodingKeys.density.stringValue: density.canonical, + ] as [String: Any] + let date: [String: Any] = if lastAccessDate == nil { + [:] + } else { + [CodingKeys.lastAccessDate.stringValue: lastAccessDate!] + } + return baseDict.merging(date, uniquingKeysWith: { _, x in x }) + } +} diff --git a/CookSmart/CookSmart/Model/IngredientGroup/IngredientGroup.swift b/CookSmart/CookSmart/Model/IngredientGroup/IngredientGroup.swift new file mode 100644 index 0000000..dd39fc3 --- /dev/null +++ b/CookSmart/CookSmart/Model/IngredientGroup/IngredientGroup.swift @@ -0,0 +1,22 @@ +// +// IngredientGroup.swift +// cake +// +// Created by Vova Galchenko on 11/28/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +import Foundation + +protocol IngredientGroup: Identifiable where ID == String { + + var name: String { get } + var id: String { get } + var ingredients: [Ingredient] { get } + + func filter(searchString: String) -> Self? +} + +extension IngredientGroup { + var id: String { name } +} diff --git a/CookSmart/CookSmart/Model/IngredientGroup/RecentsIngredientGroup.swift b/CookSmart/CookSmart/Model/IngredientGroup/RecentsIngredientGroup.swift new file mode 100644 index 0000000..78e54ed --- /dev/null +++ b/CookSmart/CookSmart/Model/IngredientGroup/RecentsIngredientGroup.swift @@ -0,0 +1,27 @@ +// +// RecentsIngredientGroup.swift +// cake +// +// Created by Vova Galchenko on 11/28/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +struct RecentsIngredientGroup: IngredientGroup { + let name: String + let ingredients: [Ingredient] + private static let maxNumRecents = 5 + + init(storedIngredientGroups: [StoredIngredientGroup]) { + name = "Recents" + ingredients = storedIngredientGroups + .flatMap(\.ingredients) + .filter { $0.lastAccessDate != nil } + .sorted { $0.lastAccessDate! > $1.lastAccessDate! } + .prefix(RecentsIngredientGroup.maxNumRecents) + .map { $0 } // <- this is to turn ArraySlice into Array + } + + func filter(searchString: String) -> RecentsIngredientGroup? { + nil + } +} diff --git a/CookSmart/CookSmart/Model/IngredientGroup/StoredIngredientGroup.swift b/CookSmart/CookSmart/Model/IngredientGroup/StoredIngredientGroup.swift new file mode 100644 index 0000000..5352839 --- /dev/null +++ b/CookSmart/CookSmart/Model/IngredientGroup/StoredIngredientGroup.swift @@ -0,0 +1,44 @@ +// +// StoredIngredientGroup.swift +// cake +// +// Created by Vova Galchenko on 11/25/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +struct StoredIngredientGroup: IngredientGroup, Codable { + + let name: String + var ingredients: [Ingredient] + var id: String { name } + + // The custom decoder / encoder implementation below is unfortunately necessary, because of the weird way + // we encode ingredient groups. Instead of static keys, such as "name" and "ingredients", we actually encode them + // as something like [String: [Ingredient]] + init(from decoder: Decoder) throws { + let topLevelContainer = try decoder.singleValueContainer() + let ingrGroupDict = try topLevelContainer.decode([String: [Ingredient]].self) + assertAndLogOnFailure(ingrGroupDict.count == 1, "Trying to deserialize an incorrectly formatted ingredient group") + name = ingrGroupDict.keys.first! + ingredients = ingrGroupDict.values.first! + } + + init(name: String, ingredients: [Ingredient]) { + self.name = name + self.ingredients = ingredients + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode([name: ingredients]) + } + + func filter(searchString: String) -> StoredIngredientGroup? { + let filteredIngredients = ingredients.filter { $0.matches(searchString) } + if filteredIngredients.isEmpty { + return nil + } else { + return StoredIngredientGroup(name: name, ingredients: filteredIngredients) + } + } +} diff --git a/CookSmart/CookSmart/Model/IngredientsStore.swift b/CookSmart/CookSmart/Model/IngredientsStore.swift new file mode 100644 index 0000000..2b73fba --- /dev/null +++ b/CookSmart/CookSmart/Model/IngredientsStore.swift @@ -0,0 +1,230 @@ +// +// IngredientsStore.swift +// cake +// +// Created by Vova Galchenko on 11/25/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +// TODO: Add thorough testing +@objc class IngredientsStore: NSObject, ObservableObject { + @objc static let shared = IngredientsStore() + @Published private(set) var storedIngredientGroups: [StoredIngredientGroup] { + didSet { + // If the setting of this var is part of a locked critical section, + // which should always be the case, so will be the execution of didSet. + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + do { + let data = try encoder.encode(storedIngredientGroups) + try data.write(to: IngredientsStore.diskIngredientsURL) + } catch { + logAndFailAssertion("Failed to write ingredient groups to disk", error) + } + } + } + + var ingredientGroups: [any IngredientGroup] { + let stored = storedIngredientGroups + return [RecentsIngredientGroup(storedIngredientGroups: stored)] + stored + } + + var flatIngredients: [Ingredient] { + ingredientGroups.flatMap(\.ingredients) + } + + var ingredientRefs: [Ingredient.ID: IngredientRef] { + storedIngredientGroups.enumerated().flatMap { groupIndex, group in + group.ingredients.enumerated().map { ($1.id, IngredientRef(storedIngredientGroupIndex: groupIndex, ingredientIndex: $0)) } + }.reduce(into: [Ingredient.ID: IngredientRef]()) { $0[$1.0] = $1.1 } + } + + subscript(flattenedIndex: Array.Index) -> Ingredient { + flatIngredients[flattenedIndex] + } + + subscript(groupIndex: Array.Index, ingredientIndex: Array.Index) -> Ingredient { + ingredientGroups[groupIndex].ingredients[ingredientIndex] + } + + subscript(safeFlattenedIndex flattenedIndex: Array.Index) -> Ingredient? { + let ingrs = flatIngredients + if ingrs.indices.contains(flattenedIndex) { + return ingrs[flattenedIndex] + } else { + return nil + } + } + + subscript(ingredientId: Ingredient.ID) -> Ingredient? { + get { + withIngredientGroupsSourceLock { + if let ingrRef = ingredientRefs[ingredientId], + storedIngredientGroups.indices.contains(ingrRef.storedIngredientGroupIndex) && + storedIngredientGroups[ingrRef.storedIngredientGroupIndex].ingredients.indices.contains(ingrRef.ingredientIndex) { + storedIngredientGroups[ingrRef.storedIngredientGroupIndex].ingredients[ingrRef.ingredientIndex] + } else { + nil + } + } + } + + set { + withIngredientGroupsSourceLock { + if let ingrRef = ingredientRefs[ingredientId], + storedIngredientGroups.indices.contains(ingrRef.storedIngredientGroupIndex) && + storedIngredientGroups[ingrRef.storedIngredientGroupIndex].ingredients.indices.contains(ingrRef.ingredientIndex) { + if let ingrToSet = newValue { + storedIngredientGroups[ingrRef.storedIngredientGroupIndex].ingredients[ingrRef.ingredientIndex] = ingrToSet + } else { + // Ingredient deletion was requested + storedIngredientGroups[ingrRef.storedIngredientGroupIndex].ingredients.remove(at: ingrRef.ingredientIndex) + } + } else if let newIngredient = newValue { + if storedIngredientGroups.last!.name != "Custom" { + storedIngredientGroups.append(StoredIngredientGroup(name: "Custom", ingredients: [])) + } + storedIngredientGroups[storedIngredientGroups.endIndex - 1].ingredients.append(newIngredient) + } + } + // If there's no ingredient by that id and a nil newValue is passed in, that means a deletion + // of an ingredient that doesn't exist was requested. There's nothing to do. + } + } + + override private init() { + storedIngredientGroups = IngredientsStore.readDataFromDisk() + } + + private static func readDataFromDisk() -> [StoredIngredientGroup] { + var isDir: ObjCBool = false + if FileManager.default.fileExists(atPath: IngredientsStore.diskIngredientsURL.path, isDirectory: &isDir) { + assertAndLogOnFailure(!isDir.boolValue, "The ingredients file's place is taken by a directory") + } else { + IngredientsStore.copyIngredientsFromBundle() + } + + do { + let ingredientsData = try Data(contentsOf: IngredientsStore.diskIngredientsURL) + return try PropertyListDecoder().decode([StoredIngredientGroup].self, from: ingredientsData) + } catch { + logAndFailAssertion("Unable to read the ingredients plist", error) + } + } + + init(testData: [StoredIngredientGroup]) { + assertAndLogOnFailure( + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1", + "This initializer is only here for test purposes" + ) + storedIngredientGroups = testData + } + + // Just a convenience function – the same can be accomplished via subscript + public func upsert(_ ingredient: Ingredient) { + self[ingredient.id] = ingredient + } + + public func delete(ingredientsWithIds ingredientIds: [Ingredient.ID]) { + withIngredientGroupsSourceLock { + ingredientIds.forEach { self[$0] = nil } + } + } + + public func resetToDefault() { + withIngredientGroupsSourceLock { + do { + try FileManager.default.removeItem(at: IngredientsStore.diskIngredientsURL) + } catch { + logAndFailAssertion("Failed to remove the ingredients from disk as part of reset", error) + } + storedIngredientGroups = IngredientsStore.readDataFromDisk() + } + } + + func flattenedForIngredient(withId id: Ingredient.ID) -> Int { + flatIngredients.firstIndex { $0.id == id }! + } + + private func flattenedIndexFor(groupIndex: Int, ingredientIndex: Int) -> Int { + ingredientGroups.prefix(groupIndex).reduce(0) { $0 + $1.ingredients.count } + ingredientIndex + } + + private let ingredientGroupsSourceLock = NSRecursiveLock() + private func withIngredientGroupsSourceLock(_ closure: () -> T) -> T { + ingredientGroupsSourceLock.lock() + defer { ingredientGroupsSourceLock.unlock() } + return closure() + } + + private static func copyIngredientsFromBundle() { + do { + try FileManager.default.copyItem(at: bundleIngredientsURL, to: diskIngredientsURL) + } catch { + logAndFailAssertion("Unable to copy the ingredients plist from the app bundle", error) + } + } + + private static var diskIngredientsURL: URL { + guard let documentDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + logAndFailAssertion("Unable to get the path to the documents directory") + } + if #available(iOS 16.0, *) { + return documentDir.appending(path: "ingredients.plist") + } else { + return documentDir.appendingPathComponent("ingredients.plist") + } + } + + private static var bundleIngredientsURL: URL { + guard let url = Bundle.main.url(forResource: "Ingredients", withExtension: "plist") else { + logAndFailAssertion("Unable to find the ingredients plist in the app bundle") + } + return url + } + + // MARK: ObjC Interop Cruft + + @objc func markAccessOfIngredientAtFlattenedIndex(_ flattenedIndex: Array.Index) { + withIngredientGroupsSourceLock { + if let existingIngredient = self[safeFlattenedIndex: flattenedIndex] { + self[existingIngredient.id] = Ingredient( + id: existingIngredient.id, + name: existingIngredient.name, + density: existingIngredient.density, + lastAccessDate: Date() + ) + } + } + } + + @objc func ingredientNameAtFlattenedIndex(_ flattenedIndex: Int) -> String? { + self[safeFlattenedIndex: flattenedIndex]?.name + } + + @objc func flattenedCountOfIngredients() -> Int { + flatIngredients.count + } + + @objc func ingredientAnalyticsDictForIngredientAtFlattenedIndex(_ flattenedIndex: Int) -> [String: Any] { + let ingr = flatIngredients[flattenedIndex] + return [ + "ingredient_name": ingr.name, + "ingredient_density": ingr.density.analyticsRepresentation, + "ingredient_id": ingr.id.uuidString, + ] + } + + @objc func flattenedIndexForGroupIndex(_ groupIndex: Int, ingredientIndex: Int) -> Int { + flattenedIndexFor(groupIndex: groupIndex, ingredientIndex: ingredientIndex) + } + + @objc func ingredientDictAtFlattenedIndex(_ flattenedIndex: Int) -> [String: Any] { + self[flattenedIndex].toDictionary() + } +} + +struct IngredientRef { + let storedIngredientGroupIndex: Int + let ingredientIndex: Int +} diff --git a/CookSmart/CookSmart/CSIngredient.h b/CookSmart/CookSmart/Model/ObjC/CSIngredient.h similarity index 92% rename from CookSmart/CookSmart/CSIngredient.h rename to CookSmart/CookSmart/Model/ObjC/CSIngredient.h index cae127d..47ca415 100644 --- a/CookSmart/CookSmart/CSIngredient.h +++ b/CookSmart/CookSmart/Model/ObjC/CSIngredient.h @@ -8,13 +8,14 @@ #import +// TODO: Delete this once the remainder of ObjC UI is gone + @class CSUnit; @interface CSIngredient : NSObject + (CSIngredient *)ingredientWithDictionary:(NSDictionary *)rawIngredientDictionary; - (id)initWithName:(NSString*)name density:(float)density lastAccessDate:(NSDate *)lastAccessDate; -- (NSDictionary *)dictionaryForAnalytics; - (NSDictionary *)dictionary; @property (nonatomic, readwrite, strong) NSString *name; @@ -24,6 +25,5 @@ - (float)densityWithVolumeUnit:(CSUnit *)volumeUnit andWeightUnit:(CSUnit *)weightUnit; - (BOOL)isIngredientDensityValid; - (BOOL)isEqualToIngredient:(CSIngredient *)otherIngredient; -- (void)markAccess; @end diff --git a/CookSmart/CookSmart/CSIngredient.m b/CookSmart/CookSmart/Model/ObjC/CSIngredient.m similarity index 61% rename from CookSmart/CookSmart/CSIngredient.m rename to CookSmart/CookSmart/Model/ObjC/CSIngredient.m index 983bade..bc9fee1 100644 --- a/CookSmart/CookSmart/CSIngredient.m +++ b/CookSmart/CookSmart/Model/ObjC/CSIngredient.m @@ -8,10 +8,7 @@ #import "CSIngredient.h" #import "CSUnit.h" - -#define INGREDIENT_KEY_NAME @"Name" -#define INGREDIENT_KEY_DENSITY @"Density" -#define INGREDIENT_KEY_LAST_ACCESS_DATE @"LastAccessDate" +#import "CSSharedConstants.h" @interface CSIngredient() @@ -23,9 +20,9 @@ @implementation CSIngredient - (id)initWithDictionary:(NSDictionary *)rawIngredientDictionary { - return [self initWithName:rawIngredientDictionary[INGREDIENT_KEY_NAME] - density:[rawIngredientDictionary[INGREDIENT_KEY_DENSITY] floatValue] - lastAccessDate:rawIngredientDictionary[INGREDIENT_KEY_LAST_ACCESS_DATE]]; + return [self initWithName:rawIngredientDictionary[IngredientKeyName] + density:[rawIngredientDictionary[IngredientKeyDensity] floatValue] + lastAccessDate:rawIngredientDictionary[IngredientKeyLastAccessDate]]; } - (id)initWithName:(NSString*)name density:(float)density lastAccessDate:(NSDate *)lastAccessDate @@ -47,32 +44,20 @@ + (CSIngredient *)ingredientWithDictionary:(NSDictionary *)rawIngredientDictiona - (NSDictionary *)dictionary { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary: @{ - INGREDIENT_KEY_NAME : self.name, - INGREDIENT_KEY_DENSITY : [NSNumber numberWithFloat:self.density], + IngredientKeyName : self.name, + IngredientKeyDensity : [NSNumber numberWithFloat:self.density], }]; if (self.lastAccessDate) { - [dict setObject:self.lastAccessDate forKey:INGREDIENT_KEY_LAST_ACCESS_DATE]; + [dict setObject:self.lastAccessDate forKey:IngredientKeyLastAccessDate]; } return dict; } -- (NSDictionary *)dictionaryForAnalytics -{ - NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[self dictionary]]; - dict[INGREDIENT_KEY_LAST_ACCESS_DATE] = [dict[INGREDIENT_KEY_LAST_ACCESS_DATE] description]; - return dict; -} - - (float)densityWithVolumeUnit:(CSUnit *)volumeUnit andWeightUnit:(CSUnit *)weightUnit { return self.density*(weightUnit.conversionFactor/volumeUnit.conversionFactor); } -- (void)markAccess -{ - self.lastAccessDate = [NSDate date]; -} - - (BOOL)isIngredientDensityValid { BOOL valid = NO; diff --git a/CookSmart/CookSmart/Model/ObjC/CSSharedConstants.h b/CookSmart/CookSmart/Model/ObjC/CSSharedConstants.h new file mode 100644 index 0000000..219ca11 --- /dev/null +++ b/CookSmart/CookSmart/Model/ObjC/CSSharedConstants.h @@ -0,0 +1,17 @@ +// +// CSSharedConstants.h +// cake +// +// Created by Vova Galchenko on 11/30/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const IngredientKeyName; +extern NSString *const IngredientKeyDensity; +extern NSString *const IngredientKeyLastAccessDate; + +extern NSString *const IngredientDeleteNotificationName; + +NS_ASSUME_NONNULL_END diff --git a/CookSmart/CookSmart/Model/ObjC/CSSharedConstants.m b/CookSmart/CookSmart/Model/ObjC/CSSharedConstants.m new file mode 100644 index 0000000..286043f --- /dev/null +++ b/CookSmart/CookSmart/Model/ObjC/CSSharedConstants.m @@ -0,0 +1,16 @@ +// +// CSSharedConstants.m +// cake +// +// Created by Vova Galchenko on 11/30/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +#import "CSSharedConstants.h" + +NSString *const IngredientKeyName = @"Name"; +NSString *const IngredientKeyDensity = @"Density"; +NSString *const IngredientKeyLastAccessDate = @"LastAccessDate"; + +NSString *const IngredientDeleteNotificationName = @"ingredient_delete_notification"; + diff --git a/CookSmart/CookSmart/CSUnit.h b/CookSmart/CookSmart/Model/ObjC/Units/CSUnit.h similarity index 84% rename from CookSmart/CookSmart/CSUnit.h rename to CookSmart/CookSmart/Model/ObjC/Units/CSUnit.h index eab3525..52095a2 100644 --- a/CookSmart/CookSmart/CSUnit.h +++ b/CookSmart/CookSmart/Model/ObjC/Units/CSUnit.h @@ -8,6 +8,8 @@ #import +// TODO: Move unit definitions out of plist + objc and into swift + @interface CSUnit : NSObject - (id)initWithDictionary:(NSDictionary*)dict; diff --git a/CookSmart/CookSmart/CSUnit.m b/CookSmart/CookSmart/Model/ObjC/Units/CSUnit.m similarity index 100% rename from CookSmart/CookSmart/CSUnit.m rename to CookSmart/CookSmart/Model/ObjC/Units/CSUnit.m diff --git a/CookSmart/CookSmart/CSUnitCollection.h b/CookSmart/CookSmart/Model/ObjC/Units/CSUnitCollection.h similarity index 100% rename from CookSmart/CookSmart/CSUnitCollection.h rename to CookSmart/CookSmart/Model/ObjC/Units/CSUnitCollection.h diff --git a/CookSmart/CookSmart/CSUnitCollection.m b/CookSmart/CookSmart/Model/ObjC/Units/CSUnitCollection.m similarity index 100% rename from CookSmart/CookSmart/CSUnitCollection.m rename to CookSmart/CookSmart/Model/ObjC/Units/CSUnitCollection.m diff --git a/CookSmart/CookSmart/Scale View/ScalesView.swift b/CookSmart/CookSmart/Scale View/ScalesView.swift deleted file mode 100644 index 0b6c2ab..0000000 --- a/CookSmart/CookSmart/Scale View/ScalesView.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// ScalesView.swift -// cake -// -// Created by Alex King on 4/18/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import Combine -import Foundation - -class ScalesView: UIView { - - enum Mode { - case sync - case edit - } - - private(set) var unitConversionFactor: CGFloat { - didSet { - guard mode == .sync else { return } - updateScaleDensity() - } - } - - private let mode: Mode - - init(unitConversionFactor: CGFloat, - syncScales: Bool = true) { - self.unitConversionFactor = unitConversionFactor - mode = syncScales ? .sync : .edit - super.init(frame: .zero) - setupViews() - setUpSubscribers() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - assertionFailure("init(coder:) has not been implemented") - return nil - } - - func updateConversionFactor(_ conversionFactor: CGFloat) { - unitConversionFactor = conversionFactor - updateScaleDensity() - } - - private let volumeScrollView = ScaleScrollView() - private let weightScrollView = ScaleScrollView(unitsPerTile: 100, mirror: true) - private let volumeLabel = Label() - private let weightLabel = Label() - private let volumeCenterLine = CenterLineView() - private let weightCenterLine = CenterLineView() - - private var volumeSubscriber: AnyCancellable? - private var weightSubscriber: AnyCancellable? - private var volumeLabelSubscriber: AnyCancellable? - private var weightLabelSubscriber: AnyCancellable? - - private func setupViews() { - weightScrollView.translatesAutoresizingMaskIntoConstraints = false - - translatesAutoresizingMaskIntoConstraints = false - addSubview(volumeScrollView) - volumeScrollView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - volumeScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true - volumeScrollView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - - addSubview(weightScrollView) - weightScrollView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - weightScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true - weightScrollView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - - volumeScrollView.trailingAnchor.constraint(equalTo: centerXAnchor).isActive = true - weightScrollView.leadingAnchor.constraint(equalTo: centerXAnchor).isActive = true - - addSubview(volumeLabel) - volumeLabel.constrain(to: volumeScrollView, anchors: [.centerX, .centerY]) - - addSubview(weightLabel) - weightLabel.constrain(to: weightScrollView, anchors: [.centerX, .centerY]) - - addSubview(volumeCenterLine) - volumeCenterLine.constrain(to: volumeScrollView, anchors: [.centerY, .leading, .trailing]) - addSubview(weightCenterLine) - weightCenterLine.constrain(to: weightScrollView, anchors: [.centerY, .leading, .trailing]) - - updateScaleDensity() - } - - private func setUpSubscribers() { - switch mode { - case .edit: - volumeSubscriber = Publishers.CombineLatest(volumeScrollView.$unitValue, weightScrollView.$unitValue) - .sink(receiveValue: { - self.unitConversionFactor = $0.1 / $0.0 - }) - case .sync: - volumeSubscriber = volumeScrollView.$unitValue - .filter { _ in self.mode == .sync } - .sink { volumeValue in - self.weightScrollView.syncToUnitValue(volumeValue * self.unitConversionFactor) - } - - weightSubscriber = weightScrollView.$unitValue - .filter { _ in self.mode == .sync } - .sink { weightValue in - self.volumeScrollView.syncToUnitValue(weightValue / self.unitConversionFactor) - } - } - - volumeLabelSubscriber = volumeScrollView.unitText - .assign(to: \.text, on: volumeLabel) - - weightLabelSubscriber = weightScrollView.unitText - .assign(to: \.text, on: weightLabel) - } -} - -extension ScalesView { - private func updateScaleDensity() { - var volumeScale: CGFloat = 1 - - let idealWeightScale = unitConversionFactor - var humanReadableWeightScale: CGFloat = 1 - if idealWeightScale >= 10 { - let orderOfMagnitue = floor(log10(idealWeightScale)) - humanReadableWeightScale = idealWeightScale - idealWeightScale.truncatingRemainder(dividingBy: pow(10, orderOfMagnitue)) - } else { - let idealVolumeScale = 1 / unitConversionFactor - if idealVolumeScale >= 10 { - let orderOfMagnitue = floor(log10(idealVolumeScale)) - volumeScale = idealVolumeScale - idealVolumeScale.truncatingRemainder(dividingBy: pow(10, orderOfMagnitue)) - } - } - - volumeScrollView.unitsPerTile = Int(volumeScale) - weightScrollView.unitsPerTile = Int(humanReadableWeightScale) - weightScrollView.syncToUnitValue(volumeScrollView.unitValue * unitConversionFactor) - } -} diff --git a/CookSmart/CookSmart/ScaleView.swift b/CookSmart/CookSmart/ScaleView.swift deleted file mode 100644 index ac9b779..0000000 --- a/CookSmart/CookSmart/ScaleView.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ScaleView.swift -// cake -// -// Created by Olga Galchenko on 4/12/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import SwiftUI - -struct ScaleView: View { - - var unitButtonText: String - @State var value: Double - var unitButtonTapped: () -> Void - - var valueLabel: some View { - Text(value.vulgarFractionString) - } - - var body: some View { - ZStack { - ScaleScrollViewRepresentable() - VStack { - Button(action: unitButtonTapped) { - Text(unitButtonText) - .font(weight: .regular, size: 20) - } - valueLabel - } - } - } -} - -struct ScalesView_Previews: PreviewProvider { - static var previews: some View { - HStack(spacing: 0) { - ScaleView(unitButtonText: "Cups", value: 0.95, unitButtonTapped: {}) - ScaleView(unitButtonText: "Grams", value: 35.44, unitButtonTapped: {}) - } - } -} diff --git a/CookSmart/CookSmart/ScaleViewController.swift b/CookSmart/CookSmart/ScaleViewController.swift deleted file mode 100644 index bd214f0..0000000 --- a/CookSmart/CookSmart/ScaleViewController.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// ScaleViewController.swift -// cake -// -// Created by Olga Galchenko on 4/11/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import Foundation -import SwiftUI -import UIKit - -class ScaleViewController: UIViewController { - - private enum DisplayMode { - case scales - case unitPicker - } - - init(ingredient: CSIngredient, - volumeUnit: CSUnit = CSUnitCollection.volumeUnits()?.unit(at: 2) ?? CSUnit(), - weightUnit: CSUnit = CSUnitCollection.weightUnits()?.unit(at: 2) ?? CSUnit(), - shouldSyncScales: Bool = true) { - self.ingredient = ingredient - let density = CGFloat(ingredient.density(withVolumeUnit: volumeUnit, andWeightUnit: weightUnit)) - scalesContainer = ScalesView(unitConversionFactor: density, syncScales: shouldSyncScales) - unitPickerView = UnitPickerView(volumeUnit: volumeUnit, weightUnit: weightUnit) - - super.init(nibName: nil, bundle: nil) - - volumeUnitButton.setTitle(volumeUnit.name, for: .normal) - weightUnitButton.setTitle(weightUnit.name, for: .normal) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - setUpViews() - } - - // MARK: Public - - var ingredient: CSIngredient - var density: CGFloat { - get { - scalesContainer.unitConversionFactor - } - set { - scalesContainer.updateConversionFactor(newValue) - } - } - - // MARK: Private - - private var displayMode: DisplayMode = .scales - - private lazy var volumeUnitButton: Button = { - let button = Button() - button.setTitleColor(.label, for: .disabled) - button.setTitle("Volume", for: .disabled) - button.addTarget(self, action: #selector(toggleDisplayMode), for: .touchUpInside) - return button - }() - - private lazy var weightUnitButton: Button = { - let button = Button() - button.setTitleColor(.label, for: .disabled) - button.setTitle("Weight", for: .disabled) - button.addTarget(self, action: #selector(toggleDisplayMode), for: .touchUpInside) - return button - }() - - private let scalesContainer: ScalesView - private let unitPickerView: UnitPickerView - - private var scalesTopConstraint: NSLayoutConstraint? - private var unitPickerBottomConstraint: NSLayoutConstraint? - - private func setUpViews() { - view.clipsToBounds = true - unitPickerView.delegate = self - - setUpScaleViews() - setUpUnitViews() - } - - private func setUpUnitViews() { - let gradientView = GradientView() - view.addSubview(gradientView) - gradientView.constrainToSuperview(anchors: [.leading, .top, .right]) - gradientView.heightAnchor.constraint(equalToConstant: 100).isActive = true - - gradientView.addSubview(volumeUnitButton) - volumeUnitButton.constrainToSuperview(anchors: [.leading, .top]) - - gradientView.addSubview(weightUnitButton) - weightUnitButton.constrainToSuperview(anchors: [.trailing, .top]) - - volumeUnitButton.trailingAnchor.constraint(equalTo: weightUnitButton.leadingAnchor).isActive = true - volumeUnitButton.widthAnchor.constraint(equalTo: weightUnitButton.widthAnchor).isActive = true - } - - private func setUpScaleViews() { - view.addSubview(scalesContainer) - scalesContainer.constrainToSuperview(anchors: [.leading, .trailing, .height]) - scalesTopConstraint = scalesContainer.topAnchor.constraint(equalTo: view.topAnchor) - scalesTopConstraint?.isActive = true - - view.addSubview(unitPickerView) - unitPickerView.constrainToSuperview(anchors: [.leading, .trailing, .height]) - unitPickerBottomConstraint = unitPickerView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - - unitPickerView.topAnchor.constraint(equalTo: scalesContainer.bottomAnchor).isActive = true - } - - @objc - private func toggleDisplayMode() { - guard let scalesTopConstraint = scalesTopConstraint, - let unitPickerBottomConstraint = unitPickerBottomConstraint - else { - return - } - displayMode = (displayMode == .scales) ? .unitPicker : .scales - UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { - scalesTopConstraint.isActive = self.displayMode == .scales - unitPickerBottomConstraint.isActive = self.displayMode == .unitPicker - self.view.layoutIfNeeded() - }.startAnimation() - weightUnitButton.isEnabled = displayMode == .scales - volumeUnitButton.isEnabled = displayMode == .scales - } -} - -extension ScaleViewController: UnitPickerDelegate { - func picked(volumeUnit: CSUnit, weightUnit: CSUnit) { - volumeUnitButton.setTitle(volumeUnit.name, for: .normal) - weightUnitButton.setTitle(weightUnit.name, for: .normal) - - density = CGFloat(ingredient.density(withVolumeUnit: volumeUnit, andWeightUnit: weightUnit)) - toggleDisplayMode() - } -} diff --git a/CookSmart/CookSmart/SceneDelegate.swift b/CookSmart/CookSmart/SceneDelegate.swift index 0687251..f60f819 100644 --- a/CookSmart/CookSmart/SceneDelegate.swift +++ b/CookSmart/CookSmart/SceneDelegate.swift @@ -8,6 +8,8 @@ import UIKit +// TODO: Instrument scene lifecycle with analytics +// TODO: do an audit of all things analytics class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? diff --git a/CookSmart/CookSmart/Edit Ingredient/EditIngredientViewController.swift b/CookSmart/CookSmart/UI/EditIngredientViewController.swift similarity index 52% rename from CookSmart/CookSmart/Edit Ingredient/EditIngredientViewController.swift rename to CookSmart/CookSmart/UI/EditIngredientViewController.swift index f949f72..b801d47 100644 --- a/CookSmart/CookSmart/Edit Ingredient/EditIngredientViewController.swift +++ b/CookSmart/CookSmart/UI/EditIngredientViewController.swift @@ -6,7 +6,7 @@ // Copyright © 2020 Olga Galchenko. All rights reserved. // -import Foundation +import Combine class EditIngredientViewController: UIViewController { @@ -15,24 +15,22 @@ class EditIngredientViewController: UIViewController { case add } - private let ingredient: CSIngredient - private let editingMode: EditingMode - private var density: Float { - Float(scaleViewController.density) - } + private let inputIngredient: Ingredient? + private weak var delegate: EditIngredientViewControllerDelegate? + private var densityRx: AnyCancellable? - @objc - public init(ingredient: CSIngredient? = nil) { - if let ingredient = ingredient { - self.ingredient = ingredient - editingMode = .edit - } else { - self.ingredient = CSIngredient(name: "", - density: 150, - lastAccessDate: Date()) - editingMode = .add - } + public init(ingredient: Ingredient? = nil, delegate: EditIngredientViewControllerDelegate?) { + inputIngredient = ingredient + self.delegate = delegate super.init(nibName: nil, bundle: nil) + + ingredientNameField.delegate = self + + DispatchQueue.main.async { + self.densityRx = self.scaleViewController.$currentDensity.sink { + self.notifyDelegate(withNewDensity: $0) + } + } } @available(*, unavailable) @@ -48,31 +46,31 @@ class EditIngredientViewController: UIViewController { textField.textAlignment = .center textField.placeholder = "Ingredient Name" textField.autocapitalizationType = .words - textField.font = AvenirFont.medium.of(size: 20) - textField.textColor = .label - textField.tintColor = Color.redLineColor + textField.applyTextStyle(.heading) return textField }() - private lazy var scaleViewController = ScaleViewController(ingredient: ingredient, shouldSyncScales: false) + private lazy var scaleViewController = ScaleViewController( + density: inputIngredient?.density ?? Density(inGramsPerCup: 150), + shouldSyncScales: false + ) override func viewDidLoad() { super.viewDidLoad() setupViews() - addBarButtonItems() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if ingredient.name.isEmpty { + if (inputIngredient?.name ?? "").isEmpty { ingredientNameField.becomeFirstResponder() } } private func setupViews() { - view.backgroundColor = Color.background + view.backgroundColor = CSColor.background.asUIColor() - ingredientNameField.text = ingredient.name + ingredientNameField.text = inputIngredient?.name ?? "" ingredientNameField.delegate = self view.addSubview(ingredientNameField) ingredientNameField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true @@ -90,55 +88,6 @@ class EditIngredientViewController: UIViewController { scaleViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true } - private func addBarButtonItems() { - let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, - target: self, - action: #selector(cancelButtonPressed)) - cancelButton.tintColor = Color.redLineColor - navigationItem.leftBarButtonItem = cancelButton - - let doneButton = UIBarButtonItem(barButtonSystemItem: .done, - target: self, - action: #selector(doneButtonPressed)) - doneButton.tintColor = Color.redLineColor - navigationItem.rightBarButtonItem = doneButton - } - - @objc - private func cancelButtonPressed() { - navigationController?.popViewController(animated: true) - } - - @objc - private func doneButtonPressed() { - guard let ingredientName = ingredientNameField.text, !ingredientName.isEmpty else { - ingredientNameField.becomeFirstResponder() - return - } - - guard densityIsValid else { - displayInvalidDensityAlert() - logUserAction("ingredient_persist_fail", analyticsDictionary) - return - } - - updateIngredient() - switch editingMode { - case .add: - CSIngredients.sharedInstance()?.add(ingredient) - case .edit: - CSIngredients.sharedInstance()?.persist() - } - - navigationController?.popViewController(animated: true) - } - - private func updateIngredient() { - ingredient.name = ingredientNameField.text - ingredient.density = density - ingredient.markAccess() - } - private func displayInvalidDensityAlert() { let alertController = UIAlertController(title: "Error", message: "Choose a weight greater than 0.", @@ -150,10 +99,21 @@ class EditIngredientViewController: UIViewController { present(alertController, animated: true) } - private var densityIsValid: Bool { - !density.isNaN - && !density.isInfinite - && !density.isZero + private func notifyDelegate( + withNewIngredientName ingredientName: String? = nil, + withNewDensity density: Density? = nil + ) { + delegate?.editViewControllerDidGenerate( + ingredient: Ingredient( + id: inputIngredient?.id ?? UUID(), + name: + ingredientName ?? + ingredientNameField.text.flatMap { $0 == "" ? nil : $0 } ?? + inputIngredient?.name ?? "", + density: density ?? scaleViewController.currentDensity, + lastAccessDate: Date() + ) + ) } } @@ -165,17 +125,27 @@ extension EditIngredientViewController: UITextFieldDelegate { ingredientNameField.resignFirstResponder() return true } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let oldText: NSString = (textField.text ?? "") as NSString + let newText = oldText.replacingCharacters(in: range, with: string) as String + notifyDelegate(withNewIngredientName: newText) + return true + } } // MARK: Analytics extension EditIngredientViewController { private var analyticsDictionary: [String: Any] { - let ingredientName = ingredientNameField.text ?? ingredient.name ?? "" - let ingredientDensity = densityIsValid ? density : Float.infinity + let ingredientName = ingredientNameField.text ?? inputIngredient?.name ?? "" return [ "ingredient_name": ingredientName, - "ingredient_density": ingredientDensity, + "ingredient_density": scaleViewController.currentDensity.analyticsRepresentation, ] } } + +protocol EditIngredientViewControllerDelegate: AnyObject { + func editViewControllerDidGenerate(ingredient: Ingredient) +} diff --git a/CookSmart/CookSmart/UI/IngredientListView.swift b/CookSmart/CookSmart/UI/IngredientListView.swift new file mode 100644 index 0000000..01807b1 --- /dev/null +++ b/CookSmart/CookSmart/UI/IngredientListView.swift @@ -0,0 +1,265 @@ +// +// IngredientListView.swift +// cake +// +// Created by Vova Galchenko on 11/25/23. +// Copyright © 2023 Olga Galchenko. All rights reserved. +// + +import SwiftUI + +struct IngredientListView: View { + @StateObject private var ingredientsStore: IngredientsStore + @State private var searchText: String = "" + @State private var isShowingResetAlert = false + private var ingredientGroupsToPresent: [any IngredientGroup] { + let relevantGroups = if searchText.isEmpty { + ingredientsStore.ingredientGroups + } else { + ingredientsStore.ingredientGroups.compactMap { group in + group.filter(searchString: searchText) + } + } + return relevantGroups.filter { !$0.ingredients.isEmpty } + } + + weak var delegate: IngredientListViewDelegate? + + init( + ingredientsStore: @escaping @autoclosure () -> IngredientsStore = IngredientsStore.shared, + delegate: IngredientListViewDelegate? = nil + ) { + _ingredientsStore = StateObject(wrappedValue: ingredientsStore()) + self.delegate = delegate + } + + var body: some View { + NavigationView { + List { + ForEach(ingredientGroupsToPresent, id: \.id) { group in + // I actually think in this case it's more readable to keep this code inline, + // than to extract it into IngredientGroupSection, because of the strong ties + // it has back to this struct, but the compiler crashes when the section code + // is here inline :( + IngredientGroupSection(group: group, delegate: delegate) + } + } + .listStyle(PlainListStyle()) + // O&A Question: There's apparently no way to customize the appearance of the search bar... what do you suggest? + .searchable(text: $searchText, prompt: "ingredient name") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button( + action: { + self.delegate?.ingredientListViewSelected(ingredientAtFlattenedIndex: 0) + }, + label: { + Image(systemName: "xmark") + .foregroundStyle(CSColor.accent.asSwiftUIColor()) + } + ) + } + ToolbarItem(placement: .principal) { + Text("Ingredients").csTextStyle(.heading) + } + ToolbarItem(placement: .topBarTrailing) { + NavigationLink( + destination: EditIngredientViewWithToolbar(), + label: { + Image(systemName: "plus") + .foregroundStyle(CSColor.accent.asSwiftUIColor()) + } + ) + } + ToolbarItem(placement: .bottomBar) { + Button(action: { + isShowingResetAlert = true + }, label: { + Text("Reset to Defaults").csTextStyle(.plainButton) + }) + } + } + .alert("Are you sure?", isPresented: $isShowingResetAlert) { + Button("Reset", role: .destructive) { + isShowingResetAlert = false + // O&A Question: Why does this not trigger a nice animation? + ingredientsStore.resetToDefault() + } + } message: { + Text("Resetting to defaults will remove all your added and edited ingredients") + } + } + } +} + +struct IngredientGroupSection: View { + let group: any IngredientGroup + weak var delegate: IngredientListViewDelegate? + + var body: some View { + Section(header: Text(group.name).csTextStyle(.subheading)) { + ForEach(group.ingredients) { ingredient in + IngredientListViewCell( + ingredient: ingredient, + delegate: delegate + ) + }.onDelete { indexSet in + // O&A Question: the animation is not great when an ingredient is removed that's both in recents + // and in one of the groups. Is there a smoother way to do this? + let groupIngrs = group.ingredients + IngredientsStore.shared.delete(ingredientsWithIds: indexSet.map { groupIngrs[$0].id }) + } + } + } +} + +struct IngredientListViewCell: View { + var ingredient: Ingredient! + weak var delegate: IngredientListViewDelegate? + + var body: some View { + HStack { + HStack { + Text(ingredient.name).csTextStyle(.coreContent) + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + self.delegate?.ingredientListViewSelected( + ingredientAtFlattenedIndex: UInt(IngredientsStore.shared.flattenedForIngredient(withId: ingredient.id)) + ) + } + Image(systemName: "info.circle") + .foregroundStyle(CSColor.accent.asSwiftUIColor()) + // O&A Question: Do you guys know of a less hacky way to accomplish something seemingly so basic. + // Even with this, the tap hit box for editing ingredients is still not perfect :( + // Also, it turns out that for some reason long-tapping anywhere in the cell triggers this action :( + .overlay( + NavigationLink( + destination: EditIngredientViewWithToolbar(ingredient: ingredient), + label: { EmptyView() } + ) + .opacity(0) + ) + } + } +} + +struct EditIngredientViewWithToolbar: View { + @State var ingredient: Ingredient? + @Environment(\.presentationMode) var presentationMode: Binding + + init(ingredient: Ingredient? = nil) { + _ingredient = State(initialValue: ingredient) + } + + var body: some View { + EditIngredientView(ingredient: $ingredient) + .ignoresSafeArea() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button( + action: { + self.presentationMode.wrappedValue.dismiss() + }, + label: { + Text("Cancel").csTextStyle(.plainButton) + } + ) + } + ToolbarItem(placement: .confirmationAction) { + Button( + action: { + if let existingIngredient = ingredient { + IngredientsStore.shared.upsert(existingIngredient) + } + self.presentationMode.wrappedValue.dismiss() + }, + label: { + Text("Done").csTextStyle(.actionButton) + } + ) + } + } + } +} + +struct EditIngredientView: UIViewControllerRepresentable { + + @Binding var ingredient: Ingredient? + + class Coordinator: NSObject, EditIngredientViewControllerDelegate { + @Binding var ingredient: Ingredient? + + init(ingredientBinding: Binding) { + _ingredient = ingredientBinding + } + + func editViewControllerDidGenerate(ingredient: Ingredient) { + if self.ingredient != ingredient { + self.ingredient = ingredient + } + } + } + + func makeUIViewController(context: Context) -> some UIViewController { + EditIngredientViewController(ingredient: ingredient, delegate: context.coordinator) + } + + func makeCoordinator() -> Coordinator { + Coordinator(ingredientBinding: $ingredient) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} +} + +// This is to be removed when we no longer need to create this in ObjC +@objc class IngredientListViewControllerFactory: NSObject { + @objc static func create(delegate: IngredientListViewDelegate) -> UIViewController { + UIHostingController(rootView: IngredientListView(delegate: delegate)) + } +} + +@objc protocol IngredientListViewDelegate { + @objc(ingredientListViewSelectedIngredientAtFlattenedIndex:) func ingredientListViewSelected( + ingredientAtFlattenedIndex flattenedIndex: UInt + ) +} + +#Preview { + IngredientListView(ingredientsStore: IngredientsStore(testData: [ + StoredIngredientGroup(name: "Flour", ingredients: [ + Ingredient( + name: "All-Purpose Flour", + density: Density(inGramsPerCup: 125), + lastAccessDate: Date(timeIntervalSince1970: TimeInterval(1_701_047_250)) + ), + Ingredient(name: "Almond Flour", density: Density(inGramsPerCup: 95.67959999999)), + Ingredient(name: "Barley Flour", density: Density(inGramsPerCup: 113.398)), + Ingredient(name: "Bread Flour", density: Density(inGramsPerCup: 120.485)), + ]), + StoredIngredientGroup(name: "Sugars", ingredients: [ + Ingredient(name: "Brown Sugar, light/dark", density: Density(inGramsPerCup: 198)), + Ingredient(name: "Powdered Sugar", density: Density(inGramsPerCup: 113)), + ]), + StoredIngredientGroup(name: "Oil and Shortening", ingredients: [ + Ingredient( + name: "Butter", + density: Density(inGramsPerCup: 227), + lastAccessDate: Date(timeIntervalSince1970: TimeInterval(1_701_047_255)) + ), + Ingredient(name: "Canola Oil", density: Density(inGramsPerCup: 219.5)), + Ingredient(name: "Coconut Oil", density: Density(inGramsPerCup: 216)), + Ingredient(name: "Lard", density: Density(inGramsPerCup: 205)), + Ingredient(name: "Peanut Oil", density: Density(inGramsPerCup: 222)), + Ingredient( + name: "Olive Oil", + density: Density(inGramsPerCup: 219.5), + lastAccessDate: Date(timeIntervalSince1970: TimeInterval(1_701_047_244)) + ), + Ingredient(name: "Vegetable Shortening", density: Density(inGramsPerCup: 190)), + ]), + ])) +} diff --git a/CookSmart/CookSmart/CSConversionVC.h b/CookSmart/CookSmart/UI/ObjC/CSConversionVC.h similarity index 66% rename from CookSmart/CookSmart/CSConversionVC.h rename to CookSmart/CookSmart/UI/ObjC/CSConversionVC.h index ac0d54a..2484e19 100644 --- a/CookSmart/CookSmart/CSConversionVC.h +++ b/CookSmart/CookSmart/UI/ObjC/CSConversionVC.h @@ -7,13 +7,10 @@ // #import -#import "CSIngredientListVC.h" #import "CSScaleView.h" #import "CSScaleVC.h" -@class CSIngredientGroup; - -@interface CSConversionVC : UIViewController +@interface CSConversionVC : UIViewController - (id)initWithIngredientGroupIndex:(NSUInteger)ingredientGroupIndex ingredientIndex:(NSUInteger)ingredientIndex; diff --git a/CookSmart/CookSmart/UI/ObjC/CSConversionVC.m b/CookSmart/CookSmart/UI/ObjC/CSConversionVC.m new file mode 100644 index 0000000..9ff44e1 --- /dev/null +++ b/CookSmart/CookSmart/UI/ObjC/CSConversionVC.m @@ -0,0 +1,262 @@ +// +// CSConversionVC.m +// CookSmart +// +// Created by Olga Galchenko on 1/23/14. +// Copyright (c) 2014 Olga Galchenko. All rights reserved. +// + +#import "CSConversionVC.h" +#import "CSIngredient.h" +#import "CSScaleView.h" +#import "CSUnit.h" +#import "CSUnitCollection.h" +#import "CSScaleVC.h" +#import "cake-Swift.h" +#import "CSSharedConstants.h" + +#define CHOOSE_UNITS_TEXT @"Choose Units" + +@interface CSConversionVC () + +@property (nonatomic, readwrite, assign) NSUInteger ingredientIndex; +@property (weak, nonatomic) IBOutlet UIScrollView *ingredientPickerScrollView; + +@property (strong, nonatomic) IBOutlet CSScaleVC* scaleVC; + +@end + +@implementation CSConversionVC + +- (id)initWithIngredientGroupIndex:(NSUInteger)ingredientGroupIndex ingredientIndex:(NSUInteger)ingredientIndex +{ + self = [super initWithNibName:@"CSConversionVC" bundle:nil]; + if (self) + { + self.ingredientIndex = [[IngredientsStore shared] flattenedIndexForGroupIndex:ingredientGroupIndex ingredientIndex:ingredientIndex]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(ingredientDeleted:) + name:IngredientDeleteNotificationName + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - View Lifecycle Management + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self addChildViewController:self.scaleVC]; + [self.view addSubview:self.scaleVC.view]; + self.scaleVC.view.translatesAutoresizingMaskIntoConstraints = NO; + self.scaleVC.delegate = self; + NSLayoutConstraint* bottom = [NSLayoutConstraint constraintWithItem:self.scaleVC.view + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0]; + NSLayoutConstraint* left = [NSLayoutConstraint constraintWithItem:self.scaleVC.view + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:0]; + NSLayoutConstraint* right = [NSLayoutConstraint constraintWithItem:self.scaleVC.view + attribute:NSLayoutAttributeRight + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:0]; + NSLayoutConstraint* top = [NSLayoutConstraint constraintWithItem:self.scaleVC.view + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.ingredientPickerScrollView + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:10]; + [self.view addConstraints:@[bottom, left, right, top]]; + self.ingredientPickerScrollView.scrollsToTop = NO; + self.ingredientPickerScrollView.showsHorizontalScrollIndicator = NO; + self.ingredientPickerScrollView.showsVerticalScrollIndicator = NO; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + // When we first appear, always select the very first ingredient – the one most recently selected. + [self selectIngredientAtIndex:self.ingredientIndex]; + logViewChange(@"conversion", [self.scaleVC analyticsAttributes]); +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self markCurrentIngredientAccess]; +} + +- (void)markCurrentIngredientAccess +{ + if (self.isViewLoaded && self.view.window != nil) { + [[IngredientsStore shared] markAccessOfIngredientAtFlattenedIndex:self.ingredientIndex]; + } +} + +#pragma mark - Ingredient Picker + +- (NSString *)nameForIngredientAtXOrigin:(CGFloat)xOrigin +{ + NSUInteger indexOfIngredient = (NSUInteger) (xOrigin/self.ingredientPickerScrollView.bounds.size.width); + return [[IngredientsStore shared] ingredientNameAtFlattenedIndex:indexOfIngredient]; +} + +- (void)refreshIngredientNameUI +{ + self.ingredientPickerScrollView.contentSize = CGSizeMake(self.ingredientPickerScrollView.bounds.size.width*[[IngredientsStore shared] flattenedCountOfIngredients], + self.ingredientPickerScrollView.bounds.size.height); + for (UIView *subview in self.ingredientPickerScrollView.subviews) + { + [subview removeFromSuperview]; + } + CGFloat initialXOffset = self.ingredientPickerScrollView.bounds.size.width*self.ingredientIndex; + for (CGFloat xOrigin = initialXOffset; xOrigin <= initialXOffset + 2*self.ingredientPickerScrollView.bounds.size.width; xOrigin += self.ingredientPickerScrollView.bounds.size.width) + { + UIButton *ingredientButton = [UIButton buttonWithType:UIButtonTypeSystem]; + ingredientButton.frame = CGRectMake(xOrigin, 0, self.ingredientPickerScrollView.bounds.size.width, self.ingredientPickerScrollView.bounds.size.height); + [ingredientButton setTitle:[self nameForIngredientAtXOrigin:xOrigin] forState:UIControlStateNormal]; + ingredientButton.titleLabel.font = [UIFont fontWithName:@"AvenirNext-Medium" size:MAJOR_BUTTON_FONT_SIZE]; + [ingredientButton setTitleColor:RED_LINE_COLOR forState:UIControlStateNormal]; + [ingredientButton setTitleColor:[UIColor blackColor] forState:UIControlStateDisabled]; + [ingredientButton addTarget:self action:@selector(handleIngredientTap:) forControlEvents:UIControlEventTouchUpInside]; + [self.ingredientPickerScrollView addSubview:ingredientButton]; + } + self.ingredientPickerScrollView.contentOffset = CGPointMake(initialXOffset, 0); +} + +- (void)handleIngredientTap:(id)sender +{ + UIViewController* ingredientListVC = [IngredientListViewControllerFactory createWithDelegate: self]; + ingredientListVC.modalPresentationStyle = UIModalPresentationPageSheet; + [self presentViewController:ingredientListVC animated:YES completion:nil]; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + CSAssert(scrollView == self.ingredientPickerScrollView, @"conversion_vc_wrong_scrollview_delegate", + @"CSConversionVC doesn't expect to be delegate of a scrollview other than its ingredientPickerScrollView"); + CGFloat minVisibleX = self.ingredientPickerScrollView.contentOffset.x; + CGFloat maxVisibleX = minVisibleX + self.ingredientPickerScrollView.bounds.size.width; + NSUInteger numSubviews = self.ingredientPickerScrollView.subviews.count; + CGFloat contentSize = self.ingredientPickerScrollView.contentSize.width; + for (UIButton *button in self.ingredientPickerScrollView.subviews) + { + if (button.frame.origin.x + button.bounds.size.width < minVisibleX && + button.frame.origin.x + (numSubviews + 1)*button.bounds.size.width <= contentSize && + button.frame.origin.x + numSubviews*button.bounds.size.width) + { + button.frame = CGRectMake(button.frame.origin.x + numSubviews*button.bounds.size.width, 0, button.frame.size.width, button.frame.size.height); + [button setTitle:[self nameForIngredientAtXOrigin:button.frame.origin.x] forState:UIControlStateNormal]; + } + if (button.frame.origin.x > maxVisibleX && + button.frame.origin.x - numSubviews*button.bounds.size.width >= 0 && + button.frame.origin.x + (1 - numSubviews)*button.bounds.size.width >= minVisibleX) + { + button.frame = CGRectMake(button.frame.origin.x - numSubviews*button.bounds.size.width, 0, button.frame.size.width, button.frame.size.height); + [button setTitle:[self nameForIngredientAtXOrigin:button.frame.origin.x] forState:UIControlStateNormal]; + } + } + + CGFloat distanceToSnap = remainder(self.ingredientPickerScrollView.contentOffset.x, self.ingredientPickerScrollView.bounds.size.width); + CGFloat distanceToMiddle = (self.ingredientPickerScrollView.bounds.size.width/2) - fabs(distanceToSnap); + CGFloat scaleViewAlpha = distanceToMiddle/(self.ingredientPickerScrollView.bounds.size.width/2); + [self.scaleVC setScalesAlpha:scaleViewAlpha]; + + NSUInteger projectedIndex = (int)round(self.ingredientPickerScrollView.contentOffset.x/self.ingredientPickerScrollView.bounds.size.width); + projectedIndex = MIN(MAX(0, projectedIndex), self.ingredientPickerScrollView.contentSize.width/self.ingredientPickerScrollView.bounds.size.width - 1); + if (projectedIndex != self.ingredientIndex) + { + self.ingredientIndex = projectedIndex; + [self refreshScalesWithCurrentIngredient]; + [self markCurrentIngredientAccess]; + logUserAction(@"ingredient_switch", [self.scaleVC analyticsAttributes]); + } +} + +- (void)ingredientListViewSelectedIngredientAtFlattenedIndex:(NSUInteger)flattenedIndex { + logUserAction( + @"ingredient_select", + [[IngredientsStore shared] ingredientAnalyticsDictForIngredientAtFlattenedIndex:flattenedIndex] + ); + [self selectIngredientAtIndex: flattenedIndex]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)selectIngredientAtIndex:(NSUInteger)ingredientIndex +{ + self.ingredientIndex = ingredientIndex; + [self refreshIngredientNameUI]; + [self refreshScalesWithCurrentIngredient]; + [self markCurrentIngredientAccess]; +} + +- (void)refreshScalesWithCurrentIngredient +{ + self.scaleVC.ingredient = [CSIngredient ingredientWithDictionary: + [[IngredientsStore shared] ingredientDictAtFlattenedIndex:self.ingredientIndex] + ]; +} + +#pragma mark - Notifications + +- (void)ingredientDeleted:(NSNotification *)notification +{ + // When an ingredient is deleted, our index into the ingredient group might change. + // In the future we might want to put a better solution for this, but for now, we'll + // just select the very first ingredient of the very first ingredient group and be done + // with it. + [self selectIngredientAtIndex:0]; +} + +#pragma mark - scaleVC delegate methods + +- (void)scaleVCDidBeginChangingUnits:(CSScaleVC*)scaleVC +{ + [self iterateOverIngredientButtons:^(UIButton *ingredientButton) { + ingredientButton.enabled = NO; + [ingredientButton setTitle:CHOOSE_UNITS_TEXT forState:UIControlStateNormal]; + }]; + [self.ingredientPickerScrollView setScrollEnabled:NO]; +} + +- (void)scaleVCDidFinishChangingUnits:(CSScaleVC *)scaleVC +{ + [self iterateOverIngredientButtons:^(UIButton *ingredientButton) { + ingredientButton.enabled = YES; + [ingredientButton setTitle:[self nameForIngredientAtXOrigin:ingredientButton.frame.origin.x] forState:UIControlStateNormal]; + }]; + [self.ingredientPickerScrollView setScrollEnabled:YES]; +} + +- (void)iterateOverIngredientButtons:(void (^)(UIButton *))work +{ + for (UIButton *ingredientButton in self.ingredientPickerScrollView.subviews) + { + if ([ingredientButton isKindOfClass:[UIButton class]]) + { + work(ingredientButton); + } + } +} + +@end diff --git a/CookSmart/CookSmart/CSConversionVC.xib b/CookSmart/CookSmart/UI/ObjC/CSConversionVC.xib similarity index 100% rename from CookSmart/CookSmart/CSConversionVC.xib rename to CookSmart/CookSmart/UI/ObjC/CSConversionVC.xib diff --git a/CookSmart/CookSmart/CenterLineView.swift b/CookSmart/CookSmart/UI/Scales/CenterLineView.swift similarity index 93% rename from CookSmart/CookSmart/CenterLineView.swift rename to CookSmart/CookSmart/UI/Scales/CenterLineView.swift index 53f7fc1..c15e933 100644 --- a/CookSmart/CookSmart/CenterLineView.swift +++ b/CookSmart/CookSmart/UI/Scales/CenterLineView.swift @@ -32,7 +32,7 @@ class CenterLineView: UIView { let leftLine = UIView() addSubview(leftLine) leftLine.translatesAutoresizingMaskIntoConstraints = false - leftLine.backgroundColor = Color.redLineColor + leftLine.backgroundColor = CSColor.accent.asUIColor() leftLine.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true leftLine.topAnchor.constraint(equalTo: topAnchor).isActive = true leftLine.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true @@ -42,7 +42,7 @@ class CenterLineView: UIView { let rightLine = UIView() addSubview(rightLine) rightLine.translatesAutoresizingMaskIntoConstraints = false - rightLine.backgroundColor = Color.redLineColor + rightLine.backgroundColor = CSColor.accent.asUIColor() rightLine.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true rightLine.topAnchor.constraint(equalTo: topAnchor).isActive = true rightLine.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true diff --git a/CookSmart/CookSmart/GradientView.swift b/CookSmart/CookSmart/UI/Scales/GradientView.swift similarity index 90% rename from CookSmart/CookSmart/GradientView.swift rename to CookSmart/CookSmart/UI/Scales/GradientView.swift index 18e20be..47f727b 100644 --- a/CookSmart/CookSmart/GradientView.swift +++ b/CookSmart/CookSmart/UI/Scales/GradientView.swift @@ -11,7 +11,10 @@ import UIKit class GradientView: UIView { - init(topColor: UIColor = Color.background, bottomColor: UIColor = Color.background.withAlphaComponent(0)) { + init( + topColor: UIColor = CSColor.background.asUIColor(), + bottomColor: UIColor = CSColor.background.asUIColor().withAlphaComponent(0) + ) { colors = [topColor, bottomColor] super.init(frame: .zero) diff --git a/CookSmart/CookSmart/CSGlassView.h b/CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.h similarity index 83% rename from CookSmart/CookSmart/CSGlassView.h rename to CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.h index a3e907c..7ad0a84 100644 --- a/CookSmart/CookSmart/CSGlassView.h +++ b/CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.h @@ -12,4 +12,6 @@ @property (nonatomic, weak) IBOutlet UIView *viewToMagnify; +- (id)initWithMagnifiedView: (UIView *)magnifiedView; + @end diff --git a/CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.m b/CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.m new file mode 100644 index 0000000..52b5ad4 --- /dev/null +++ b/CookSmart/CookSmart/UI/Scales/ObjC/CSGlassView.m @@ -0,0 +1,143 @@ +// +// CSMagnifyingView.m +// CookSmart +// +// Created by Vova Galchenko on 2/27/14. +// Copyright (c) 2014 Olga Galchenko. All rights reserved. +// + +#import "CSGlassView.h" + +#define SHADOW_SIZE 5 +#define MAGNIFYING_FACTOR 1.1 + +@interface CSGlassView() + +@property (nonatomic, weak) UIView *magnifiedView; +@property (nonatomic, weak) UIView *glassening; +@property (nonatomic, weak) CADisplayLink *displayLink; +@property (nonatomic) BOOL isLaidOutViaIB; + +@end + +@implementation CSGlassView + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + if (self) { + self.isLaidOutViaIB = YES; + [self configureView]; + } + return self; +} + +- (id)initWithMagnifiedView: (UIView *)viewToMagnify +{ + self = [super initWithFrame:CGRectZero]; + if (self) { + self.translatesAutoresizingMaskIntoConstraints = NO; + self.isLaidOutViaIB = NO; + self.viewToMagnify = viewToMagnify; + [self configureView]; + } + return self; +} + +- (void)configureView +{ + self.backgroundColor = UIColor.whiteColor; + self.clipsToBounds = NO; + self.opaque = NO; + self.layer.masksToBounds = NO; + self.layer.shadowColor = [[UIColor blackColor] CGColor]; + self.layer.shadowOffset = CGSizeMake(0, SHADOW_SIZE); + self.layer.shadowOpacity = 0.075; + self.layer.shadowRadius = 10; + + UIView *glassening = [[UIView alloc] init]; + glassening.translatesAutoresizingMaskIntoConstraints = self.isLaidOutViaIB; + [self addSubview: glassening]; + + self.glassening = glassening; + self.glassening.opaque = NO; + self.glassening.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.5 alpha:0.025]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(observeSceneLifecycle:) + name:UISceneWillEnterForegroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(observeSceneLifecycle:) + name:UISceneDidEnterBackgroundNotification + object:nil]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)willMoveToWindow:(UIWindow *)newWindow { + if (newWindow) { + [self startDisplayLink]; + } else { + [self.displayLink invalidate]; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.glassening.frame = self.bounds; +} + +- (void)observeSceneLifecycle: (NSNotification *)sceneLifecycleChangeNotification +{ + if (((UIScene *)[sceneLifecycleChangeNotification object]) == self.window.windowScene) { + if ([[sceneLifecycleChangeNotification name] isEqualToString:UISceneDidEnterBackgroundNotification]) { + [self.displayLink invalidate]; + } else if ([[sceneLifecycleChangeNotification name] isEqualToString:UISceneWillEnterForegroundNotification]) { + if (!self.displayLink) { + [self startDisplayLink]; + } + } + } +} + +- (void)startDisplayLink { + CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(refreshMagnifiedView)]; + [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + self.displayLink = displayLink; +} + + +- (void)refreshMagnifiedView +{ + [self.magnifiedView removeFromSuperview]; + + CGPoint imageUnderGlassOrigin = [self.viewToMagnify convertPoint:CGPointMake(0, 0) fromView:self]; + CGFloat widthIncrease = (MAGNIFYING_FACTOR - 1)*self.bounds.size.width; + CGFloat heightIncrease = (MAGNIFYING_FACTOR - 1)*self.bounds.size.height; + + UIView *magnifiedView = [self.viewToMagnify resizableSnapshotViewFromRect:CGRectMake( + imageUnderGlassOrigin.x + widthIncrease/2, + imageUnderGlassOrigin.y + heightIncrease/2, + self.bounds.size.width - widthIncrease, + self.bounds.size.height - heightIncrease + ) + afterScreenUpdates:NO + withCapInsets:UIEdgeInsetsZero]; + + magnifiedView.translatesAutoresizingMaskIntoConstraints = self.isLaidOutViaIB; + magnifiedView.frame = self.bounds; + [self insertSubview:magnifiedView atIndex:0]; + self.magnifiedView = magnifiedView; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + // Pass through all events + return NO; +} + +@end diff --git a/CookSmart/CookSmart/CSGradientView.h b/CookSmart/CookSmart/UI/Scales/ObjC/CSGradientView.h similarity index 100% rename from CookSmart/CookSmart/CSGradientView.h rename to CookSmart/CookSmart/UI/Scales/ObjC/CSGradientView.h diff --git a/CookSmart/CookSmart/CSGradientView.m b/CookSmart/CookSmart/UI/Scales/ObjC/CSGradientView.m similarity index 100% rename from CookSmart/CookSmart/CSGradientView.m rename to CookSmart/CookSmart/UI/Scales/ObjC/CSGradientView.m diff --git a/CookSmart/CookSmart/CSScaleVC.h b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.h similarity index 100% rename from CookSmart/CookSmart/CSScaleVC.h rename to CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.h diff --git a/CookSmart/CookSmart/CSScaleVC.m b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.m similarity index 100% rename from CookSmart/CookSmart/CSScaleVC.m rename to CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.m diff --git a/CookSmart/CookSmart/CSScaleVC.xib b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.xib similarity index 81% rename from CookSmart/CookSmart/CSScaleVC.xib rename to CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.xib index 743631b..ec42567 100644 --- a/CookSmart/CookSmart/CSScaleVC.xib +++ b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVC.xib @@ -1,7 +1,10 @@ - - + + + - + + + @@ -25,62 +28,55 @@ - - - - + - - + - - - + + - + @@ -103,40 +99,37 @@ - - - - + @@ -151,7 +144,7 @@ - + @@ -161,12 +154,10 @@ - - - - + + @@ -175,7 +166,7 @@ - + @@ -185,6 +176,9 @@ + + + - \ No newline at end of file + diff --git a/CookSmart/CookSmart/CSScaleVCInternals.h b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVCInternals.h similarity index 100% rename from CookSmart/CookSmart/CSScaleVCInternals.h rename to CookSmart/CookSmart/UI/Scales/ObjC/CSScaleVCInternals.h diff --git a/CookSmart/CookSmart/CSScaleView.h b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleView.h similarity index 100% rename from CookSmart/CookSmart/CSScaleView.h rename to CookSmart/CookSmart/UI/Scales/ObjC/CSScaleView.h diff --git a/CookSmart/CookSmart/CSScaleView.m b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleView.m similarity index 98% rename from CookSmart/CookSmart/CSScaleView.m rename to CookSmart/CookSmart/UI/Scales/ObjC/CSScaleView.m index 6cc89b2..72c0be6 100644 --- a/CookSmart/CookSmart/CSScaleView.m +++ b/CookSmart/CookSmart/UI/Scales/ObjC/CSScaleView.m @@ -51,7 +51,6 @@ - (id)initWithFrame:(CGRect)frame - (void)initialize { -// self.backgroundColor = [UIColor systemRedColor]; self.bounces = NO; self.pagingEnabled = NO; self.alwaysBounceHorizontal = NO; @@ -72,7 +71,7 @@ - (void)layoutSubviews { [super layoutSubviews]; - if ([self.delegate respondsToSelector:@selector(isSnapping)] && [self.delegate performSelector:@ selector(isSnapping)]) + if ([self.delegate respondsToSelector:@selector(isSnapping)] && [self.delegate performSelector:@selector(isSnapping)]) { // We snap these views to values in an animated way. We don't want to be messing with the contentOffset // and tile moves inside an animation block. For that reason, we will forego this work during the snapping animation. diff --git a/CookSmart/CookSmart/Scale View/ScaleScrollView.swift b/CookSmart/CookSmart/UI/Scales/ScaleScrollView.swift similarity index 65% rename from CookSmart/CookSmart/Scale View/ScaleScrollView.swift rename to CookSmart/CookSmart/UI/Scales/ScaleScrollView.swift index 40aaf0b..0456a0b 100644 --- a/CookSmart/CookSmart/Scale View/ScaleScrollView.swift +++ b/CookSmart/CookSmart/UI/Scales/ScaleScrollView.swift @@ -14,15 +14,10 @@ import UIKit class ScaleScrollView: UIScrollView { static let TileHeight: CGFloat = 200 - @Published private(set) var unitValue: CGFloat = 1 + @Published private(set) var unitValue: CGFloat - var unitText: AnyPublisher { - publisher(for: \.contentOffset) - .map { _ in - Double(self.virtualContentYOffset * self.unitsPerPoint).vulgarFractionString - } - .eraseToAnyPublisher() - } + private let stableUnitValueSubject = PassthroughSubject() + let stableUnitValuePublisher: AnyPublisher init(unitsPerTile: Int = 1, mirror: Bool = false) { self.unitsPerTile = unitsPerTile @@ -31,6 +26,10 @@ class ScaleScrollView: UIScrollView { pointsPerUnit = ScaleScrollView.TileHeight / CGFloat(unitsPerTile) unitsPerPoint = 1 / pointsPerUnit + unitValue = 1 + + stableUnitValuePublisher = stableUnitValueSubject.eraseToAnyPublisher() + super.init(frame: .zero) setUpViews() @@ -45,7 +44,7 @@ class ScaleScrollView: UIScrollView { override var bounds: CGRect { didSet { if bounds.size != oldValue.size { - updateContentSize() + createTiles() } } } @@ -58,6 +57,12 @@ class ScaleScrollView: UIScrollView { } } + override var contentOffset: CGPoint { + didSet { + unitValue = virtualContentYOffset * unitsPerPoint + } + } + // MARK: Private private let mirror: Bool @@ -65,7 +70,11 @@ class ScaleScrollView: UIScrollView { private var unitsPerPoint: CGFloat private var pointsPerUnit: CGFloat - private var accumulatedOffset: CGFloat = 0 + private var accumulatedOffset: CGFloat = 0 { + didSet { + unitValue = virtualContentYOffset * unitsPerPoint + } + } private func setUpViews() { translatesAutoresizingMaskIntoConstraints = false @@ -73,11 +82,11 @@ class ScaleScrollView: UIScrollView { addSubview(tileContainer) - updateContentSize() + createTiles() } private func setUpScrollView() { - backgroundColor = Color.background + backgroundColor = CSColor.background.asUIColor() bounces = false isPagingEnabled = false alwaysBounceVertical = false @@ -89,7 +98,7 @@ class ScaleScrollView: UIScrollView { delegate = self } - private func updateContentSize() { + private func createTiles() { contentSize = CGSize(width: bounds.size.width, height: bounds.size.height * 10) tileContainer.subviews.forEach { $0.removeFromSuperview() } @@ -113,21 +122,17 @@ class ScaleScrollView: UIScrollView { } accumulatedOffset = actualCenter * pointsPerUnit + unitValue = virtualContentYOffset * unitsPerPoint setNeedsLayout() - layoutIfNeeded() - - syncToUnitValue(virtualContentYOffset * unitsPerPoint) } - func syncToUnitValue(_ unitValue: CGFloat) { - delegate = nil - setContentOffset(CGPoint(x: 0, y: pointsPerUnit * unitValue - accumulatedOffset), animated: false) - delegate = self + func syncTo(unitValue: CGFloat) { + contentOffset = CGPoint(x: 0, y: pointsPerUnit * unitValue - accumulatedOffset) + stableUnitValueSubject.send(Float(unitValue)) } - private func updateCenterValue(_ newCenterValue: CGFloat, notifyDelegate: Bool = false, animated: Bool = false) { - if !notifyDelegate { delegate = nil } + private func updateCenterValue(_ newCenterValue: CGFloat, animated: Bool = false) { let updateContentOffset = { self.contentOffset = CGPoint(x: 0, y: self.pointsPerUnit * newCenterValue - self.accumulatedOffset) } @@ -138,18 +143,15 @@ class ScaleScrollView: UIScrollView { } else { updateContentOffset() } - delegate = self } override func layoutSubviews() { super.layoutSubviews() let newYOffset = getNewYOffset() - if yOffset != newYOffset { - let yOffsetDelta = yOffset - newYOffset - delegate = nil + if contentOffset.y != newYOffset { + let yOffsetDelta = contentOffset.y - newYOffset contentOffset = CGPoint(x: 0, y: newYOffset) - delegate = self tileContainer.center = CGPoint(x: tileContainer.center.x, y: tileContainer.center.y - yOffsetDelta) accumulatedOffset += yOffsetDelta } @@ -184,9 +186,10 @@ class ScaleScrollView: UIScrollView { } } - private func snapToHumanReadableValue() { - let humanReadableValue = CGFloat(Double(virtualContentYOffset * unitsPerPoint).roundedValue) - updateCenterValue(humanReadableValue, notifyDelegate: true, animated: true) + private func handleScrollCompletion() { + let roundedValue = CGFloat(Double(virtualContentYOffset * unitsPerPoint).roundedValue) + updateCenterValue(roundedValue, animated: true) + stableUnitValueSubject.send(Float(roundedValue)) } } @@ -194,19 +197,15 @@ class ScaleScrollView: UIScrollView { extension ScaleScrollView: UIScrollViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - unitValue = virtualContentYOffset * unitsPerPoint - } - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { guard !decelerate else { return } - snapToHumanReadableValue() + handleScrollCompletion() } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - snapToHumanReadableValue() + handleScrollCompletion() } func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { @@ -225,59 +224,12 @@ private extension ScaleScrollView { contentOffset.y + accumulatedOffset } - private var yOffset: CGFloat { - contentOffset.y - } - private func getNewYOffset() -> CGFloat { let maxYOffset = targetContentYOffset + bounds.size.height let minYOffset = targetContentYOffset - bounds.size.height - if yOffset > maxYOffset || (yOffset < minYOffset && accumulatedOffset > 0) { - return min(targetContentYOffset, yOffset + accumulatedOffset) - } - return yOffset - } -} - -// MARK: - ScalePreview - -struct ScalePreview: PreviewProvider { - static var preview: some View { - HStack(spacing: 0) { - ScalePreviewContainer(unitsPerTile: 1, mirror: false) - .frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.height) - ScalePreviewContainer(unitsPerTile: 125, mirror: true) - .frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.height) - } - } - - static var previews: some View { - Group { - NavigationView { - preview - }.environment(\.colorScheme, .light) - NavigationView { - preview - }.environment(\.sizeCategory, .extraLarge) - NavigationView { - preview - }.environment(\.colorScheme, .dark) + if contentOffset.y > maxYOffset || (contentOffset.y < minYOffset && accumulatedOffset > 0) { + return min(targetContentYOffset, contentOffset.y + accumulatedOffset) } - } - - struct ScalePreviewContainer: UIViewRepresentable { - var unitsPerTile: Int - var mirror: Bool - - func makeUIView(context _: UIViewRepresentableContext) -> UIView { - ScaleScrollView( - unitsPerTile: unitsPerTile, - mirror: mirror - ) - } - - func updateUIView(_: UIViewType, context _: UIViewRepresentableContext) {} - - typealias UIViewType = UIView + return contentOffset.y } } diff --git a/CookSmart/CookSmart/Scale View/ScaleTile.swift b/CookSmart/CookSmart/UI/Scales/ScaleTile.swift similarity index 98% rename from CookSmart/CookSmart/Scale View/ScaleTile.swift rename to CookSmart/CookSmart/UI/Scales/ScaleTile.swift index e75136d..622e4e2 100644 --- a/CookSmart/CookSmart/Scale View/ScaleTile.swift +++ b/CookSmart/CookSmart/UI/Scales/ScaleTile.swift @@ -78,11 +78,11 @@ class ScaleTile: UIView { // MARK: Private - let valueLabel: Label = .init(style: .tiny) + let valueLabel = UILabel(style: .minorContent) let mirror: Bool private func setUpViews() { - backgroundColor = UIColor.clear + backgroundColor = CSColor.background.asUIColor() setUpLabel() } diff --git a/CookSmart/CookSmart/UI/Scales/ScaleViewController.swift b/CookSmart/CookSmart/UI/Scales/ScaleViewController.swift new file mode 100644 index 0000000..146609a --- /dev/null +++ b/CookSmart/CookSmart/UI/Scales/ScaleViewController.swift @@ -0,0 +1,202 @@ +// +// ScaleViewController.swift +// cake +// +// Created by Olga Galchenko on 4/11/20. +// Copyright © 2020 Olga Galchenko. All rights reserved. +// + +import Combine +import Foundation +import SwiftUI +import UIKit + +class ScaleViewController: UIViewController { + + static let glassHeight: CGFloat = 40 + + private enum DisplayMode { + case scales + case unitPicker + } + + init(density: Density, + volumeUnit: CSUnit = CSUnitCollection.volumeUnits()?.unit(at: 2) ?? CSUnit(), + weightUnit: CSUnit = CSUnitCollection.weightUnits()?.unit(at: 2) ?? CSUnit(), + shouldSyncScales: Bool = true) { + scalesView = ScalesView( + unitConversionFactor: CGFloat(density.in(weightUnit, per: volumeUnit)), + syncScales: shouldSyncScales + ) + unitPickerView = UnitPickerView(volumeUnit: volumeUnit, weightUnit: weightUnit) + self.volumeUnit = volumeUnit + self.weightUnit = weightUnit + + currentDensity = density + + super.init(nibName: nil, bundle: nil) + + volumeUnitButton.setTitle(volumeUnit.name, for: .normal) + weightUnitButton.setTitle(weightUnit.name, for: .normal) + + stableUnitConversionFactorRx = scalesView.stableUnitConversionFactorPublisher.sink { + self.currentDensity = Density($0, in: self.weightUnit, per: self.volumeUnit) + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setUpViews() + } + + // MARK: Public + + @Published public var currentDensity: Density + + // MARK: Private + + private var stableUnitConversionFactorRx: AnyCancellable? + + private var volumeUnit: CSUnit { + willSet { + if !newValue.isEqual(volumeUnit) { + scalesView.update( + conversionFactor: CGFloat(currentDensity.in(weightUnit, per: newValue)), + fixing: .Weight() + ) + volumeUnitButton.setTitle(newValue.name, for: .normal) + } + } + } + + private var weightUnit: CSUnit { + willSet { + if !newValue.isEqual(weightUnit) { + scalesView.update( + conversionFactor: CGFloat(currentDensity.in(newValue, per: volumeUnit)), + fixing: .Volume() + ) + weightUnitButton.setTitle(newValue.name, for: .normal) + } + } + } + + private var displayMode: DisplayMode = .scales + + private lazy var volumeUnitButton: UIButton = { + let button = UIButton(style: .plainButton) + button.setTitleColor(.label, for: .disabled) + button.setTitle("Volume", for: .disabled) + button.addTarget(self, action: #selector(toggleDisplayMode), for: .touchUpInside) + return button + }() + + private lazy var weightUnitButton: UIButton = { + let button = UIButton(style: .plainButton) + button.setTitleColor(.label, for: .disabled) + button.setTitle("Weight", for: .disabled) + button.addTarget(self, action: #selector(toggleDisplayMode), for: .touchUpInside) + return button + }() + + private let scalesView: ScalesView + private let unitPickerView: UnitPickerView + + private var scalesTopConstraint: NSLayoutConstraint? + private var unitPickerBottomConstraint: NSLayoutConstraint? + + private func setUpViews() { + view.clipsToBounds = true + unitPickerView.delegate = self + + let magnifiedContainer = UIView() + magnifiedContainer.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(magnifiedContainer) + magnifiedContainer.constrainToSuperview() + + setUpScaleViews(container: magnifiedContainer) + setUpUnitViews(container: magnifiedContainer) + setUpGlassView(viewToMagnify: magnifiedContainer) + } + + private func setUpGlassView(viewToMagnify: UIView) { + let glassView = CSGlassView(magnifiedView: viewToMagnify)! + view.addSubview(glassView) + glassView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + glassView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + glassView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + glassView.heightAnchor.constraint(equalToConstant: ScaleViewController.glassHeight).isActive = true + } + + private func setUpUnitViews(container: UIView) { + let gradientView = GradientView() + container.addSubview(gradientView) + gradientView.constrainToSuperview(anchors: [.leading, .top, .right]) + gradientView.heightAnchor.constraint(equalToConstant: 100).isActive = true + + gradientView.addSubview(volumeUnitButton) + volumeUnitButton.constrainToSuperview(anchors: [.leading, .top]) + + gradientView.addSubview(weightUnitButton) + weightUnitButton.constrainToSuperview(anchors: [.trailing, .top]) + + volumeUnitButton.trailingAnchor.constraint(equalTo: weightUnitButton.leadingAnchor).isActive = true + volumeUnitButton.widthAnchor.constraint(equalTo: weightUnitButton.widthAnchor).isActive = true + } + + private func setUpScaleViews(container: UIView) { + container.addSubview(scalesView) + scalesView.constrainToSuperview(anchors: [.leading, .trailing, .height]) + scalesTopConstraint = scalesView.topAnchor.constraint(equalTo: view.topAnchor) + scalesTopConstraint?.isActive = true + + container.addSubview(unitPickerView) + unitPickerView.constrainToSuperview(anchors: [.leading, .trailing, .height]) + unitPickerBottomConstraint = unitPickerView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + + unitPickerView.topAnchor.constraint(equalTo: scalesView.bottomAnchor).isActive = true + } + + @objc + private func toggleDisplayMode() { + guard let scalesTopConstraint = scalesTopConstraint, + let unitPickerBottomConstraint = unitPickerBottomConstraint + else { + return + } + displayMode = (displayMode == .scales) ? .unitPicker : .scales + UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { + scalesTopConstraint.isActive = self.displayMode == .scales + unitPickerBottomConstraint.isActive = self.displayMode == .unitPicker + self.view.layoutIfNeeded() + }.startAnimation() + + let textAnimations = UIViewPropertyAnimator(duration: 0.15, curve: .linear) { + self.weightUnitButton.alpha = 0 + self.volumeUnitButton.alpha = 0 + } + textAnimations.addCompletion { _ in + self.weightUnitButton.isEnabled = self.displayMode == .scales + self.volumeUnitButton.isEnabled = self.displayMode == .scales + UIViewPropertyAnimator(duration: 0.15, curve: .linear) { + self.weightUnitButton.alpha = 1 + self.volumeUnitButton.alpha = 1 + }.startAnimation() + } + textAnimations.startAnimation() + } +} + +extension ScaleViewController: UnitPickerDelegate { + func picked(volumeUnit: CSUnit, weightUnit: CSUnit) { + self.volumeUnit = volumeUnit + self.weightUnit = weightUnit + toggleDisplayMode() + } +} diff --git a/CookSmart/CookSmart/UI/Scales/ScalesView.swift b/CookSmart/CookSmart/UI/Scales/ScalesView.swift new file mode 100644 index 0000000..29a2d9f --- /dev/null +++ b/CookSmart/CookSmart/UI/Scales/ScalesView.swift @@ -0,0 +1,185 @@ +// +// ScalesView.swift +// cake +// +// Created by Alex King on 4/18/20. +// Copyright © 2020 Olga Galchenko. All rights reserved. +// + +import Combine + +class ScalesView: UIView { + + enum Mode { + case sync + case edit + } + + private var unitConversionFactor: CGFloat = 1 + + let stableUnitConversionFactorPublisher: AnyPublisher + + private let mode: Mode + + init(unitConversionFactor: CGFloat, + syncScales: Bool = true) { + mode = syncScales ? .sync : .edit + stableUnitConversionFactorPublisher = Publishers.CombineLatest( + volumeScrollView.stableUnitValuePublisher, + weightScrollView.stableUnitValuePublisher + ) + .map { volumeUnitValue, weightUnitValue in weightUnitValue / volumeUnitValue } + .eraseToAnyPublisher() + + super.init(frame: .zero) + + clearsContextBeforeDrawing = true + self.unitConversionFactor = unitConversionFactor + contentMode = .scaleToFill + autoresizesSubviews = true + translatesAutoresizingMaskIntoConstraints = false + insetsLayoutMarginsFromSafeArea = false + + setupViews() + syncScaleViews(fixedDimension: .Volume(1)) + setUpSubscribers() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + assertionFailure("init(coder:) has not been implemented") + return nil + } + + func update(conversionFactor: CGFloat, fixing dimension: ConstantDimension) { + unitConversionFactor = conversionFactor + syncScaleViews(fixedDimension: dimension) + } + + private let volumeScrollView = ScaleScrollView() + private let weightScrollView = ScaleScrollView(unitsPerTile: 100, mirror: true) + private let volumeLabel = UILabel(style: .coreContent) + private let weightLabel = UILabel(style: .coreContent) + private let volumeCenterLine = CenterLineView() + private let weightCenterLine = CenterLineView() + + private var subscriptions: [AnyCancellable] = [] + + private func setupViews() { + weightScrollView.translatesAutoresizingMaskIntoConstraints = false + + translatesAutoresizingMaskIntoConstraints = false + + addSubview(volumeScrollView) + volumeScrollView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + volumeScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true + volumeScrollView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + addSubview(weightScrollView) + weightScrollView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + weightScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true + weightScrollView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + volumeScrollView.trailingAnchor.constraint(equalTo: centerXAnchor).isActive = true + weightScrollView.leadingAnchor.constraint(equalTo: centerXAnchor).isActive = true + + addSubview(volumeLabel) + volumeLabel.constrain(to: volumeScrollView, anchors: [.centerX, .centerY]) + volumeLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + volumeLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + addSubview(weightLabel) + weightLabel.constrain(to: weightScrollView, anchors: [.centerX, .centerY]) + weightLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + weightLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + addSubview(volumeCenterLine) + volumeCenterLine.constrain(to: volumeScrollView, anchors: [.centerY, .leading, .trailing]) + addSubview(weightCenterLine) + weightCenterLine.constrain(to: weightScrollView, anchors: [.centerY, .leading, .trailing]) + } + + private func setUpSubscribers() { + switch mode { + case .edit: + subscriptions.append( + Publishers.CombineLatest(volumeScrollView.$unitValue, weightScrollView.$unitValue) + .sink { + self.unitConversionFactor = $0.1 / $0.0 + } + ) + case .sync: + subscriptions.append( + volumeScrollView.$unitValue + .filter { _ in self.mode == .sync } + .sink { volumeValue in + self.weightScrollView.syncTo(unitValue: volumeValue * self.unitConversionFactor) + } + ) + + subscriptions.append( + weightScrollView.$unitValue + .filter { _ in self.mode == .sync } + .sink { weightValue in + self.volumeScrollView.syncTo(unitValue: weightValue / self.unitConversionFactor) + } + ) + } + + subscriptions.append(contentsOf: [ + volumeScrollView.$unitValue + .sink { self.volumeLabel.text = Double($0).humanReabableString }, + weightScrollView.$unitValue + .sink { self.weightLabel.text = Double($0).humanReabableString }, + ]) + } + + private func syncScaleViews(fixedDimension: ConstantDimension) { + var volumeScale: CGFloat = 1 + + let idealWeightScale = unitConversionFactor + var weightScale: CGFloat = 1 + if idealWeightScale >= 10 { + let orderOfMagnitude = floor(log10(idealWeightScale)) + weightScale = idealWeightScale - idealWeightScale.truncatingRemainder(dividingBy: pow(10, orderOfMagnitude)) + } else { + let idealVolumeScale = 1 / unitConversionFactor + if idealVolumeScale >= 10 { + let orderOfMagnitude = floor(log10(idealVolumeScale)) + volumeScale = idealVolumeScale - idealVolumeScale.truncatingRemainder(dividingBy: pow(10, orderOfMagnitude)) + } + } + + let scaleValueSpecs: [(ScaleScrollView, CGFloat)] + switch fixedDimension { + case let .Weight(constantUnits: constantWeight): + scaleValueSpecs = [ + (weightScrollView, constantWeight.map { CGFloat($0) } ?? weightScrollView.unitValue), + (volumeScrollView, ( + constantWeight.map { CGFloat($0) } ?? weightScrollView.unitValue + ) / unitConversionFactor), + ] + case let .Volume(constantUnits: constantVolume): + scaleValueSpecs = [ + (volumeScrollView, constantVolume.map { CGFloat($0) } ?? volumeScrollView.unitValue), + (weightScrollView, ( + constantVolume.map { CGFloat($0) } ?? volumeScrollView.unitValue + ) * unitConversionFactor), + ] + } + + volumeScrollView.unitsPerTile = Int(volumeScale) + weightScrollView.unitsPerTile = Int(weightScale) + + DispatchQueue.main.async { + scaleValueSpecs.forEach { scaleScrollView, unitValue in + scaleScrollView.syncTo(unitValue: unitValue) + } + } + } +} + +enum ConstantDimension { + case Weight(_ constantUnits: Float? = nil) + case Volume(_ constantUnits: Float? = nil) +} diff --git a/CookSmart/CookSmart/UnitPickerView.swift b/CookSmart/CookSmart/UI/Scales/UnitPickerView.swift similarity index 93% rename from CookSmart/CookSmart/UnitPickerView.swift rename to CookSmart/CookSmart/UI/Scales/UnitPickerView.swift index 7eb31cb..d49cc30 100644 --- a/CookSmart/CookSmart/UnitPickerView.swift +++ b/CookSmart/CookSmart/UI/Scales/UnitPickerView.swift @@ -37,7 +37,7 @@ class UnitPickerView: UIView { } private lazy var doneButton: UIButton = { - let button = Button() + let button = UIButton(style: .plainButton) button.setTitle("Done", for: .normal) button.addTarget(self, action: #selector(doneButtonPressed), for: .touchUpInside) return button @@ -144,7 +144,7 @@ private class UnitPickerScrollView: UIView { scrollView.constrainToSuperview() scrollView.addSubview(unitStackView) - unitStackView.constrainToSuperview(anchors: [.leading, .trailing, .width]) + unitStackView.constrainToSuperview(anchors: [.leading, .width]) unitStackViewTopConstraint = unitStackView.topAnchor.constraint(equalTo: scrollView.topAnchor) unitStackViewTopConstraint?.isActive = true unitStackViewBottomConstraint = unitStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) @@ -160,7 +160,7 @@ private class UnitPickerScrollView: UIView { unitCollection.units .compactMap { $0 as? CSUnit } .forEach { unit in - let unitLabel = Label(style: .medium) + let unitLabel = UILabel(style: .supportingContent) unitLabel.heightAnchor.constraint(equalToConstant: Constants.unitLabelHeight).isActive = true unitLabel.text = unit.name unitStackView.addArrangedSubview(unitLabel) @@ -171,7 +171,7 @@ private class UnitPickerScrollView: UIView { selectedIndex = UInt(round(scrollView.contentOffset.y / Constants.unitLabelHeight)) } - private func scrollToNearestLabel() { + private func handleScrollCompletion() { updateSelectedIndex() let yOffset = CGFloat(selectedIndex) * Constants.unitLabelHeight scrollView.setContentOffset(CGPoint(x: 0, y: yOffset), animated: true) @@ -182,15 +182,16 @@ private class UnitPickerScrollView: UIView { extension UnitPickerScrollView: UIScrollViewDelegate { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - scrollToNearestLabel() + handleScrollCompletion() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { guard !decelerate else { return } - scrollToNearestLabel() + handleScrollCompletion() } +} - func scrollViewDidScroll(_ scrollView: UIScrollView) { - scrollView.contentOffset.x = 0 - } +@objc +protocol UnitPickerDelegate: AnyObject { + @objc(pickedVolumeUnit:weightUnit:) func picked(volumeUnit: CSUnit, weightUnit: CSUnit) } diff --git a/CookSmart/CookSmart/Core/UIView+Constraints.swift b/CookSmart/CookSmart/UI/UIView+Constraints.swift similarity index 100% rename from CookSmart/CookSmart/Core/UIView+Constraints.swift rename to CookSmart/CookSmart/UI/UIView+Constraints.swift diff --git a/CookSmart/CookSmart/UnitPickerDelegate.swift b/CookSmart/CookSmart/UnitPickerDelegate.swift deleted file mode 100644 index 0ab3c70..0000000 --- a/CookSmart/CookSmart/UnitPickerDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UnitPickerDelegate.swift -// cake -// -// Created by Alex King on 4/5/20. -// Copyright © 2020 Olga Galchenko. All rights reserved. -// - -import Foundation - -@objc -protocol UnitPickerDelegate: AnyObject { - @objc(pickedVolumeUnit:weightUnit:) func picked(volumeUnit: CSUnit, weightUnit: CSUnit) -} diff --git a/CookSmart/CookSmart/cake-Bridging-Header.h b/CookSmart/CookSmart/cake-Bridging-Header.h index 042f261..439de68 100644 --- a/CookSmart/CookSmart/cake-Bridging-Header.h +++ b/CookSmart/CookSmart/cake-Bridging-Header.h @@ -9,5 +9,6 @@ #import "CSConversionVC.h" #import "CSScaleVC.h" #import "CSIngredient.h" -#import "CSIngredients.h" -#import "CSIngredientListVC.h" +#import "CSSharedConstants.h" +#import "CookSmart-Prefix.pch" +#import "CSGlassView.h" From ed3d9cf271491bbabbfcbf062cb7b91ea0615843 Mon Sep 17 00:00:00 2001 From: Vova Galchenko Date: Wed, 27 Dec 2023 17:58:19 -0800 Subject: [PATCH 2/4] Changed if expressions to ternary in some cases --- CookSmart/CookSmart/Design Language/CSColor.swift | 6 +----- CookSmart/CookSmart/Model/Ingredient.swift | 6 +----- CookSmart/CookSmart/UI/IngredientListView.swift | 6 ++---- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/CookSmart/CookSmart/Design Language/CSColor.swift b/CookSmart/CookSmart/Design Language/CSColor.swift index d0f840d..28ead50 100644 --- a/CookSmart/CookSmart/Design Language/CSColor.swift +++ b/CookSmart/CookSmart/Design Language/CSColor.swift @@ -17,11 +17,7 @@ enum CSColor: String { case clear = "Clear" func asUIColor() -> UIColor { - if rawValue == "Clear" { - UIColor.clear - } else { - UIColor(named: rawValue)! - } + rawValue == "Clear" ? UIColor.clear : UIColor(named: rawValue)! } func asSwiftUIColor() -> Color { diff --git a/CookSmart/CookSmart/Model/Ingredient.swift b/CookSmart/CookSmart/Model/Ingredient.swift index 4e75638..24ac49e 100644 --- a/CookSmart/CookSmart/Model/Ingredient.swift +++ b/CookSmart/CookSmart/Model/Ingredient.swift @@ -72,11 +72,7 @@ struct Ingredient: Codable, Identifiable, Equatable { CodingKeys.name.stringValue: name, CodingKeys.density.stringValue: density.canonical, ] as [String: Any] - let date: [String: Any] = if lastAccessDate == nil { - [:] - } else { - [CodingKeys.lastAccessDate.stringValue: lastAccessDate!] - } + let date: [String: Any] = lastAccessDate == nil ? [:] : [CodingKeys.lastAccessDate.stringValue: lastAccessDate!] return baseDict.merging(date, uniquingKeysWith: { _, x in x }) } } diff --git a/CookSmart/CookSmart/UI/IngredientListView.swift b/CookSmart/CookSmart/UI/IngredientListView.swift index 01807b1..9217236 100644 --- a/CookSmart/CookSmart/UI/IngredientListView.swift +++ b/CookSmart/CookSmart/UI/IngredientListView.swift @@ -13,13 +13,11 @@ struct IngredientListView: View { @State private var searchText: String = "" @State private var isShowingResetAlert = false private var ingredientGroupsToPresent: [any IngredientGroup] { - let relevantGroups = if searchText.isEmpty { - ingredientsStore.ingredientGroups - } else { + let relevantGroups = searchText.isEmpty ? + ingredientsStore.ingredientGroups : ingredientsStore.ingredientGroups.compactMap { group in group.filter(searchString: searchText) } - } return relevantGroups.filter { !$0.ingredients.isEmpty } } From 2d0b57926bec68eb00d78d835757215d2c889a7a Mon Sep 17 00:00:00 2001 From: Vova Galchenko Date: Wed, 27 Dec 2023 22:08:04 -0800 Subject: [PATCH 3/4] Use newer XCode in the pipeline --- .github/workflows/Tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 3804554..2233318 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -43,6 +43,8 @@ jobs: - name: Run Tests run: | + sudo xcode-select -s /Applications/Xcode_15.1.app + xcrun swift --version cd $GITHUB_WORKSPACE/CookSmart fastlane testlane From 4bc588677514560a31507df97eaea418ae975674 Mon Sep 17 00:00:00 2001 From: Vova Galchenko Date: Wed, 27 Dec 2023 23:43:12 -0800 Subject: [PATCH 4/4] Use macos-13 image for github actions --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 2233318..069c136 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -2,7 +2,7 @@ name: Cake Tests on: pull_request jobs: test: - runs-on: [macos-latest] + runs-on: [macos-13] steps: - uses: actions/checkout@v2