diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..47ef03fca --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ \ No newline at end of file diff --git a/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.pbxproj b/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.pbxproj new file mode 100644 index 000000000..425e9bb7b --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.pbxproj @@ -0,0 +1,971 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 15A3905F5B905C60D99161C6 /* Pods_Drink_O_Matic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 722FDDB2D778BBFDFD776D0E /* Pods_Drink_O_Matic.framework */; }; + 28AC160222F352E300534A96 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC160122F352E300534A96 /* AppDelegate.swift */; }; + 28AC160422F352E300534A96 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC160322F352E300534A96 /* HomeViewController.swift */; }; + 28AC160722F352E300534A96 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 28AC160522F352E300534A96 /* Main.storyboard */; }; + 28AC160922F352E500534A96 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC160822F352E500534A96 /* Assets.xcassets */; }; + 28AC160C22F352E500534A96 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 28AC160A22F352E500534A96 /* LaunchScreen.storyboard */; }; + 28AC161722F352E500534A96 /* Drink_O_MaticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC161622F352E500534A96 /* Drink_O_MaticTests.swift */; }; + 28AC162222F352E500534A96 /* Drink_O_MaticUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC162122F352E500534A96 /* Drink_O_MaticUITests.swift */; }; + 28EFF4B922F3958400608AD3 /* APIRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4B822F3958400608AD3 /* APIRouter.swift */; }; + 28EFF4BA22F3958400608AD3 /* APIRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4B822F3958400608AD3 /* APIRouter.swift */; }; + 28EFF4BB22F3958400608AD3 /* APIRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4B822F3958400608AD3 /* APIRouter.swift */; }; + 28EFF4BD22F3959100608AD3 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4BC22F3959100608AD3 /* APIClient.swift */; }; + 28EFF4BE22F3959100608AD3 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4BC22F3959100608AD3 /* APIClient.swift */; }; + 28EFF4BF22F3959100608AD3 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4BC22F3959100608AD3 /* APIClient.swift */; }; + 28EFF4C122F3972900608AD3 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C022F3972900608AD3 /* Constants.swift */; }; + 28EFF4C222F3972900608AD3 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C022F3972900608AD3 /* Constants.swift */; }; + 28EFF4C322F3972900608AD3 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C022F3972900608AD3 /* Constants.swift */; }; + 28EFF4C522F3994B00608AD3 /* Drink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C422F3994B00608AD3 /* Drink.swift */; }; + 28EFF4C622F3994B00608AD3 /* Drink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C422F3994B00608AD3 /* Drink.swift */; }; + 28EFF4C722F3994B00608AD3 /* Drink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C422F3994B00608AD3 /* Drink.swift */; }; + 28EFF4C922F39FD900608AD3 /* DrinkDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C822F39FD900608AD3 /* DrinkDetails.swift */; }; + 28EFF4CA22F39FD900608AD3 /* DrinkDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C822F39FD900608AD3 /* DrinkDetails.swift */; }; + 28EFF4CB22F39FD900608AD3 /* DrinkDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4C822F39FD900608AD3 /* DrinkDetails.swift */; }; + 28EFF4D222F49E4C00608AD3 /* CardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4CD22F49E4C00608AD3 /* CardContentView.swift */; }; + 28EFF4D322F49E4C00608AD3 /* CardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4CD22F49E4C00608AD3 /* CardContentView.swift */; }; + 28EFF4D422F49E4C00608AD3 /* CardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4CD22F49E4C00608AD3 /* CardContentView.swift */; }; + 28EFF4D522F49E4C00608AD3 /* CardContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28EFF4CE22F49E4C00608AD3 /* CardContentView.xib */; }; + 28EFF4D622F49E4C00608AD3 /* CardContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28EFF4CE22F49E4C00608AD3 /* CardContentView.xib */; }; + 28EFF4D722F49E4C00608AD3 /* CardContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28EFF4CE22F49E4C00608AD3 /* CardContentView.xib */; }; + 28EFF4DB22F49E4C00608AD3 /* CardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4D022F49E4C00608AD3 /* CardCollectionViewCell.swift */; }; + 28EFF4DC22F49E4C00608AD3 /* CardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4D022F49E4C00608AD3 /* CardCollectionViewCell.swift */; }; + 28EFF4DD22F49E4C00608AD3 /* CardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4D022F49E4C00608AD3 /* CardCollectionViewCell.swift */; }; + 28EFF4DE22F49E4C00608AD3 /* CardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28EFF4D122F49E4C00608AD3 /* CardCollectionViewCell.xib */; }; + 28EFF4DF22F49E4C00608AD3 /* CardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28EFF4D122F49E4C00608AD3 /* CardCollectionViewCell.xib */; }; + 28EFF4E022F49E4C00608AD3 /* CardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28EFF4D122F49E4C00608AD3 /* CardCollectionViewCell.xib */; }; + 28EFF4E922F49E8900608AD3 /* GlobalConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E222F49E8900608AD3 /* GlobalConstants.swift */; }; + 28EFF4EA22F49E8900608AD3 /* GlobalConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E222F49E8900608AD3 /* GlobalConstants.swift */; }; + 28EFF4EB22F49E8900608AD3 /* GlobalConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E222F49E8900608AD3 /* GlobalConstants.swift */; }; + 28EFF4EC22F49E8900608AD3 /* StatusBarAnimatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E322F49E8900608AD3 /* StatusBarAnimatable.swift */; }; + 28EFF4ED22F49E8900608AD3 /* StatusBarAnimatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E322F49E8900608AD3 /* StatusBarAnimatable.swift */; }; + 28EFF4EE22F49E8900608AD3 /* StatusBarAnimatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E322F49E8900608AD3 /* StatusBarAnimatable.swift */; }; + 28EFF4EF22F49E8900608AD3 /* UIView+AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E422F49E8900608AD3 /* UIView+AutoLayout.swift */; }; + 28EFF4F022F49E8900608AD3 /* UIView+AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E422F49E8900608AD3 /* UIView+AutoLayout.swift */; }; + 28EFF4F122F49E8900608AD3 /* UIView+AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E422F49E8900608AD3 /* UIView+AutoLayout.swift */; }; + 28EFF4F222F49E8900608AD3 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E522F49E8900608AD3 /* NibLoadable.swift */; }; + 28EFF4F322F49E8900608AD3 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E522F49E8900608AD3 /* NibLoadable.swift */; }; + 28EFF4F422F49E8900608AD3 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E522F49E8900608AD3 /* NibLoadable.swift */; }; + 28EFF4FB22F49E8900608AD3 /* StatusBarAnimatableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E822F49E8900608AD3 /* StatusBarAnimatableViewController.swift */; }; + 28EFF4FC22F49E8900608AD3 /* StatusBarAnimatableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E822F49E8900608AD3 /* StatusBarAnimatableViewController.swift */; }; + 28EFF4FD22F49E8900608AD3 /* StatusBarAnimatableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF4E822F49E8900608AD3 /* StatusBarAnimatableViewController.swift */; }; + 28EFF50B22F49EA400608AD3 /* CardDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF50A22F49EA400608AD3 /* CardDetailViewController.swift */; }; + 28EFF50C22F49EA400608AD3 /* CardDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF50A22F49EA400608AD3 /* CardDetailViewController.swift */; }; + 28EFF50D22F49EA400608AD3 /* CardDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF50A22F49EA400608AD3 /* CardDetailViewController.swift */; }; + 28EFF51322F49ECA00608AD3 /* DismissCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF50F22F49ECA00608AD3 /* DismissCardAnimator.swift */; }; + 28EFF51422F49ECA00608AD3 /* DismissCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF50F22F49ECA00608AD3 /* DismissCardAnimator.swift */; }; + 28EFF51522F49ECA00608AD3 /* DismissCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF50F22F49ECA00608AD3 /* DismissCardAnimator.swift */; }; + 28EFF51622F49ECA00608AD3 /* CardTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51022F49ECA00608AD3 /* CardTransition.swift */; }; + 28EFF51722F49ECA00608AD3 /* CardTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51022F49ECA00608AD3 /* CardTransition.swift */; }; + 28EFF51822F49ECA00608AD3 /* CardTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51022F49ECA00608AD3 /* CardTransition.swift */; }; + 28EFF51922F49ECA00608AD3 /* CardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51122F49ECA00608AD3 /* CardPresentationController.swift */; }; + 28EFF51A22F49ECA00608AD3 /* CardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51122F49ECA00608AD3 /* CardPresentationController.swift */; }; + 28EFF51B22F49ECA00608AD3 /* CardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51122F49ECA00608AD3 /* CardPresentationController.swift */; }; + 28EFF51C22F49ECA00608AD3 /* PresentCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51222F49ECA00608AD3 /* PresentCardAnimator.swift */; }; + 28EFF51D22F49ECA00608AD3 /* PresentCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51222F49ECA00608AD3 /* PresentCardAnimator.swift */; }; + 28EFF51E22F49ECA00608AD3 /* PresentCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51222F49ECA00608AD3 /* PresentCardAnimator.swift */; }; + 28EFF52022F76F5400608AD3 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51F22F76F5400608AD3 /* GradientView.swift */; }; + 28EFF52122F76F5400608AD3 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51F22F76F5400608AD3 /* GradientView.swift */; }; + 28EFF52222F76F5400608AD3 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EFF51F22F76F5400608AD3 /* GradientView.swift */; }; + B4F4B24AE5A1254861A61C70 /* Pods_Drink_O_MaticUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C51183A47160CDDC05CA9F1 /* Pods_Drink_O_MaticUITests.framework */; }; + E1F05E0F067030F4E49E19F0 /* Pods_Drink_O_MaticTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AF8414AB55563959CDBEA16 /* Pods_Drink_O_MaticTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 28AC161322F352E500534A96 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28AC15F622F352E300534A96 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 28AC15FD22F352E300534A96; + remoteInfo = "Drink-O-Matic"; + }; + 28AC161E22F352E500534A96 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28AC15F622F352E300534A96 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 28AC15FD22F352E300534A96; + remoteInfo = "Drink-O-Matic"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 28AC15FE22F352E300534A96 /* Drink-O-Matic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Drink-O-Matic.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 28AC160122F352E300534A96 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 28AC160322F352E300534A96 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + 28AC160622F352E300534A96 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 28AC160822F352E500534A96 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 28AC160B22F352E500534A96 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 28AC160D22F352E500534A96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 28AC161222F352E500534A96 /* Drink-O-MaticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Drink-O-MaticTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 28AC161622F352E500534A96 /* Drink_O_MaticTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drink_O_MaticTests.swift; sourceTree = ""; }; + 28AC161822F352E500534A96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 28AC161D22F352E500534A96 /* Drink-O-MaticUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Drink-O-MaticUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 28AC162122F352E500534A96 /* Drink_O_MaticUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drink_O_MaticUITests.swift; sourceTree = ""; }; + 28AC162322F352E500534A96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 28EFF4B822F3958400608AD3 /* APIRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRouter.swift; sourceTree = ""; }; + 28EFF4BC22F3959100608AD3 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; + 28EFF4C022F3972900608AD3 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 28EFF4C422F3994B00608AD3 /* Drink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drink.swift; sourceTree = ""; }; + 28EFF4C822F39FD900608AD3 /* DrinkDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrinkDetails.swift; sourceTree = ""; }; + 28EFF4CD22F49E4C00608AD3 /* CardContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardContentView.swift; sourceTree = ""; }; + 28EFF4CE22F49E4C00608AD3 /* CardContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CardContentView.xib; sourceTree = ""; }; + 28EFF4D022F49E4C00608AD3 /* CardCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardCollectionViewCell.swift; sourceTree = ""; }; + 28EFF4D122F49E4C00608AD3 /* CardCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CardCollectionViewCell.xib; sourceTree = ""; }; + 28EFF4E222F49E8900608AD3 /* GlobalConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalConstants.swift; sourceTree = ""; }; + 28EFF4E322F49E8900608AD3 /* StatusBarAnimatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarAnimatable.swift; sourceTree = ""; }; + 28EFF4E422F49E8900608AD3 /* UIView+AutoLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+AutoLayout.swift"; sourceTree = ""; }; + 28EFF4E522F49E8900608AD3 /* NibLoadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; + 28EFF4E822F49E8900608AD3 /* StatusBarAnimatableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarAnimatableViewController.swift; sourceTree = ""; }; + 28EFF50A22F49EA400608AD3 /* CardDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardDetailViewController.swift; sourceTree = ""; }; + 28EFF50F22F49ECA00608AD3 /* DismissCardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissCardAnimator.swift; sourceTree = ""; }; + 28EFF51022F49ECA00608AD3 /* CardTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardTransition.swift; sourceTree = ""; }; + 28EFF51122F49ECA00608AD3 /* CardPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentationController.swift; sourceTree = ""; }; + 28EFF51222F49ECA00608AD3 /* PresentCardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentCardAnimator.swift; sourceTree = ""; }; + 28EFF51F22F76F5400608AD3 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; + 3C51183A47160CDDC05CA9F1 /* Pods_Drink_O_MaticUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Drink_O_MaticUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3FB58FBF10A2F913A03F21D9 /* Pods-Drink-O-Matic.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Drink-O-Matic.debug.xcconfig"; path = "Target Support Files/Pods-Drink-O-Matic/Pods-Drink-O-Matic.debug.xcconfig"; sourceTree = ""; }; + 4E14FE35F6EF15E3794D1139 /* Pods-Drink-O-Matic.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Drink-O-Matic.release.xcconfig"; path = "Target Support Files/Pods-Drink-O-Matic/Pods-Drink-O-Matic.release.xcconfig"; sourceTree = ""; }; + 722FDDB2D778BBFDFD776D0E /* Pods_Drink_O_Matic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Drink_O_Matic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 85C720A72337B4F217A1742F /* Pods-Drink-O-MaticTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Drink-O-MaticTests.release.xcconfig"; path = "Target Support Files/Pods-Drink-O-MaticTests/Pods-Drink-O-MaticTests.release.xcconfig"; sourceTree = ""; }; + 8AF8414AB55563959CDBEA16 /* Pods_Drink_O_MaticTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Drink_O_MaticTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B07EFC6918A357A1E45FD335 /* Pods-Drink-O-MaticTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Drink-O-MaticTests.debug.xcconfig"; path = "Target Support Files/Pods-Drink-O-MaticTests/Pods-Drink-O-MaticTests.debug.xcconfig"; sourceTree = ""; }; + C691AAB6153BB3F6B3F4CE6B /* Pods-Drink-O-MaticUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Drink-O-MaticUITests.debug.xcconfig"; path = "Target Support Files/Pods-Drink-O-MaticUITests/Pods-Drink-O-MaticUITests.debug.xcconfig"; sourceTree = ""; }; + D316EDD25E531DA09364BD3A /* Pods-Drink-O-MaticUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Drink-O-MaticUITests.release.xcconfig"; path = "Target Support Files/Pods-Drink-O-MaticUITests/Pods-Drink-O-MaticUITests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 28AC15FB22F352E300534A96 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 15A3905F5B905C60D99161C6 /* Pods_Drink_O_Matic.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28AC160F22F352E500534A96 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E1F05E0F067030F4E49E19F0 /* Pods_Drink_O_MaticTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28AC161A22F352E500534A96 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B4F4B24AE5A1254861A61C70 /* Pods_Drink_O_MaticUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 28AC15F522F352E300534A96 = { + isa = PBXGroup; + children = ( + 28AC160022F352E300534A96 /* Drink-O-Matic */, + 28AC161522F352E500534A96 /* Drink-O-MaticTests */, + 28AC162022F352E500534A96 /* Drink-O-MaticUITests */, + 28AC15FF22F352E300534A96 /* Products */, + E6D245425B6C1A5A69858807 /* Pods */, + F4F8D8FBF3D1A81A6FF00C5A /* Frameworks */, + ); + sourceTree = ""; + }; + 28AC15FF22F352E300534A96 /* Products */ = { + isa = PBXGroup; + children = ( + 28AC15FE22F352E300534A96 /* Drink-O-Matic.app */, + 28AC161222F352E500534A96 /* Drink-O-MaticTests.xctest */, + 28AC161D22F352E500534A96 /* Drink-O-MaticUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 28AC160022F352E300534A96 /* Drink-O-Matic */ = { + isa = PBXGroup; + children = ( + 28EFF50E22F49EC000608AD3 /* Transition */, + 28EFF4E122F49E7C00608AD3 /* Utils */, + 28EFF4CC22F49E3B00608AD3 /* Views */, + 28EFF4B222F394FB00608AD3 /* APIClient */, + 28EFF4B122F394EE00608AD3 /* Models */, + 28EFF4B322F3950F00608AD3 /* Application */, + 28EFF4B422F3951700608AD3 /* ViewControllers */, + 28EFF4B622F3953C00608AD3 /* Resources */, + 28EFF4B722F3954700608AD3 /* Storyboards */, + 28EFF4B522F3952500608AD3 /* Supporting Files */, + ); + path = "Drink-O-Matic"; + sourceTree = ""; + }; + 28AC161522F352E500534A96 /* Drink-O-MaticTests */ = { + isa = PBXGroup; + children = ( + 28AC161622F352E500534A96 /* Drink_O_MaticTests.swift */, + 28AC161822F352E500534A96 /* Info.plist */, + ); + path = "Drink-O-MaticTests"; + sourceTree = ""; + }; + 28AC162022F352E500534A96 /* Drink-O-MaticUITests */ = { + isa = PBXGroup; + children = ( + 28AC162122F352E500534A96 /* Drink_O_MaticUITests.swift */, + 28AC162322F352E500534A96 /* Info.plist */, + ); + path = "Drink-O-MaticUITests"; + sourceTree = ""; + }; + 28EFF4B122F394EE00608AD3 /* Models */ = { + isa = PBXGroup; + children = ( + 28EFF4C422F3994B00608AD3 /* Drink.swift */, + 28EFF4C822F39FD900608AD3 /* DrinkDetails.swift */, + ); + path = Models; + sourceTree = ""; + }; + 28EFF4B222F394FB00608AD3 /* APIClient */ = { + isa = PBXGroup; + children = ( + 28EFF4B822F3958400608AD3 /* APIRouter.swift */, + 28EFF4BC22F3959100608AD3 /* APIClient.swift */, + ); + path = APIClient; + sourceTree = ""; + }; + 28EFF4B322F3950F00608AD3 /* Application */ = { + isa = PBXGroup; + children = ( + 28AC160122F352E300534A96 /* AppDelegate.swift */, + ); + path = Application; + sourceTree = ""; + }; + 28EFF4B422F3951700608AD3 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + 28EFF50A22F49EA400608AD3 /* CardDetailViewController.swift */, + 28AC160322F352E300534A96 /* HomeViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; + 28EFF4B522F3952500608AD3 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 28AC160D22F352E500534A96 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 28EFF4B622F3953C00608AD3 /* Resources */ = { + isa = PBXGroup; + children = ( + 28AC160822F352E500534A96 /* Assets.xcassets */, + 28EFF4C022F3972900608AD3 /* Constants.swift */, + ); + path = Resources; + sourceTree = ""; + }; + 28EFF4B722F3954700608AD3 /* Storyboards */ = { + isa = PBXGroup; + children = ( + 28AC160522F352E300534A96 /* Main.storyboard */, + 28AC160A22F352E500534A96 /* LaunchScreen.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; + 28EFF4CC22F49E3B00608AD3 /* Views */ = { + isa = PBXGroup; + children = ( + 28EFF4CE22F49E4C00608AD3 /* CardContentView.xib */, + 28EFF4CD22F49E4C00608AD3 /* CardContentView.swift */, + 28EFF4D122F49E4C00608AD3 /* CardCollectionViewCell.xib */, + 28EFF4D022F49E4C00608AD3 /* CardCollectionViewCell.swift */, + ); + path = Views; + sourceTree = ""; + }; + 28EFF4E122F49E7C00608AD3 /* Utils */ = { + isa = PBXGroup; + children = ( + 28EFF4E522F49E8900608AD3 /* NibLoadable.swift */, + 28EFF4E422F49E8900608AD3 /* UIView+AutoLayout.swift */, + 28EFF4E222F49E8900608AD3 /* GlobalConstants.swift */, + 28EFF4E322F49E8900608AD3 /* StatusBarAnimatable.swift */, + 28EFF4E822F49E8900608AD3 /* StatusBarAnimatableViewController.swift */, + 28EFF51F22F76F5400608AD3 /* GradientView.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 28EFF50E22F49EC000608AD3 /* Transition */ = { + isa = PBXGroup; + children = ( + 28EFF51122F49ECA00608AD3 /* CardPresentationController.swift */, + 28EFF51022F49ECA00608AD3 /* CardTransition.swift */, + 28EFF50F22F49ECA00608AD3 /* DismissCardAnimator.swift */, + 28EFF51222F49ECA00608AD3 /* PresentCardAnimator.swift */, + ); + path = Transition; + sourceTree = ""; + }; + E6D245425B6C1A5A69858807 /* Pods */ = { + isa = PBXGroup; + children = ( + 3FB58FBF10A2F913A03F21D9 /* Pods-Drink-O-Matic.debug.xcconfig */, + 4E14FE35F6EF15E3794D1139 /* Pods-Drink-O-Matic.release.xcconfig */, + B07EFC6918A357A1E45FD335 /* Pods-Drink-O-MaticTests.debug.xcconfig */, + 85C720A72337B4F217A1742F /* Pods-Drink-O-MaticTests.release.xcconfig */, + C691AAB6153BB3F6B3F4CE6B /* Pods-Drink-O-MaticUITests.debug.xcconfig */, + D316EDD25E531DA09364BD3A /* Pods-Drink-O-MaticUITests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + F4F8D8FBF3D1A81A6FF00C5A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 722FDDB2D778BBFDFD776D0E /* Pods_Drink_O_Matic.framework */, + 8AF8414AB55563959CDBEA16 /* Pods_Drink_O_MaticTests.framework */, + 3C51183A47160CDDC05CA9F1 /* Pods_Drink_O_MaticUITests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 28AC15FD22F352E300534A96 /* Drink-O-Matic */ = { + isa = PBXNativeTarget; + buildConfigurationList = 28AC162622F352E500534A96 /* Build configuration list for PBXNativeTarget "Drink-O-Matic" */; + buildPhases = ( + 73BDAA272CB8048DC90C66D3 /* [CP] Check Pods Manifest.lock */, + 28AC15FA22F352E300534A96 /* Sources */, + 28AC15FB22F352E300534A96 /* Frameworks */, + 28AC15FC22F352E300534A96 /* Resources */, + 72CC0B3C96BA913907982921 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Drink-O-Matic"; + productName = "Drink-O-Matic"; + productReference = 28AC15FE22F352E300534A96 /* Drink-O-Matic.app */; + productType = "com.apple.product-type.application"; + }; + 28AC161122F352E500534A96 /* Drink-O-MaticTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 28AC162922F352E500534A96 /* Build configuration list for PBXNativeTarget "Drink-O-MaticTests" */; + buildPhases = ( + 6E4B5B7111BF089F9F0AE92E /* [CP] Check Pods Manifest.lock */, + 28AC160E22F352E500534A96 /* Sources */, + 28AC160F22F352E500534A96 /* Frameworks */, + 28AC161022F352E500534A96 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 28AC161422F352E500534A96 /* PBXTargetDependency */, + ); + name = "Drink-O-MaticTests"; + productName = "Drink-O-MaticTests"; + productReference = 28AC161222F352E500534A96 /* Drink-O-MaticTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 28AC161C22F352E500534A96 /* Drink-O-MaticUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 28AC162C22F352E500534A96 /* Build configuration list for PBXNativeTarget "Drink-O-MaticUITests" */; + buildPhases = ( + 78C9AE30C4FB79752315BF9E /* [CP] Check Pods Manifest.lock */, + 28AC161922F352E500534A96 /* Sources */, + 28AC161A22F352E500534A96 /* Frameworks */, + 28AC161B22F352E500534A96 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 28AC161F22F352E500534A96 /* PBXTargetDependency */, + ); + name = "Drink-O-MaticUITests"; + productName = "Drink-O-MaticUITests"; + productReference = 28AC161D22F352E500534A96 /* Drink-O-MaticUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 28AC15F622F352E300534A96 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Ramiro Coll Doñetz"; + TargetAttributes = { + 28AC15FD22F352E300534A96 = { + CreatedOnToolsVersion = 10.2.1; + }; + 28AC161122F352E500534A96 = { + CreatedOnToolsVersion = 10.2.1; + TestTargetID = 28AC15FD22F352E300534A96; + }; + 28AC161C22F352E500534A96 = { + CreatedOnToolsVersion = 10.2.1; + TestTargetID = 28AC15FD22F352E300534A96; + }; + }; + }; + buildConfigurationList = 28AC15F922F352E300534A96 /* Build configuration list for PBXProject "Drink-O-Matic" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 28AC15F522F352E300534A96; + productRefGroup = 28AC15FF22F352E300534A96 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 28AC15FD22F352E300534A96 /* Drink-O-Matic */, + 28AC161122F352E500534A96 /* Drink-O-MaticTests */, + 28AC161C22F352E500534A96 /* Drink-O-MaticUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 28AC15FC22F352E300534A96 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28EFF4D522F49E4C00608AD3 /* CardContentView.xib in Resources */, + 28EFF4DE22F49E4C00608AD3 /* CardCollectionViewCell.xib in Resources */, + 28AC160C22F352E500534A96 /* LaunchScreen.storyboard in Resources */, + 28AC160922F352E500534A96 /* Assets.xcassets in Resources */, + 28AC160722F352E300534A96 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28AC161022F352E500534A96 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28EFF4DF22F49E4C00608AD3 /* CardCollectionViewCell.xib in Resources */, + 28EFF4D622F49E4C00608AD3 /* CardContentView.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28AC161B22F352E500534A96 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28EFF4E022F49E4C00608AD3 /* CardCollectionViewCell.xib in Resources */, + 28EFF4D722F49E4C00608AD3 /* CardContentView.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6E4B5B7111BF089F9F0AE92E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Drink-O-MaticTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 72CC0B3C96BA913907982921 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Drink-O-Matic/Pods-Drink-O-Matic-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Drink-O-Matic/Pods-Drink-O-Matic-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Drink-O-Matic/Pods-Drink-O-Matic-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 73BDAA272CB8048DC90C66D3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Drink-O-Matic-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 78C9AE30C4FB79752315BF9E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Drink-O-MaticUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 28AC15FA22F352E300534A96 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28EFF4D222F49E4C00608AD3 /* CardContentView.swift in Sources */, + 28EFF4C122F3972900608AD3 /* Constants.swift in Sources */, + 28EFF4EC22F49E8900608AD3 /* StatusBarAnimatable.swift in Sources */, + 28EFF51622F49ECA00608AD3 /* CardTransition.swift in Sources */, + 28AC160422F352E300534A96 /* HomeViewController.swift in Sources */, + 28EFF4B922F3958400608AD3 /* APIRouter.swift in Sources */, + 28EFF51322F49ECA00608AD3 /* DismissCardAnimator.swift in Sources */, + 28EFF4E922F49E8900608AD3 /* GlobalConstants.swift in Sources */, + 28EFF4EF22F49E8900608AD3 /* UIView+AutoLayout.swift in Sources */, + 28EFF51922F49ECA00608AD3 /* CardPresentationController.swift in Sources */, + 28EFF52022F76F5400608AD3 /* GradientView.swift in Sources */, + 28AC160222F352E300534A96 /* AppDelegate.swift in Sources */, + 28EFF4F222F49E8900608AD3 /* NibLoadable.swift in Sources */, + 28EFF4C522F3994B00608AD3 /* Drink.swift in Sources */, + 28EFF50B22F49EA400608AD3 /* CardDetailViewController.swift in Sources */, + 28EFF4BD22F3959100608AD3 /* APIClient.swift in Sources */, + 28EFF51C22F49ECA00608AD3 /* PresentCardAnimator.swift in Sources */, + 28EFF4DB22F49E4C00608AD3 /* CardCollectionViewCell.swift in Sources */, + 28EFF4C922F39FD900608AD3 /* DrinkDetails.swift in Sources */, + 28EFF4FB22F49E8900608AD3 /* StatusBarAnimatableViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28AC160E22F352E500534A96 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28AC161722F352E500534A96 /* Drink_O_MaticTests.swift in Sources */, + 28EFF4F022F49E8900608AD3 /* UIView+AutoLayout.swift in Sources */, + 28EFF52122F76F5400608AD3 /* GradientView.swift in Sources */, + 28EFF51A22F49ECA00608AD3 /* CardPresentationController.swift in Sources */, + 28EFF4BA22F3958400608AD3 /* APIRouter.swift in Sources */, + 28EFF51D22F49ECA00608AD3 /* PresentCardAnimator.swift in Sources */, + 28EFF4EA22F49E8900608AD3 /* GlobalConstants.swift in Sources */, + 28EFF4BE22F3959100608AD3 /* APIClient.swift in Sources */, + 28EFF4D322F49E4C00608AD3 /* CardContentView.swift in Sources */, + 28EFF4ED22F49E8900608AD3 /* StatusBarAnimatable.swift in Sources */, + 28EFF4FC22F49E8900608AD3 /* StatusBarAnimatableViewController.swift in Sources */, + 28EFF4CA22F39FD900608AD3 /* DrinkDetails.swift in Sources */, + 28EFF4C622F3994B00608AD3 /* Drink.swift in Sources */, + 28EFF4DC22F49E4C00608AD3 /* CardCollectionViewCell.swift in Sources */, + 28EFF51422F49ECA00608AD3 /* DismissCardAnimator.swift in Sources */, + 28EFF50C22F49EA400608AD3 /* CardDetailViewController.swift in Sources */, + 28EFF4F322F49E8900608AD3 /* NibLoadable.swift in Sources */, + 28EFF4C222F3972900608AD3 /* Constants.swift in Sources */, + 28EFF51722F49ECA00608AD3 /* CardTransition.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28AC161922F352E500534A96 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28AC162222F352E500534A96 /* Drink_O_MaticUITests.swift in Sources */, + 28EFF4F122F49E8900608AD3 /* UIView+AutoLayout.swift in Sources */, + 28EFF52222F76F5400608AD3 /* GradientView.swift in Sources */, + 28EFF51B22F49ECA00608AD3 /* CardPresentationController.swift in Sources */, + 28EFF4BB22F3958400608AD3 /* APIRouter.swift in Sources */, + 28EFF51E22F49ECA00608AD3 /* PresentCardAnimator.swift in Sources */, + 28EFF4EB22F49E8900608AD3 /* GlobalConstants.swift in Sources */, + 28EFF4BF22F3959100608AD3 /* APIClient.swift in Sources */, + 28EFF4D422F49E4C00608AD3 /* CardContentView.swift in Sources */, + 28EFF4EE22F49E8900608AD3 /* StatusBarAnimatable.swift in Sources */, + 28EFF4FD22F49E8900608AD3 /* StatusBarAnimatableViewController.swift in Sources */, + 28EFF4CB22F39FD900608AD3 /* DrinkDetails.swift in Sources */, + 28EFF4C722F3994B00608AD3 /* Drink.swift in Sources */, + 28EFF4DD22F49E4C00608AD3 /* CardCollectionViewCell.swift in Sources */, + 28EFF51522F49ECA00608AD3 /* DismissCardAnimator.swift in Sources */, + 28EFF50D22F49EA400608AD3 /* CardDetailViewController.swift in Sources */, + 28EFF4F422F49E8900608AD3 /* NibLoadable.swift in Sources */, + 28EFF4C322F3972900608AD3 /* Constants.swift in Sources */, + 28EFF51822F49ECA00608AD3 /* CardTransition.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 28AC161422F352E500534A96 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 28AC15FD22F352E300534A96 /* Drink-O-Matic */; + targetProxy = 28AC161322F352E500534A96 /* PBXContainerItemProxy */; + }; + 28AC161F22F352E500534A96 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 28AC15FD22F352E300534A96 /* Drink-O-Matic */; + targetProxy = 28AC161E22F352E500534A96 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 28AC160522F352E300534A96 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 28AC160622F352E300534A96 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 28AC160A22F352E500534A96 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 28AC160B22F352E500534A96 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 28AC162422F352E500534A96 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 28AC162522F352E500534A96 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 28AC162722F352E500534A96 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3FB58FBF10A2F913A03F21D9 /* Pods-Drink-O-Matic.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 3JEJ7T2FN9; + INFOPLIST_FILE = "$(SRCROOT)/Drink-O-Matic/Supporting Files/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codetest.Drink-O-Matic"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 28AC162822F352E500534A96 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E14FE35F6EF15E3794D1139 /* Pods-Drink-O-Matic.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 3JEJ7T2FN9; + INFOPLIST_FILE = "$(SRCROOT)/Drink-O-Matic/Supporting Files/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codetest.Drink-O-Matic"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 28AC162A22F352E500534A96 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B07EFC6918A357A1E45FD335 /* Pods-Drink-O-MaticTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 3JEJ7T2FN9; + INFOPLIST_FILE = "Drink-O-MaticTests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codetest.Drink-O-MaticTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Drink-O-Matic.app/Drink-O-Matic"; + }; + name = Debug; + }; + 28AC162B22F352E500534A96 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 85C720A72337B4F217A1742F /* Pods-Drink-O-MaticTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 3JEJ7T2FN9; + INFOPLIST_FILE = "Drink-O-MaticTests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codetest.Drink-O-MaticTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Drink-O-Matic.app/Drink-O-Matic"; + }; + name = Release; + }; + 28AC162D22F352E500534A96 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C691AAB6153BB3F6B3F4CE6B /* Pods-Drink-O-MaticUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 3JEJ7T2FN9; + INFOPLIST_FILE = "Drink-O-MaticUITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codetest.Drink-O-MaticUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Drink-O-Matic"; + }; + name = Debug; + }; + 28AC162E22F352E500534A96 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D316EDD25E531DA09364BD3A /* Pods-Drink-O-MaticUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 3JEJ7T2FN9; + INFOPLIST_FILE = "Drink-O-MaticUITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codetest.Drink-O-MaticUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Drink-O-Matic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 28AC15F922F352E300534A96 /* Build configuration list for PBXProject "Drink-O-Matic" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28AC162422F352E500534A96 /* Debug */, + 28AC162522F352E500534A96 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 28AC162622F352E500534A96 /* Build configuration list for PBXNativeTarget "Drink-O-Matic" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28AC162722F352E500534A96 /* Debug */, + 28AC162822F352E500534A96 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 28AC162922F352E500534A96 /* Build configuration list for PBXNativeTarget "Drink-O-MaticTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28AC162A22F352E500534A96 /* Debug */, + 28AC162B22F352E500534A96 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 28AC162C22F352E500534A96 /* Build configuration list for PBXNativeTarget "Drink-O-MaticUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28AC162D22F352E500534A96 /* Debug */, + 28AC162E22F352E500534A96 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 28AC15F622F352E300534A96 /* Project object */; +} diff --git a/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..be29cd054 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Drink-O-Matic/Drink-O-Matic.xcworkspace/contents.xcworkspacedata b/Drink-O-Matic/Drink-O-Matic.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..3b781b54e --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Drink-O-Matic/Drink-O-Matic.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Drink-O-Matic/Drink-O-Matic.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Drink-O-Matic/Drink-O-Matic/APIClient/APIClient.swift b/Drink-O-Matic/Drink-O-Matic/APIClient/APIClient.swift new file mode 100644 index 000000000..5b9d09eed --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/APIClient/APIClient.swift @@ -0,0 +1,45 @@ +// +// APIClient.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import Alamofire + +class APIClient { + @discardableResult + private static func performRequest(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:@escaping (AFResult)->Void) -> DataRequest { + return AF.request(route) + .responseDecodable (decoder: decoder){ (response: DataResponse) in + completion(response.result) + } + } + +// static func login(email: String, password: String, completion:@escaping (Result)->Void) { +// performRequest(route: APIRouter.login(email: email, password: password), completion: completion) +// } + + static func getDrinks(completion:@escaping (AFResult)->Void) { + let jsonDecoder = JSONDecoder() + performRequest(route: APIRouter.drinks(filter: K.APIDrinksFilter.filterByCocktailGlass), decoder: jsonDecoder, completion: completion) + } + + static func getDrinkDetails(drinkId: String, completion:@escaping (AFResult)->Void) { + + AF.request(APIRouter.drink(id: drinkId)).responseJSON { (response) in + switch response.result { + case .success(let value): + if let responseDict = value as? [String: Any] { + let drinkDetail = DrinkDetails.initFromDictionary(dict: responseDict) + completion(.success(drinkDetail)) + } + case .failure(let error): + // error handling + completion(.failure(error)) + } + } + } +} + diff --git a/Drink-O-Matic/Drink-O-Matic/APIClient/APIRouter.swift b/Drink-O-Matic/Drink-O-Matic/APIClient/APIRouter.swift new file mode 100644 index 000000000..c1a54bc5a --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/APIClient/APIRouter.swift @@ -0,0 +1,99 @@ +// +// APIRouter.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import Alamofire + +enum APIRouter: URLRequestConvertible { + + case drinks(filter: [String:String]) + case drink(id: String) + + // MARK: - HTTPMethod + private var method: HTTPMethod { + switch self { +// case .login: +// return .post + case .drinks, .drink: + return .get + } + } + + // MARK: - Path + private var path: String { + switch self { +// case .login: +// return "/login" + case .drinks: + return "/filter.php" + case .drink: + return "/lookup.php" + } + } + + // MARK: - Parameters + private var queryParameters: Parameters? { + switch self { + case .drinks(let filter): + return filter + case .drink(let id): + return [K.APIParameterKey.drinkId: id] + } + } + + // MARK: - Body Parameters + private var bodyParameters: Parameters? { + switch self { +// case .login(let email, let password): +// return [K.APIParameterKey.email: email, K.APIParameterKey.password: password] + default: + return nil + } + } + + // MARK: - URLRequestConvertible + func asURLRequest() throws -> URLRequest { + //create url and query params + let baseUrl = try K.ProductionServer.baseURL.asURL() + var urlComponents = URLComponents.init(string: baseUrl.appendingPathComponent(path).absoluteString) + if let parameters = queryParameters { + urlComponents?.setQueryItems(with: parameters) + } + + //protective code + guard let requestUrl = urlComponents?.url else { + throw AFError.invalidURL(url: baseUrl.appendingPathComponent(path)) + } + + //here we start to work with the URLRequest object + var urlRequest = URLRequest(url: requestUrl) + + // HTTP Method + urlRequest.httpMethod = method.rawValue + + // Common Headers + urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue) + urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue) + + // Parameters + if let parameters = bodyParameters { + do { + urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + } + + return urlRequest + } +} + +extension URLComponents { + mutating func setQueryItems(with parameters: Parameters) { + self.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Application/AppDelegate.swift b/Drink-O-Matic/Drink-O-Matic/Application/AppDelegate.swift new file mode 100644 index 000000000..c2a36af8a --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Application/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/Drink-O-Matic/Drink-O-Matic/Models/Drink.swift b/Drink-O-Matic/Drink-O-Matic/Models/Drink.swift new file mode 100644 index 000000000..6d2c64fa0 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Models/Drink.swift @@ -0,0 +1,34 @@ +// +// Drink.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import Foundation +import UIKit + +struct DrinkList: Codable { + let drinks: [Drink] +} + +struct Drink: Codable { + let name: String + let thumbUrl: String + let id: String +} + +extension Drink { + //Customizing Key Names to match the naming style + enum CodingKeys: String, CodingKey { + case name = "strDrink" + case thumbUrl = "strDrinkThumb" + case id = "idDrink" + } + + func highlightedImage() -> Drink { +// let scaledImage = image.resize(toWidth: image.size.width * GlobalConstants.cardHighlightedFactor) + return Drink.init(name: name, thumbUrl: thumbUrl, id: id) + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Models/DrinkDetails.swift b/Drink-O-Matic/Drink-O-Matic/Models/DrinkDetails.swift new file mode 100644 index 000000000..00691ab73 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Models/DrinkDetails.swift @@ -0,0 +1,32 @@ +// +// DrinkDetails.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import UIKit + +class DrinkDetails { + var instructions: String? + var ingredients = [String]() + + static func initFromDictionary(dict: [String: Any]) -> DrinkDetails { + let drinkDetail = DrinkDetails() + + if let drinkArray = dict["drinks"] as? [Any], let drinkDict = drinkArray.first as? [String: Any] { + //parse instructions + drinkDetail.instructions = drinkDict["strInstructions"] as? String + + //parse ingredients + var ingredientCount = 1 + while let ingredient = drinkDict["strIngredient\(ingredientCount)"] as? String, !ingredient.isEmpty{ + drinkDetail.ingredients.append(ingredient) + ingredientCount += 1 + } + } + + return drinkDetail + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 000000000..8d8f4bb49 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "ItunesArtwork@2x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..b694e9af4 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..e7639a9c6 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..6f94ff3eb Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..908e1b878 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..b0526c50a Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..e45cb8581 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..e7639a9c6 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..535cf314e Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..5ff6d6632 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..5ff6d6632 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..adcd585e5 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..1ee8ea05e Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..5774eeb37 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..ed4466237 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 000000000..6248322af Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/Contents.json b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/noInternetIcon.imageset/Contents.json b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/noInternetIcon.imageset/Contents.json new file mode 100644 index 000000000..9a6ceb170 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/noInternetIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "no-wifi.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/noInternetIcon.imageset/no-wifi.png b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/noInternetIcon.imageset/no-wifi.png new file mode 100644 index 000000000..c07845802 Binary files /dev/null and b/Drink-O-Matic/Drink-O-Matic/Resources/Assets.xcassets/noInternetIcon.imageset/no-wifi.png differ diff --git a/Drink-O-Matic/Drink-O-Matic/Resources/Constants.swift b/Drink-O-Matic/Drink-O-Matic/Resources/Constants.swift new file mode 100644 index 000000000..a755b8c7d --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Resources/Constants.swift @@ -0,0 +1,43 @@ +// +// Constants.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import Foundation + +struct K { + struct ProductionServer { + static let baseURL = "https://www.thecocktaildb.com/api/json/v1/1" + } + + struct APIParameterKey { + static let drinkId = "i" + + // static let password = "password" + // static let email = "email" + } + + struct APIDrinksFilter { + //Filter by Glass + static let filterByCocktailGlass = ["g" : "Cocktail_glass"] + static let filterByChampagneFlute = ["g" : "Champagne_flute"] + + //Filter by Alcoholic + static let filterByAlcoholic = ["a" : "Alcoholic"] + static let filterByNonAlcoholic = ["a" : "Non_Alcoholic"] + } +} + +enum HTTPHeaderField: String { + case authentication = "Authorization" + case contentType = "Content-Type" + case acceptType = "Accept" + case acceptEncoding = "Accept-Encoding" +} + +enum ContentType: String { + case json = "application/json" +} diff --git a/Drink-O-Matic/Drink-O-Matic/Storyboards/Base.lproj/LaunchScreen.storyboard b/Drink-O-Matic/Drink-O-Matic/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..bfa361294 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Drink-O-Matic/Drink-O-Matic/Storyboards/Base.lproj/Main.storyboard b/Drink-O-Matic/Drink-O-Matic/Storyboards/Base.lproj/Main.storyboard new file mode 100644 index 000000000..9a2f51e24 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Storyboards/Base.lproj/Main.storyboard @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Drink-O-Matic/Drink-O-Matic/Supporting Files/Info.plist b/Drink-O-Matic/Drink-O-Matic/Supporting Files/Info.plist new file mode 100644 index 000000000..16be3b681 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Supporting Files/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Drink-O-Matic/Drink-O-Matic/Transition/CardPresentationController.swift b/Drink-O-Matic/Drink-O-Matic/Transition/CardPresentationController.swift new file mode 100755 index 000000000..9c15a740c --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Transition/CardPresentationController.swift @@ -0,0 +1,57 @@ +// +// CardPresentationController.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 31/7/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +final class CardPresentationController: UIPresentationController { + + private lazy var blurView = UIVisualEffectView(effect: nil) + + // Default is false. + // And also means you can access only `.to` when present, and `.from` when dismiss (e.g., can touch only 'presented view'). + // + // If true, the presenting view is removed and you have to add it during animation accessing `.from` key. + // And you will have access to both `.to` and `.from` view. (In the typical .fullScreen mode) + override var shouldRemovePresentersView: Bool { + return false + } + + override func presentationTransitionWillBegin() { + let container = containerView! + blurView.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(blurView) + blurView.edges(to: container) + blurView.alpha = 0.0 + + presentingViewController.beginAppearanceTransition(false, animated: false) + presentedViewController.transitionCoordinator!.animate(alongsideTransition: { (ctx) in + UIView.animate(withDuration: 0.5, animations: { + self.blurView.effect = UIBlurEffect(style: .light) + self.blurView.alpha = 1 + }) + }) { (ctx) in } + } + + override func presentationTransitionDidEnd(_ completed: Bool) { + presentingViewController.endAppearanceTransition() + } + + override func dismissalTransitionWillBegin() { + presentingViewController.beginAppearanceTransition(true, animated: true) + presentedViewController.transitionCoordinator!.animate(alongsideTransition: { (ctx) in + self.blurView.alpha = 0.0 + }, completion: nil) + } + + override func dismissalTransitionDidEnd(_ completed: Bool) { + presentingViewController.endAppearanceTransition() + if completed { + blurView.removeFromSuperview() + } + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Transition/CardTransition.swift b/Drink-O-Matic/Drink-O-Matic/Transition/CardTransition.swift new file mode 100755 index 000000000..f60175e71 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Transition/CardTransition.swift @@ -0,0 +1,54 @@ +// +// CardTransition.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 31/7/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +final class CardTransition: NSObject, UIViewControllerTransitioningDelegate { + struct Params { + let fromCardFrame: CGRect + let fromCardFrameWithoutTransform: CGRect + let fromCell: CardCollectionViewCell + } + + let params: Params + + init(params: Params) { + self.params = params + super.init() + } + + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + let params = PresentCardAnimator.Params.init( + fromCardFrame: self.params.fromCardFrame, + fromCell: self.params.fromCell + ) + return PresentCardAnimator(params: params) + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + let params = DismissCardAnimator.Params.init( + fromCardFrame: self.params.fromCardFrame, + fromCardFrameWithoutTransform: self.params.fromCardFrameWithoutTransform, + fromCell: self.params.fromCell + ) + return DismissCardAnimator(params: params) + } + + func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + return nil + } + + func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + return nil + } + + // IMPORTANT: Must set modalPresentationStyle to `.custom` for this to be used. + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + return CardPresentationController(presentedViewController: presented, presenting: presenting) + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Transition/DismissCardAnimator.swift b/Drink-O-Matic/Drink-O-Matic/Transition/DismissCardAnimator.swift new file mode 100755 index 000000000..b0fd3abfe --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Transition/DismissCardAnimator.swift @@ -0,0 +1,130 @@ +// +// DismissCardAnimator.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 7/8/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +final class DismissCardAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + struct Params { + let fromCardFrame: CGRect + let fromCardFrameWithoutTransform: CGRect + let fromCell: CardCollectionViewCell + } + + struct Constants { + static let relativeDurationBeforeNonInteractive: TimeInterval = 0.5 + static let minimumScaleBeforeNonInteractive: CGFloat = 0.8 + } + + private let params: Params + + init(params: Params) { + self.params = params + super.init() + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return GlobalConstants.dismissalAnimationDuration + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let ctx = transitionContext + let container = ctx.containerView + let screens: (cardDetail: CardDetailViewController, home: HomeViewController) = ( + ctx.viewController(forKey: .from)! as! CardDetailViewController, + ctx.viewController(forKey: .to)! as! HomeViewController + ) + + let cardDetailView = ctx.view(forKey: .from)! + + let animatedContainerView = UIView() + if GlobalConstants.isEnabledDebugAnimatingViews { + animatedContainerView.layer.borderColor = UIColor.yellow.cgColor + animatedContainerView.layer.borderWidth = 4 + cardDetailView.layer.borderColor = UIColor.red.cgColor + cardDetailView.layer.borderWidth = 2 + } + animatedContainerView.translatesAutoresizingMaskIntoConstraints = false + cardDetailView.translatesAutoresizingMaskIntoConstraints = false + + container.removeConstraints(container.constraints) + + container.addSubview(animatedContainerView) + animatedContainerView.addSubview(cardDetailView) + + // Card fills inside animated container view + cardDetailView.edges(to: animatedContainerView) + + animatedContainerView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true + let animatedContainerTopConstraint = animatedContainerView.topAnchor.constraint(equalTo: container.topAnchor, constant: 0) + let animatedContainerWidthConstraint = animatedContainerView.widthAnchor.constraint(equalToConstant: cardDetailView.frame.width) + let animatedContainerHeightConstraint = animatedContainerView.heightAnchor.constraint(equalToConstant: cardDetailView.frame.height) + + NSLayoutConstraint.activate([animatedContainerTopConstraint, animatedContainerWidthConstraint, animatedContainerHeightConstraint]) + + // Fix weird top inset + let topTemporaryFix = screens.cardDetail.cardContentView.topAnchor.constraint(equalTo: cardDetailView.topAnchor) + topTemporaryFix.isActive = GlobalConstants.isEnabledWeirdTopInsetsFix + + container.layoutIfNeeded() + + // Force card filling bottom + let stretchCardToFillBottom = screens.cardDetail.cardContentView.bottomAnchor.constraint(equalTo: cardDetailView.bottomAnchor) + + func animateCardViewBackToPlace() { + stretchCardToFillBottom.isActive = true + screens.cardDetail.isFontStateHighlighted = false + // Back to identity + // NOTE: Animated container view in a way, helps us to not messing up `transform` with `AutoLayout` animation. + cardDetailView.transform = CGAffineTransform.identity + animatedContainerTopConstraint.constant = self.params.fromCardFrameWithoutTransform.minY + animatedContainerWidthConstraint.constant = self.params.fromCardFrameWithoutTransform.width + animatedContainerHeightConstraint.constant = self.params.fromCardFrameWithoutTransform.height + + container.layoutIfNeeded() + } + + func completeEverything() { + let success = !ctx.transitionWasCancelled + animatedContainerView.removeConstraints(animatedContainerView.constraints) + animatedContainerView.removeFromSuperview() + if success { + cardDetailView.removeFromSuperview() + self.params.fromCell.isHidden = false + } else { + screens.cardDetail.isFontStateHighlighted = true + + // Remove temporary fixes if not success! + topTemporaryFix.isActive = false + stretchCardToFillBottom.isActive = false + + cardDetailView.removeConstraint(topTemporaryFix) + cardDetailView.removeConstraint(stretchCardToFillBottom) + + container.removeConstraints(container.constraints) + + container.addSubview(cardDetailView) + cardDetailView.edges(to: container) + } + ctx.completeTransition(success) + } + + UIView.animate(withDuration: transitionDuration(using: ctx), delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: { + animateCardViewBackToPlace() + }) { (finished) in + completeEverything() + } + + UIView.animate(withDuration: transitionDuration(using: ctx) * 0.6) { + screens.cardDetail.scrollView.contentOffset = .zero + + //animate gradient view alpha to show it back at home screen + screens.cardDetail.cardContentView.gradientView.alpha = 1 + } + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Transition/PresentCardAnimator.swift b/Drink-O-Matic/Drink-O-Matic/Transition/PresentCardAnimator.swift new file mode 100755 index 000000000..7410c142a --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Transition/PresentCardAnimator.swift @@ -0,0 +1,220 @@ +// +// PresentCardAnimator.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 31/7/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +final class PresentCardAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + private let params: Params + + struct Params { + let fromCardFrame: CGRect + let fromCell: CardCollectionViewCell + } + + private let presentAnimationDuration: TimeInterval + private let springAnimator: UIViewPropertyAnimator + private var transitionDriver: PresentCardTransitionDriver? + + init(params: Params) { + self.params = params + self.springAnimator = PresentCardAnimator.createBaseSpringAnimator(params: params) + self.presentAnimationDuration = springAnimator.duration + super.init() + } + + private static func createBaseSpringAnimator(params: PresentCardAnimator.Params) -> UIViewPropertyAnimator { + // Damping between 0.7 (far away) and 1.0 (nearer) + let cardPositionY = params.fromCardFrame.minY + let distanceToBounce = abs(params.fromCardFrame.minY) + let extentToBounce = cardPositionY < 0 ? params.fromCardFrame.height : UIScreen.main.bounds.height + let dampFactorInterval: CGFloat = 0.3 + let damping: CGFloat = 1.0 - dampFactorInterval * (distanceToBounce / extentToBounce) + + // Duration between 0.5 (nearer) and 0.9 (nearer) + let baselineDuration: TimeInterval = 0.5 + let maxDuration: TimeInterval = 0.9 + let duration: TimeInterval = baselineDuration + (maxDuration - baselineDuration) * TimeInterval(max(0, distanceToBounce)/UIScreen.main.bounds.height) + + let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: .init(dx: 0, dy: 0)) + return UIViewPropertyAnimator(duration: duration, timingParameters: springTiming) + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + // 1. + return presentAnimationDuration + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + // 2. + transitionDriver = PresentCardTransitionDriver(params: params, + transitionContext: transitionContext, + baseAnimator: springAnimator) + interruptibleAnimator(using: transitionContext).startAnimation() + } + + func animationEnded(_ transitionCompleted: Bool) { + // 4. + transitionDriver = nil + } + + func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { + // 3. + return transitionDriver!.animator + } +} + +final class PresentCardTransitionDriver { + let animator: UIViewPropertyAnimator + init(params: PresentCardAnimator.Params, transitionContext: UIViewControllerContextTransitioning, baseAnimator: UIViewPropertyAnimator) { + let ctx = transitionContext + let container = ctx.containerView + let screens: (home: HomeViewController, cardDetail: CardDetailViewController) = ( + ctx.viewController(forKey: .from)! as! HomeViewController, + ctx.viewController(forKey: .to)! as! CardDetailViewController + ) + + let cardDetailView = ctx.view(forKey: .to)! + let fromCardFrame = params.fromCardFrame + + // Temporary container view for animation + let animatedContainerView = UIView() + animatedContainerView.translatesAutoresizingMaskIntoConstraints = false + if GlobalConstants.isEnabledDebugAnimatingViews { + animatedContainerView.layer.borderColor = UIColor.yellow.cgColor + animatedContainerView.layer.borderWidth = 4 + cardDetailView.layer.borderColor = UIColor.red.cgColor + cardDetailView.layer.borderWidth = 2 + } + container.addSubview(animatedContainerView) + + do /* Fix centerX/width/height of animated container to container */ { + let animatedContainerConstraints = [ + animatedContainerView.widthAnchor.constraint(equalToConstant: container.bounds.width), + animatedContainerView.heightAnchor.constraint(equalToConstant: container.bounds.height), + animatedContainerView.centerXAnchor.constraint(equalTo: container.centerXAnchor) + ] + NSLayoutConstraint.activate(animatedContainerConstraints) + } + + let animatedContainerVerticalConstraint: NSLayoutConstraint = { + switch GlobalConstants.cardVerticalExpandingStyle { + case .fromCenter: + return animatedContainerView.centerYAnchor.constraint( + equalTo: container.centerYAnchor, + constant: (fromCardFrame.height/2 + fromCardFrame.minY) - container.bounds.height/2 + ) + case .fromTop: + return animatedContainerView.topAnchor.constraint(equalTo: container.topAnchor, constant: fromCardFrame.minY) + } + + }() + animatedContainerVerticalConstraint.isActive = true + + animatedContainerView.addSubview(cardDetailView) + cardDetailView.translatesAutoresizingMaskIntoConstraints = false + + let weirdCardToAnimatedContainerTopAnchor: NSLayoutConstraint + + do /* Pin top (or center Y) and center X of the card, in animated container view */ { + let verticalAnchor: NSLayoutConstraint = { + switch GlobalConstants.cardVerticalExpandingStyle { + case .fromCenter: + return cardDetailView.centerYAnchor.constraint(equalTo: animatedContainerView.centerYAnchor) + case .fromTop: + // WTF: SUPER WEIRD BUG HERE. + // I should set this constant to 0 (or nil), to make cardDetailView sticks to the animatedContainerView's top. + // BUT, I can't set constant to 0, or any value in range (-1,1) here, or there will be abrupt top space inset while animating. + // Funny how -1 and 1 work! WTF. You can try set it to 0. + return cardDetailView.topAnchor.constraint(equalTo: animatedContainerView.topAnchor, constant: -1) + } + }() + let cardConstraints = [ + verticalAnchor, + cardDetailView.centerXAnchor.constraint(equalTo: animatedContainerView.centerXAnchor), + ] + NSLayoutConstraint.activate(cardConstraints) + } + let cardWidthConstraint = cardDetailView.widthAnchor.constraint(equalToConstant: fromCardFrame.width) + let cardHeightConstraint = cardDetailView.heightAnchor.constraint(equalToConstant: fromCardFrame.height) + NSLayoutConstraint.activate([cardWidthConstraint, cardHeightConstraint]) + + cardDetailView.layer.cornerRadius = GlobalConstants.cardCornerRadius + + // ------------------------------- + // Final preparation + // ------------------------------- + params.fromCell.isHidden = true + params.fromCell.resetTransform() + + let topTemporaryFix = screens.cardDetail.cardContentView.topAnchor.constraint(equalTo: cardDetailView.topAnchor, constant: 0) + topTemporaryFix.isActive = GlobalConstants.isEnabledWeirdTopInsetsFix + + container.layoutIfNeeded() + + // ------------------------------ + // 1. Animate container bouncing up + // ------------------------------ + func animateContainerBouncingUp() { + animatedContainerVerticalConstraint.constant = 0 + container.layoutIfNeeded() + } + + // ------------------------------ + // 2. Animate cardDetail filling up the container + // ------------------------------ + func animateCardDetailViewSizing() { + cardWidthConstraint.constant = animatedContainerView.bounds.width + cardHeightConstraint.constant = animatedContainerView.bounds.height + cardDetailView.layer.cornerRadius = 0 + container.layoutIfNeeded() + + //animate gradient view alpha to hide it at detail screen + screens.cardDetail.cardContentView.gradientView.alpha = 0 + } + + func completeEverything() { + // Remove temporary `animatedContainerView` + animatedContainerView.removeConstraints(animatedContainerView.constraints) + animatedContainerView.removeFromSuperview() + + // Re-add to the top + container.addSubview(cardDetailView) + + cardDetailView.removeConstraints([topTemporaryFix, cardWidthConstraint, cardHeightConstraint]) + + // Keep -1 to be consistent with the weird bug above. + cardDetailView.edges(to: container, top: -1) + + // No longer need the bottom constraint that pins bottom of card content to its root. + screens.cardDetail.cardBottomToRootBottomConstraint.isActive = false + screens.cardDetail.scrollView.isScrollEnabled = true + + let success = !ctx.transitionWasCancelled + ctx.completeTransition(success) + } + + baseAnimator.addAnimations { + + // Spring animation for bouncing up + animateContainerBouncingUp() + + // Linear animation for expansion + let cardExpanding = UIViewPropertyAnimator(duration: baseAnimator.duration * 0.6, curve: .linear) { + animateCardDetailViewSizing() + } + cardExpanding.startAnimation() + } + + baseAnimator.addCompletion { (_) in + completeEverything() + } + + self.animator = baseAnimator + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Utils/GlobalConstants.swift b/Drink-O-Matic/Drink-O-Matic/Utils/GlobalConstants.swift new file mode 100755 index 000000000..70f6c2e4d --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Utils/GlobalConstants.swift @@ -0,0 +1,41 @@ +// +// Constants.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 31/7/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +enum GlobalConstants { + static let cardHighlightedFactor: CGFloat = 0.96 + static let statusBarAnimationDuration: TimeInterval = 0.4 + static let cardCornerRadius: CGFloat = 16 + static let dismissalAnimationDuration = 0.6 + + static let cardVerticalExpandingStyle: CardVerticalExpandingStyle = .fromTop + + + /// Without this, there'll be weird offset (probably from scrollView) that obscures the card content view of the cardDetailView. + static let isEnabledWeirdTopInsetsFix = true + + /// If true, will draw borders on animating views. + static let isEnabledDebugAnimatingViews = false + + /// If true, this will add a 'reverse' additional top safe area insets to make the final top safe area insets zero. + static let isEnabledTopSafeAreaInsetsFixOnCardDetailViewController = false + + /// If true, will always allow user to scroll while it's animated. + static let isEnabledAllowsUserInteractionWhileHighlightingCard = false +} + +extension GlobalConstants { + enum CardVerticalExpandingStyle { + /// Expanding card pinning at the top of animatingContainerView + case fromTop + + /// Expanding card pinning at the center of animatingContainerView + case fromCenter + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Utils/GradientView.swift b/Drink-O-Matic/Drink-O-Matic/Utils/GradientView.swift new file mode 100644 index 000000000..345422b92 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Utils/GradientView.swift @@ -0,0 +1,110 @@ +// +// GradientView.swift +// +// Created by Mathieu Vandeginste on 06/12/2016. +// Copyright © 2018 Mathieu Vandeginste. All rights reserved. +// + +import UIKit + +@IBDesignable class GradientView: UIView { + + private var gradientLayer: CAGradientLayer! + + @IBInspectable var topColor: UIColor = .red { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var bottomColor: UIColor = .yellow { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var shadowColor: UIColor = .clear { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var shadowX: CGFloat = 0 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var shadowY: CGFloat = -3 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var shadowBlur: CGFloat = 3 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var startPointX: CGFloat = 0 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var startPointY: CGFloat = 0.5 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var endPointX: CGFloat = 1 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var endPointY: CGFloat = 0.5 { + didSet { + setNeedsLayout() + } + } + + @IBInspectable var cornerRadius: CGFloat = 0 { + didSet { + setNeedsLayout() + } + } + + override class var layerClass: AnyClass { + return CAGradientLayer.self + } + + override func layoutSubviews() { + self.gradientLayer = self.layer as? CAGradientLayer + self.gradientLayer.colors = [topColor.cgColor, bottomColor.cgColor] + self.gradientLayer.startPoint = CGPoint(x: startPointX, y: startPointY) + self.gradientLayer.endPoint = CGPoint(x: endPointX, y: endPointY) + self.layer.cornerRadius = cornerRadius + self.layer.shadowColor = shadowColor.cgColor + self.layer.shadowOffset = CGSize(width: shadowX, height: shadowY) + self.layer.shadowRadius = shadowBlur + self.layer.shadowOpacity = 1 + + } + + func animate(duration: TimeInterval, newTopColor: UIColor, newBottomColor: UIColor) { + let fromColors = self.gradientLayer?.colors + let toColors: [AnyObject] = [ newTopColor.cgColor, newBottomColor.cgColor] + self.gradientLayer?.colors = toColors + let animation : CABasicAnimation = CABasicAnimation(keyPath: "colors") + animation.fromValue = fromColors + animation.toValue = toColors + animation.duration = duration + animation.isRemovedOnCompletion = true + animation.fillMode = .forwards + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + self.gradientLayer?.add(animation, forKey:"animateGradient") + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Utils/NibLoadable.swift b/Drink-O-Matic/Drink-O-Matic/Utils/NibLoadable.swift new file mode 100755 index 000000000..1d32b7cda --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Utils/NibLoadable.swift @@ -0,0 +1,29 @@ +// +// NibLoadable.swift +// AppStoreHomeInteractiveTransition +// +// Created by Wirawit Rueopas on 30/7/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +protocol NibLoadable where Self: UIView { + + /// Setup this view with nib: + /// 1. Load content view from nib (with the class name) + /// 2. Set owner to self + /// 3. Add it as a subview and fill edges with AutoLayout + func fromNib() -> UIView? +} + +extension NibLoadable { + @discardableResult + func fromNib() -> UIView? { + let contentView = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as! UIView + self.addSubview(contentView) + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.edges(to: self) + return contentView + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Utils/StatusBarAnimatable.swift b/Drink-O-Matic/Drink-O-Matic/Utils/StatusBarAnimatable.swift new file mode 100755 index 000000000..3605169fb --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Utils/StatusBarAnimatable.swift @@ -0,0 +1,107 @@ +// +// StatusBarAnimatable.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 1/8/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +@objc +protocol StatusBarAnimatable where Self: UIViewController { + var statusBarAnimatableHidesStatusBar: Bool { get } + var statusBarAnimatableAnimationDuration: TimeInterval { get } + var statusBarAnimatableUpdateAnimation: UIStatusBarAnimation { get } + @objc optional var statusBarAnimatableAfterInteractivityEnds: Bool { get } +} + + +private let swizzling: (AnyClass, Selector, Selector) -> () = { forClass, originalSelector, swizzledSelector in + let originalMethod = class_getInstanceMethod(forClass, originalSelector)! + let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)! + method_exchangeImplementations(originalMethod, swizzledMethod) +} + +extension UIViewController { + static func doSwizzle() { + do { + let originalSelector = #selector(viewWillAppear(_:)) + let swizzledSelector = #selector(swizzled_viewWillAppear(_:)) + swizzling(UIViewController.self, originalSelector, swizzledSelector) + } + + do { + let originalSelector = #selector(viewDidDisappear(_:)) + let swizzledSelector = #selector(swizzled_viewDidDisappear(_:)) + swizzling(UIViewController.self, originalSelector, swizzledSelector) + } + + do { + let originalSelector = #selector(getter: preferredStatusBarUpdateAnimation) + let swizzledSelector = #selector(getter: swizzled_preferredStatusBarUpdateAnimation) + swizzling(UIViewController.self, originalSelector, swizzledSelector) + } + } + + @objc func swizzled_viewWillAppear(_ animated: Bool) { + self.swizzled_viewWillAppear(animated) + if let avc = self as? StatusBarAnimatable { + avc.performViewWillAppear() + } + } + + @objc func swizzled_viewDidDisappear(_ animated: Bool) { + self.swizzled_viewDidDisappear(animated) + if let avc = self as? StatusBarAnimatable { + avc.performViewDidDisappear() + } + } + + @objc var swizzled_preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + if let avc = self as? StatusBarAnimatable { + return avc.statusBarAnimatableUpdateAnimation + } else { + return self.swizzled_preferredStatusBarUpdateAnimation + } + } +} + +private var key: Void? + +extension StatusBarAnimatable { + var shouldHideStatusBar: Bool { + get { + return (objc_getAssociatedObject(self, &key) as? Bool) ?? UIApplication.shared.isStatusBarHidden + } + + set { + objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + fileprivate func performViewWillAppear() { + guard let coordinator = transitionCoordinator else { return } + let onlyAfterNonInteractive = statusBarAnimatableAfterInteractivityEnds ?? true + if onlyAfterNonInteractive && coordinator.initiallyInteractive { + coordinator.notifyWhenInteractionChanges { [unowned self] (ctx) in + if ctx.isCancelled { return } + self.shouldHideStatusBar = self.statusBarAnimatableHidesStatusBar + UIView.animate(withDuration: self.statusBarAnimatableAnimationDuration) { + self.setNeedsStatusBarAppearanceUpdate() + } + } + } else { + coordinator.animate(alongsideTransition: { [unowned self] (_) in + self.shouldHideStatusBar = self.statusBarAnimatableHidesStatusBar + UIView.animate(withDuration: self.statusBarAnimatableAnimationDuration) { + self.setNeedsStatusBarAppearanceUpdate() + } + }) + } + } + + fileprivate func performViewDidDisappear() { + self.shouldHideStatusBar = UIApplication.shared.isStatusBarHidden + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Utils/StatusBarAnimatableViewController.swift b/Drink-O-Matic/Drink-O-Matic/Utils/StatusBarAnimatableViewController.swift new file mode 100755 index 000000000..4392bdb4d --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Utils/StatusBarAnimatableViewController.swift @@ -0,0 +1,83 @@ +// +// StatusBarAnimatableViewController.swift +// AppStoreInteractiveTransition +// +// Created by Wirawit Rueopas on 2/8/18. +// Copyright © 2018 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +/// Info about status bar animation +struct StatusBarAnimatableConfig { + let prefersHidden: Bool + let animation: UIStatusBarAnimation + + /// Animation duration for status bar. Default is `nil`, which `transitionDuration` is used. + let animationDuration: TimeInterval? + + /// Status bar animation starts after interactivity phase is ended. + let animatesAfterInteractivityEnds: Bool +} + +extension StatusBarAnimatableConfig { + init(prefersHidden: Bool, animation: UIStatusBarAnimation) { + self.init(prefersHidden: prefersHidden, + animation: animation, + animationDuration: nil, + animatesAfterInteractivityEnds: true) + } +} + + +/// No-swizzle approach for animating status bar. Subclass view controller with this and override `statusBarAnimatableConfig`. +class StatusBarAnimatableViewController: UIViewController { + + private var shouldCurrentlyHideStatusBar = false + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + guard let coordinator = transitionCoordinator else { return } + let config = statusBarAnimatableConfig + let onlyAfterNonInteractive = config.animatesAfterInteractivityEnds + + // IMPORTANT: + // If you return `interactionControllerFor_`, + // Even if it does nothing, coordinator.initiallyInteractive will be `true`, + // BUT this block registered below won't get called at all! + if onlyAfterNonInteractive && coordinator.initiallyInteractive { + coordinator.notifyWhenInteractionChanges { [unowned self] (ctx) in + if ctx.isCancelled { return } + self.shouldCurrentlyHideStatusBar = config.prefersHidden + UIView.animate(withDuration: config.animationDuration ?? ctx.transitionDuration) { + self.setNeedsStatusBarAppearanceUpdate() + } + } + } else { + coordinator.animate(alongsideTransition: { [unowned self] (ctx) in + self.shouldCurrentlyHideStatusBar = config.prefersHidden + UIView.animate(withDuration: config.animationDuration ?? ctx.transitionDuration) { + self.setNeedsStatusBarAppearanceUpdate() + } + }) + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + shouldCurrentlyHideStatusBar = UIApplication.shared.isStatusBarHidden + } + + final override var prefersStatusBarHidden: Bool { + return shouldCurrentlyHideStatusBar + } + + final override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + return statusBarAnimatableConfig.animation + } + + open var statusBarAnimatableConfig: StatusBarAnimatableConfig { + return StatusBarAnimatableConfig(prefersHidden: false, + animation: .none) + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Utils/UIView+AutoLayout.swift b/Drink-O-Matic/Drink-O-Matic/Utils/UIView+AutoLayout.swift new file mode 100755 index 000000000..6d52d843a --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Utils/UIView+AutoLayout.swift @@ -0,0 +1,40 @@ +// +// Extensions.swift +// AppStoreHomeInteractiveTransition +// +// Created by Wirawit Rueopas on 3/4/2561 BE. +// Copyright © 2561 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +extension UIView { + /// Constrain 4 edges of `self` to specified `view`. + func edges(to view: UIView, top: CGFloat=0, left: CGFloat=0, bottom: CGFloat=0, right: CGFloat=0) { + NSLayoutConstraint.activate([ + self.leftAnchor.constraint(equalTo: view.leftAnchor, constant: left), + self.rightAnchor.constraint(equalTo: view.rightAnchor, constant: right), + self.topAnchor.constraint(equalTo: view.topAnchor, constant: top), + self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: bottom) + ]) + } +} + +extension UIImage { + /// Resize UIImage to new width keeping the image's aspect ratio. + func resize(toWidth scaledToWidth: CGFloat) -> UIImage { + let image = self + let oldWidth = image.size.width + let scaleFactor = scaledToWidth / oldWidth + + let newHeight = image.size.height * scaleFactor + let newWidth = oldWidth * scaleFactor + + let scaledSize = CGSize(width:newWidth, height:newHeight) + UIGraphicsBeginImageContextWithOptions(scaledSize, true, image.scale) + image.draw(in: CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height)) + let scaledImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return scaledImage! + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/ViewControllers/CardDetailViewController.swift b/Drink-O-Matic/Drink-O-Matic/ViewControllers/CardDetailViewController.swift new file mode 100755 index 000000000..250d60996 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/ViewControllers/CardDetailViewController.swift @@ -0,0 +1,288 @@ +// +// CardDetailViewController.swift +// AppStoreHomeInteractiveTransition +// +// Created by Wirawit Rueopas on 4/4/2561 BE. +// Copyright © 2561 Wirawit Rueopas. All rights reserved. +// + +import UIKit + + +class CardDetailViewController: StatusBarAnimatableViewController, UIScrollViewDelegate { + + // This constraint limits card content to not be covered by root view. + // This is useful to make the card content expands when presenting, + // as intially the card is fully contained in a smaller environment (card cell). + // When animating detail view controller to be full-screen size, it should gradually expands along the bottom edge. + // + // ***But we dismiss disable this after presenting*** + @IBOutlet weak var cardBottomToRootBottomConstraint: NSLayoutConstraint! + + @IBOutlet weak var cardContentView: CardContentView! + @IBOutlet weak var textView: UITextView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var scrollView: UIScrollView! + + var drinkViewModel: Drink! { + didSet { + if self.cardContentView != nil { + self.cardContentView.drinkModel = drinkViewModel + } + } + } + + var drinkDetailModel: DrinkDetails! { + didSet { + if self.cardContentView != nil { + self.cardContentView.drinkDetailModel = drinkDetailModel + } + } + } + + var unhighlightedDrinkViewModel: Drink! + + var isFontStateHighlighted: Bool = true { + didSet { + cardContentView.setFontState(isHighlighted: isFontStateHighlighted) + } + } + + var draggingDownToDismiss = false + + final class DismissalPanGesture: UIPanGestureRecognizer {} + final class DismissalScreenEdgePanGesture: UIScreenEdgePanGestureRecognizer {} + + private lazy var dismissalPanGesture: DismissalPanGesture = { + let pan = DismissalPanGesture() + pan.maximumNumberOfTouches = 1 + return pan + }() + + private lazy var dismissalScreenEdgePanGesture: DismissalScreenEdgePanGesture = { + let pan = DismissalScreenEdgePanGesture() + pan.edges = .left + return pan + }() + + override func viewDidLoad() { + super.viewDidLoad() + + if GlobalConstants.isEnabledDebugAnimatingViews { + scrollView.layer.borderWidth = 3 + scrollView.layer.borderColor = UIColor.green.cgColor + + scrollView.subviews.first!.layer.borderWidth = 3 + scrollView.subviews.first!.layer.borderColor = UIColor.purple.cgColor + } + + scrollView.delegate = self + scrollView.contentInsetAdjustmentBehavior = .never + cardContentView.drinkModel = drinkViewModel + cardContentView.drinkDetailModel = drinkDetailModel + setTitleAndDescription() + cardContentView.setFontState(isHighlighted: isFontStateHighlighted) + + dismissalPanGesture.addTarget(self, action: #selector(handleDismissalPan(gesture:))) + dismissalPanGesture.delegate = self + + dismissalScreenEdgePanGesture.addTarget(self, action: #selector(handleDismissalPan(gesture:))) + dismissalScreenEdgePanGesture.delegate = self + + // Make drag down/scroll pan gesture waits til screen edge pan to fail first to begin + dismissalPanGesture.require(toFail: dismissalScreenEdgePanGesture) + scrollView.panGestureRecognizer.require(toFail: dismissalScreenEdgePanGesture) + + loadViewIfNeeded() + view.addGestureRecognizer(dismissalPanGesture) + view.addGestureRecognizer(dismissalScreenEdgePanGesture) + } + + func setTitleAndDescription() { + //set drink title + titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) + titleLabel.text = drinkViewModel.name + + //set drink description and ingredients + let descriptionTitleAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 14, weight: .bold), + .foregroundColor: UIColor.darkText] + + let descriptionBodyAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 14, weight: .light), + .foregroundColor: UIColor.darkText] + + let ingredientsAttributedString = NSMutableAttributedString() + + if let ingredientsToShow = drinkDetailModel?.ingredients { + ingredientsAttributedString.append(NSAttributedString(string: "Ingredients\n", attributes: descriptionTitleAttributes)) + + for ingredient in ingredientsToShow { + var ingredientString = "" + if ingredientsAttributedString.length > 0 { + ingredientString.append("\n") + } + ingredientString.append("• \(ingredient)") + + ingredientsAttributedString.append(NSAttributedString(string: ingredientString, attributes: descriptionBodyAttributes)) + } + } + + ingredientsAttributedString.append(NSAttributedString(string: "\n\nHow to prepare", attributes: descriptionTitleAttributes)) + + if let instructions = drinkDetailModel.instructions, !instructions.isEmpty { + ingredientsAttributedString.append(NSAttributedString(string: "\n\n" + instructions, attributes: descriptionBodyAttributes)) + } else { + ingredientsAttributedString.append(NSAttributedString(string: "\n\nNo description available", attributes: descriptionBodyAttributes)) + } + + descriptionLabel.attributedText = ingredientsAttributedString + } + + func didSuccessfullyDragDownToDismiss() { + drinkViewModel = unhighlightedDrinkViewModel + dismiss(animated: true) + } + + func userWillCancelDissmissalByDraggingToTop(velocityY: CGFloat) {} + + func didCancelDismissalTransition() { + // Clean up + interactiveStartingPoint = nil + dismissalAnimator = nil + draggingDownToDismiss = false + } + + var interactiveStartingPoint: CGPoint? + var dismissalAnimator: UIViewPropertyAnimator? + + // This handles both screen edge and dragdown pan. As screen edge pan is a subclass of pan gesture, this input param works. + @objc func handleDismissalPan(gesture: UIPanGestureRecognizer) { + + let isScreenEdgePan = gesture.isKind(of: DismissalScreenEdgePanGesture.self) + let canStartDragDownToDismissPan = !isScreenEdgePan && !draggingDownToDismiss + + // Don't do anything when it's not in the drag down mode + if canStartDragDownToDismissPan { return } + + let targetAnimatedView = gesture.view! + let startingPoint: CGPoint + + if let p = interactiveStartingPoint { + startingPoint = p + } else { + // Initial location + startingPoint = gesture.location(in: nil) + interactiveStartingPoint = startingPoint + } + + let currentLocation = gesture.location(in: nil) + let progress = isScreenEdgePan ? (gesture.translation(in: targetAnimatedView).x / 100) : (currentLocation.y - startingPoint.y) / 100 + let targetShrinkScale: CGFloat = 0.86 + let targetCornerRadius: CGFloat = GlobalConstants.cardCornerRadius + + func createInteractiveDismissalAnimatorIfNeeded() -> UIViewPropertyAnimator { + if let animator = dismissalAnimator { + return animator + } else { + let animator = UIViewPropertyAnimator(duration: 0, curve: .linear, animations: { + targetAnimatedView.transform = .init(scaleX: targetShrinkScale, y: targetShrinkScale) + targetAnimatedView.layer.cornerRadius = targetCornerRadius + + }) + animator.isReversed = false + animator.pauseAnimation() + animator.fractionComplete = progress + return animator + } + } + + switch gesture.state { + case .began: + dismissalAnimator = createInteractiveDismissalAnimatorIfNeeded() + + case .changed: + dismissalAnimator = createInteractiveDismissalAnimatorIfNeeded() + + let actualProgress = progress + let isDismissalSuccess = actualProgress >= 1.0 + + dismissalAnimator!.fractionComplete = actualProgress + + if isDismissalSuccess { + dismissalAnimator!.stopAnimation(false) + dismissalAnimator!.addCompletion { [unowned self] (pos) in + switch pos { + case .end: + self.didSuccessfullyDragDownToDismiss() + default: + fatalError("Must finish dismissal at end!") + } + } + dismissalAnimator!.finishAnimation(at: .end) + } + + case .ended, .cancelled: + if dismissalAnimator == nil { + // Gesture's too quick that it doesn't have dismissalAnimator! + print("Too quick there's no animator!") + didCancelDismissalTransition() + return + } + // NOTE: + // If user lift fingers -> ended + // If gesture.isEnabled -> cancelled + + // Ended, Animate back to start + dismissalAnimator!.pauseAnimation() + dismissalAnimator!.isReversed = true + + // Disable gesture until reverse closing animation finishes. + gesture.isEnabled = false + dismissalAnimator!.addCompletion { [unowned self] (pos) in + self.didCancelDismissalTransition() + gesture.isEnabled = true + } + dismissalAnimator!.startAnimation() + default: + fatalError("Impossible gesture state? \(gesture.state.rawValue)") + } + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + scrollView.scrollIndicatorInsets = .init(top: cardContentView.bounds.height, left: 0, bottom: 0, right: 0) + if GlobalConstants.isEnabledTopSafeAreaInsetsFixOnCardDetailViewController { + self.additionalSafeAreaInsets = .init(top: max(-view.safeAreaInsets.top,0), left: 0, bottom: 0, right: 0) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if draggingDownToDismiss || (scrollView.isTracking && scrollView.contentOffset.y < 0) { + draggingDownToDismiss = true + scrollView.contentOffset = .zero + } + + scrollView.showsVerticalScrollIndicator = !draggingDownToDismiss + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + // Without this, when user drag down and lift the finger fast at the top, there'll be some scrolling going on. + // This check prevents that. + if velocity.y > 0 && scrollView.contentOffset.y <= 0 { + scrollView.contentOffset = .zero + } + } + + override var statusBarAnimatableConfig: StatusBarAnimatableConfig { + return StatusBarAnimatableConfig(prefersHidden: true, + animation: .slide) + } +} + +extension CardDetailViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/ViewControllers/HomeViewController.swift b/Drink-O-Matic/Drink-O-Matic/ViewControllers/HomeViewController.swift new file mode 100644 index 000000000..64e0f1b99 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/ViewControllers/HomeViewController.swift @@ -0,0 +1,259 @@ +// +// ViewController.swift +// Drink-O-Matic +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import UIKit + +class HomeViewController: StatusBarAnimatableViewController { + + private var isFetchingDrinks: Bool = true { + didSet { + if isFetchingDrinks { + //show loading view and hide ingredients label + UIView.animate(withDuration: 0.3) { + self.collectionView.isHidden = true + self.noInternetView.isHidden = true + self.activityIndicator.isHidden = false + } + } else { + UIView.animate(withDuration: 0.3) { + self.collectionView.isHidden = false + self.noInternetView.isHidden = false + self.activityIndicator.isHidden = true + } + } + } + } + + enum HomeState { + case loading + case error + case success + } + + var state: HomeState? { + didSet { + guard let state = state else { return } + switch state { + case .error: + UIView.animate(withDuration: 0.3) { + self.collectionView.isHidden = true + self.noInternetView.isHidden = false + self.activityIndicator.stopAnimating() + } + case .loading: + UIView.animate(withDuration: 0.3) { + self.collectionView.isHidden = true + self.noInternetView.isHidden = true + self.activityIndicator.startAnimating() + } + case .success: + UIView.animate(withDuration: 0.3) { + self.collectionView.isHidden = false + self.noInternetView.isHidden = true + self.activityIndicator.stopAnimating() + self.refreshControl.endRefreshing() + } + } + } + } + + @IBOutlet weak var collectionView: UICollectionView! + + private var transition: CardTransition? + + var drinks = [Drink]() + var drinkDetails = [String:DrinkDetails]() + + private let refreshControl = UIRefreshControl() + @IBOutlet weak var noInternetView: UIView! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + + override func viewDidLoad() { + super.viewDidLoad() + + // Make it responds to highlight state faster + collectionView.delaysContentTouches = false + + if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { + layout.minimumLineSpacing = 20 + layout.minimumInteritemSpacing = 0 + layout.sectionInset = .init(top: 20, left: 0, bottom: 64, right: 0) + } + + collectionView.delegate = self + collectionView.dataSource = self + collectionView.clipsToBounds = false + collectionView.register(UINib(nibName: "\(CardCollectionViewCell.self)", bundle: nil), forCellWithReuseIdentifier: "card") + + // Add Refresh Control to Collection View + if #available(iOS 10.0, *) { + collectionView.refreshControl = refreshControl + } else { + collectionView.addSubview(refreshControl) + } + + // Configure Refresh Control + refreshControl.addTarget(self, action: #selector(fetchDrinks(_:)), for: .valueChanged) + + fetchDrinks() + } + + override var statusBarAnimatableConfig: StatusBarAnimatableConfig { + return StatusBarAnimatableConfig(prefersHidden: false, + animation: .slide) + } + + @IBAction func tryAgainPressed(_ sender: Any) { + fetchDrinks() + } +} + +// MARK: - Server Fetch Methods +extension HomeViewController { + @objc private func fetchDrinks(_ sender: Any? = nil) { + //set screen state to update UI + state = .loading + + //query to get drinks list from server + APIClient.getDrinks{ result in + switch result { + case .success(let drinkList): + //clear data from arrays + self.drinks.removeAll() + self.drinkDetails.removeAll() + + if drinkList.drinks.isEmpty { + //response array is empty, return error screen + self.state = .error + } else { + //update array and collectionview + self.drinks = drinkList.drinks + self.collectionView.reloadData() + self.state = .success + } + case .failure(let error): + self.state = .error + print(error.localizedDescription) + } + } + } + + private func fetchDrinkDetail(drinkId: String, indexPath: IndexPath, completion:@escaping (DrinkDetails)->Void) { + //fetch single drink detail based on id + APIClient.getDrinkDetails(drinkId: drinkId) { (result) in + switch result { + case .success(let drinkDetail): + completion(drinkDetail) + case .failure(let error): + self.state = .error + print(error.localizedDescription) + } + } + } +} + +extension HomeViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return drinks.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + return collectionView.dequeueReusableCell(withReuseIdentifier: "card", for: indexPath) + } + + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + let cell = cell as! CardCollectionViewCell + //get drink from array and set it on cell + let drink = drinks[indexPath.row] + cell.cardContentView?.drinkModel = drink + + //check if we already have the drinkDetail for that drinkId + if let drinkDetail = drinkDetails[drink.id] { + //set the detail at the cell + cell.cardContentView?.drinkDetailModel = drinkDetail + } else { + //fetch drinkDetail from server and update cell at completion + cell.cardContentView?.isFetchingDetails = true + fetchDrinkDetail(drinkId: drink.id, indexPath: indexPath, completion: { drinkDetail in + self.drinkDetails[drink.id] = drinkDetail + self.collectionView.reloadItems(at: [indexPath]) + }) + } + } + + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + let cell = cell as! CardCollectionViewCell + cell.cardContentView.imageView.image = nil + cell.cardContentView.imageView.setNeedsDisplay() + } +} + +extension HomeViewController { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let cardHorizontalOffset: CGFloat = 20 + let cardHeightByWidthRatio: CGFloat = 1.2 + let width = collectionView.bounds.size.width - 2 * cardHorizontalOffset + let height: CGFloat = width * cardHeightByWidthRatio + return CGSize(width: width, height: height) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + // Get tapped cell location + let cell = collectionView.cellForItem(at: indexPath) as! CardCollectionViewCell + + // Freeze highlighted state (or else it will bounce back) + cell.freezeAnimations() + + // Get current frame on screen + let currentCellFrame = cell.layer.presentation()!.frame + + // Convert current frame to screen's coordinates + let cardPresentationFrameOnScreen = cell.superview!.convert(currentCellFrame, to: nil) + + // Get card frame without transform in screen's coordinates (for the dismissing back later to original location) + let cardFrameWithoutTransform = { () -> CGRect in + let center = cell.center + let size = cell.bounds.size + let r = CGRect( + x: center.x - size.width / 2, + y: center.y - size.height / 2, + width: size.width, + height: size.height + ) + return cell.superview!.convert(r, to: nil) + }() + + let drinkModel = drinks[indexPath.row] + let drinkDetailModel = drinkDetails[drinkModel.id] + + // Set up card detail view controller + let vc = storyboard!.instantiateViewController(withIdentifier: "cardDetailVc") as! CardDetailViewController + vc.drinkViewModel = drinkModel.highlightedImage() + vc.unhighlightedDrinkViewModel = drinkModel // Keep the original one to restore when dismiss + vc.drinkDetailModel = drinkDetailModel + let params = CardTransition.Params(fromCardFrame: cardPresentationFrameOnScreen, + fromCardFrameWithoutTransform: cardFrameWithoutTransform, + fromCell: cell) + transition = CardTransition(params: params) + vc.transitioningDelegate = transition + + // If `modalPresentationStyle` is not `.fullScreen`, this should be set to true to make status bar depends on presented vc. + vc.modalPresentationCapturesStatusBarAppearance = true + vc.modalPresentationStyle = .custom + + present(vc, animated: true, completion: { [unowned cell] in + // Unfreeze + cell.unfreezeAnimations() + }) + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Views/CardCollectionViewCell.swift b/Drink-O-Matic/Drink-O-Matic/Views/CardCollectionViewCell.swift new file mode 100755 index 000000000..085e88fda --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Views/CardCollectionViewCell.swift @@ -0,0 +1,81 @@ +// +// CardCollectionViewCell.swift +// AppStoreHomeInteractiveTransition +// +// Created by Wirawit Rueopas on 31/3/2561 BE. +// Copyright © 2561 Wirawit Rueopas. All rights reserved. +// + +import UIKit + +final class CardCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var cardContentView: CardContentView! + + var disabledHighlightedAnimation = false + + func resetTransform() { + transform = .identity + } + + func freezeAnimations() { + disabledHighlightedAnimation = true + layer.removeAllAnimations() + } + + func unfreezeAnimations() { + disabledHighlightedAnimation = false + } + + override func awakeFromNib() { + cardContentView.layer.cornerRadius = 16 + cardContentView.layer.masksToBounds = true + cardContentView.ingredientsLabel.isHidden = false + backgroundColor = .clear + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.2 + layer.shadowOffset = .init(width: 0, height: 4) + layer.shadowRadius = 12 + } + + // Make it appears very responsive to touch + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + animate(isHighlighted: true) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + animate(isHighlighted: false) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + animate(isHighlighted: false) + } + + private func animate(isHighlighted: Bool, completion: ((Bool) -> Void)?=nil) { + if disabledHighlightedAnimation { + return + } + let animationOptions: UIView.AnimationOptions = GlobalConstants.isEnabledAllowsUserInteractionWhileHighlightingCard + ? [.allowUserInteraction] : [] + if isHighlighted { + UIView.animate(withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: animationOptions, animations: { + self.transform = .init(scaleX: GlobalConstants.cardHighlightedFactor, y: GlobalConstants.cardHighlightedFactor) + }, completion: completion) + } else { + UIView.animate(withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: animationOptions, animations: { + self.transform = .identity + }, completion: completion) + } + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Views/CardCollectionViewCell.xib b/Drink-O-Matic/Drink-O-Matic/Views/CardCollectionViewCell.xib new file mode 100755 index 000000000..dfe43b93c --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Views/CardCollectionViewCell.xib @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Drink-O-Matic/Drink-O-Matic/Views/CardContentView.swift b/Drink-O-Matic/Drink-O-Matic/Views/CardContentView.swift new file mode 100755 index 000000000..9bba3bab2 --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Views/CardContentView.swift @@ -0,0 +1,124 @@ +// +// CardContentView.swift +// AppStoreHomeInteractiveTransition +// +// Created by Wirawit Rueopas on 3/4/2561 BE. +// Copyright © 2561 Wirawit Rueopas. All rights reserved. +// + +import UIKit +import SDWebImage + +@IBDesignable final class CardContentView: UIView, NibLoadable { + + static let linesOfVisibleIngredients = 2 + + var drinkModel: Drink? { + didSet { + titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) + titleLabel.text = drinkModel?.name + if let imageString = drinkModel?.thumbUrl, let imageUrl = URL.init(string: imageString) { + imageView.sd_imageTransition = .fade + imageView.sd_setImage(with: imageUrl, completed: nil) + } + } + } + + var drinkDetailModel: DrinkDetails? { + didSet { + let ingredientsAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 14, weight: .bold), + .foregroundColor: UIColor.white] + + let moreIngredientsAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 14, weight: .light), + .foregroundColor: UIColor.white] + + let ingredientsAttributedString = NSMutableAttributedString() + + if let ingredientsToShow = drinkDetailModel?.ingredients.prefix(CardContentView.linesOfVisibleIngredients) { + for ingredient in ingredientsToShow { + var ingredientString = "" + if ingredientsAttributedString.length > 0 { + ingredientString.append("\n") + } + ingredientString.append("• \(ingredient)") + + ingredientsAttributedString.append(NSAttributedString(string: ingredientString, attributes: ingredientsAttributes)) + } + } + + if let ingredientsToCount = drinkDetailModel?.ingredients.dropFirst(CardContentView.linesOfVisibleIngredients), !ingredientsToCount.isEmpty { + ingredientsAttributedString.append(NSAttributedString(string: "\nand \(ingredientsToCount.count) ingredients more", attributes: moreIngredientsAttributes)) + } + + ingredientsLabel.attributedText = ingredientsAttributedString + + isFetchingDetails = false + } + } + + var isFetchingDetails: Bool = true { + didSet { + if isFetchingDetails { + //show loading view and hide ingredients label + UIView.animate(withDuration: 0.3) { + self.ingredientsLabel.isHidden = true + self.activityIndicator.startAnimating() + } + } else { + UIView.animate(withDuration: 0.3) { + self.ingredientsLabel.isHidden = false + self.activityIndicator.stopAnimating() + } + } + } + } + + + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var ingredientsLabel: UILabel! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var gradientView: GradientView! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + + @IBOutlet weak var imageToTopAnchor: NSLayoutConstraint! + @IBOutlet weak var imageToLeadingAnchor: NSLayoutConstraint! + @IBOutlet weak var imageToTrailingAnchor: NSLayoutConstraint! + @IBOutlet weak var imageToBottomAnchor: NSLayoutConstraint! + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + fromNib() + commonSetup() + } + + override init(frame: CGRect) { + super.init(frame: frame) + fromNib() + commonSetup() + } + + override func awakeFromNib() { + super.awakeFromNib() + commonSetup() + } + + private func commonSetup() { + // *Make the background image stays still at the center while we animationg, + // else the image will get resized during animation. + imageView.contentMode = .scaleAspectFill + setFontState(isHighlighted: false) + } + + // This "connects" highlighted (pressedDown) font's sizes with the destination card's font sizes + func setFontState(isHighlighted: Bool) { + if isHighlighted { +// titleLabel.font = UIFont.systemFont(ofSize: 24 * GlobalConstants.cardHighlightedFactor, weight: .bold) +// ingredientsLabel.font = UIFont.systemFont(ofSize: 40 * GlobalConstants.cardHighlightedFactor, weight: .semibold) + } else { +// titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) +// ingredientsLabel.font = UIFont.systemFont(ofSize: 40, weight: .semibold) + } + } +} diff --git a/Drink-O-Matic/Drink-O-Matic/Views/CardContentView.xib b/Drink-O-Matic/Drink-O-Matic/Views/CardContentView.xib new file mode 100755 index 000000000..e96660b8b --- /dev/null +++ b/Drink-O-Matic/Drink-O-Matic/Views/CardContentView.xib @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Drink-O-Matic/Drink-O-MaticTests/Drink_O_MaticTests.swift b/Drink-O-Matic/Drink-O-MaticTests/Drink_O_MaticTests.swift new file mode 100644 index 000000000..fbf0a5e33 --- /dev/null +++ b/Drink-O-Matic/Drink-O-MaticTests/Drink_O_MaticTests.swift @@ -0,0 +1,34 @@ +// +// Drink_O_MaticTests.swift +// Drink-O-MaticTests +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import XCTest +@testable import Drink_O_Matic + +class Drink_O_MaticTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Drink-O-Matic/Drink-O-MaticTests/Info.plist b/Drink-O-Matic/Drink-O-MaticTests/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/Drink-O-Matic/Drink-O-MaticTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Drink-O-Matic/Drink-O-MaticUITests/Drink_O_MaticUITests.swift b/Drink-O-Matic/Drink-O-MaticUITests/Drink_O_MaticUITests.swift new file mode 100644 index 000000000..111c725f9 --- /dev/null +++ b/Drink-O-Matic/Drink-O-MaticUITests/Drink_O_MaticUITests.swift @@ -0,0 +1,34 @@ +// +// Drink_O_MaticUITests.swift +// Drink-O-MaticUITests +// +// Created by Ramiro Coll Doñetz on 01/08/2019. +// Copyright © 2019 Ramiro Coll Doñetz. All rights reserved. +// + +import XCTest + +class Drink_O_MaticUITests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + +} diff --git a/Drink-O-Matic/Drink-O-MaticUITests/Info.plist b/Drink-O-Matic/Drink-O-MaticUITests/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/Drink-O-Matic/Drink-O-MaticUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Drink-O-Matic/Podfile b/Drink-O-Matic/Podfile new file mode 100644 index 000000000..41848f2cd --- /dev/null +++ b/Drink-O-Matic/Podfile @@ -0,0 +1,23 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'Drink-O-Matic' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for Drink-O-Matic + + pod 'Alamofire', '~> 5.0.0-beta.5' + pod 'SDWebImage', '~> 5.0' + + target 'Drink-O-MaticTests' do + inherit! :search_paths + # Pods for testing + end + + target 'Drink-O-MaticUITests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/Drink-O-Matic/Podfile.lock b/Drink-O-Matic/Podfile.lock new file mode 100644 index 000000000..79bf05f51 --- /dev/null +++ b/Drink-O-Matic/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - Alamofire (5.0.0-beta.7) + - SDWebImage (5.0.6): + - SDWebImage/Core (= 5.0.6) + - SDWebImage/Core (5.0.6) + +DEPENDENCIES: + - Alamofire (~> 5.0.0-beta.5) + - SDWebImage (~> 5.0) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - Alamofire + - SDWebImage + +SPEC CHECKSUMS: + Alamofire: bd07938ceecad59d2492aeecb1f785a8f2721160 + SDWebImage: 920f1a2ff1ca8296ad34f6e0510a1ef1d70ac965 + +PODFILE CHECKSUM: 5e18ca987c770749227fcaff2bd21048e155b54f + +COCOAPODS: 1.7.5 diff --git a/README.md b/README.md index 11bf4f62a..51bd98fa1 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,22 @@ Please clone the repository, complete the exercise, and submit a PR for us to re A) Describe the strategy used to consume the API endpoints and the data management. +To consume the API endpoints I used Alamofire5 (beta version). If you check the two models, you'll notice that I used different approachs to parse the data. At Drink model, I used the new Codable Swift object, that gives you a super easy and quick way to parse JSON response into models. For the DrinkDetail, I used standard dict parsing to fetch properly the N ingredients. + B) Explain which library was used for the routing and why. Would you use the same for a consumer facing app targeting thousands of users? Why? +I used a different approach for navigation in the app basing myself in a demo proyect. Using advanced transitions I copied the navigation from AppStore, using UIViewControllerTransitioningDelegate to achive it. + C) Have you used any strategy to optimize the performance of the list generated for the first feature? +Yes, I'm fetching each drink detail at the willDisplay delegate of the collectionview and requesting it from API asynchronously. Also, I'm caching the models into an array during the applife time, so we only need to fetch it once. If you perform a pull to request at the collectionview, all data will be refreshed from server. Anyway, this can be a lot better with a better API. + D) Would you like to add any further comments or observations? +The API haves a really poor design. It can be improved in a lot of different ways, for example: + - Having all the data that we need at home screen at the first request. + - Using pagination + - Using timestamps to keep track of changes in data. In this way, we can use a database that can keep record of all drinks and just update some if is necessary. ## Overview: