├── .gitignore
├── .swift-version
├── .swiftlint.yml
├── Podfile
├── Podfile.lock
├── README.md
├── Recipes.xcodeproj
└── project.pbxproj
├── RecipesTests
├── Fixture
│ ├── recipes.json
│ └── singleRecipe.json
├── Info.plist
├── Library
│ ├── Model
│ │ └── RecipeTests.swift
│ ├── Service
│ │ ├── CacheServiceTests.swift
│ │ └── RecipesService.swift
│ └── Utils
│ │ └── DebouncerTests.swift
└── Mock
│ └── MockNetworkService.swift
├── RecipesUITests
├── Info.plist
└── RecipesUITests.swift
├── Resource
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon_1024@1x.png
│ │ ├── icon_20@1x.png
│ │ ├── icon_20@2x.png
│ │ ├── icon_20@3x.png
│ │ ├── icon_29@1x.png
│ │ ├── icon_29@2x.png
│ │ ├── icon_29@3x.png
│ │ ├── icon_40@1x.png
│ │ ├── icon_40@2x.png
│ │ ├── icon_40@3x.png
│ │ ├── icon_60@2x.png
│ │ ├── icon_60@3x.png
│ │ ├── icon_76@1x.png
│ │ ├── icon_76@2x.png
│ │ └── icon_83.5@2x.png
│ ├── Contents.json
│ ├── launchImage.imageset
│ │ ├── Contents.json
│ │ └── Spoon.jpg
│ ├── notFound.imageset
│ │ ├── Contents.json
│ │ └── notFound.png
│ └── recipePlaceholder.imageset
│ │ ├── Contents.json
│ │ └── recipePlaceholder.jpg
├── Base.lproj
│ └── LaunchScreen.storyboard
└── Info.plist
├── Screenshots
├── AppIcon.png
├── AppStore.png
├── Detail.png
├── Home.png
├── Insomnia.png
├── LaunchScreen.png
├── MARK.png
├── MainGuard.png
├── Measurement.png
├── Project.png
└── SwiftLint.png
└── Source
├── Feature
├── Detail
│ ├── RecipeDetailView.swift
│ └── RecipeDetailViewController.swift
├── Home
│ └── HomeViewController.swift
├── List
│ ├── RecipeCell.swift
│ └── RecipeListViewController.swift
└── Search
│ └── SearchComponent.swift
└── Library
├── Adapter
└── Adapter.swift
├── App
├── AppConfig.swift
├── AppDelegate.swift
└── Color.swift
├── Base
└── BaseController.swift
├── Constants
└── R.generated.swift
├── Extensions
├── NSLayoutConstraint+Extensions.swift
├── UICollectionView+Extensions.swift
├── UIColor+Extensions.swift
├── UIImageView+Extensions.swift
├── UIView+Extensions.swift
└── UIViewController+Extensions.swift
├── Flow
├── AppFlowController.swift
└── RecipeFlowController.swift
├── Model
└── Recipe.swift
├── Networking
├── Networking.swift
└── Resource.swift
├── Service
├── CacheService.swift
├── ImageService.swift
├── NetworkService.swift
└── RecipesService.swift
├── Utils
└── Debouncer.swift
└── View
├── EmptyView.swift
└── ScrollableView.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | .DS_Store
3 | */build/*
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | profile
14 | *.moved-aside
15 | DerivedData
16 | .idea/
17 | *.hmap
18 | *.xccheckout
19 | *.xcworkspace
20 | !default.xcworkspace
21 |
22 | #CocoaPods
23 | Pods
24 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 4.1
2 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - nesting
3 | - closure_parameter_position
4 | - type_name
5 | - for_where
6 | - class_delegate_protocol
7 | opt_in_rules: # some rules are only opt-in
8 | - empty_count
9 | # Find all the available rules by running:
10 | # swiftlint rules
11 | included: # paths to include during linting. `--path` is ignored if present.
12 | - Source
13 | excluded: # paths to ignore during linting. Takes precedence over `included`.
14 | - Carthage
15 | - Pods
16 | - Source/Library/Constants/R.generated.swift
17 |
18 | # configurable rules can be customized from this configuration file
19 | # binary rules can set their severity level
20 | force_cast: warning # implicitly
21 | force_try:
22 | severity: warning # explicitly
23 | # rules that have both warning and error levels, can set just the warning level
24 | # implicitly
25 | line_length: 200
26 | function_body_length:
27 | - 150 #warning
28 | # they can set both implicitly with an array
29 | type_body_length:
30 | - 300 # warning
31 | - 400 # error
32 | # or they can set both explicitly
33 | file_length:
34 | warning: 500
35 | error: 1200
36 | # naming rules can set warnings/errors for min_length and max_length
37 | # additionally they can set excluded names
38 | type_name:
39 | min_length: 2 # only warning
40 | max_length: # warning and error
41 | warning: 40
42 | error: 50
43 | excluded: iPhone # excluded via string
44 | variable_name:
45 | min_length: # only min_length
46 | error: 2 # only error
47 | excluded: # excluded via string array
48 | - me
49 | - x
50 | - y
51 | - id
52 | - URL
53 | - GlobalAPIKey
54 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle)
55 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 | platform :ios, '9.0'
3 |
4 | target 'Recipes' do
5 |
6 | use_frameworks!
7 |
8 | pod 'SwiftLint', '~> 0.25'
9 | pod 'SwiftHash', '~> 2.0'
10 | pod 'R.swift', '~> 4.0'
11 |
12 | # Pods for Recipes
13 |
14 | target 'RecipesTests' do
15 | inherit! :search_paths
16 | # Pods for testing
17 | end
18 |
19 | target 'RecipesUITests' do
20 | inherit! :search_paths
21 | # Pods for testing
22 | end
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - R.swift (4.0.0):
3 | - R.swift.Library (~> 4.0.0)
4 | - R.swift.Library (4.0.0)
5 | - SwiftHash (2.0.1)
6 | - SwiftLint (0.25.0)
7 |
8 | DEPENDENCIES:
9 | - R.swift (~> 4.0)
10 | - SwiftHash (~> 2.0)
11 | - SwiftLint (~> 0.25)
12 |
13 | SPEC CHECKSUMS:
14 | R.swift: d6a5ec2f55a8441dc0ed9f1f8b37d7d11ae85c66
15 | R.swift.Library: c3af34921024333546e23b70e70d0b4e0cffca75
16 | SwiftHash: c805de5a434eacfa0dac7210745a50c55d7fe33d
17 | SwiftLint: e14651157288e9e01d6e1a71db7014fb5744a8ea
18 |
19 | PODFILE CHECKSUM: 50e70ce7458761321dd026beefe340c38b858fe6
20 |
21 | COCOAPODS: 1.4.0
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Recipes App
2 | ==
3 |
4 | ❤️ Support my apps ❤️
5 |
6 | - [Push Hero - pure Swift native macOS application to test push notifications](https://onmyway133.com/pushhero)
7 | - [PastePal - Pasteboard, note and shortcut manager](https://onmyway133.com/pastepal)
8 | - [Quick Check - smart todo manager](https://onmyway133.com/quickcheck)
9 | - [Alias - App and file shortcut manager](https://onmyway133.com/alias)
10 | - [My other apps](https://onmyway133.com/apps/)
11 |
12 | ❤️❤️😇😍🤘❤️❤️
13 |
14 | ## Description
15 |
16 | - An app that showcases many recipes together with their detail information
17 | - Contain lots of good practices for structuring iOS app and handle dependencies
18 | - Support iOS 9+
19 | - Use Xcode 9.3, Swift 4.0
20 | - Please read [Learn iOS best practices by building a simple recipes app](https://medium.freecodecamp.org/learn-ios-best-practices-by-building-a-simple-recipes-app-9bcbce4d10d)
21 |
22 |
23 |

24 |

25 |
26 |
27 | ## Credit
28 |
29 | - Launch image is from http://desertrosemediapa.com/
30 | - App icon is from https://www.flaticon.com/free-icon/rice_168559
31 |
32 | ## Licence
33 |
34 | This project is released under the MIT license. See LICENSE.md.
35 |
--------------------------------------------------------------------------------
/Recipes.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 48;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2DB1405B29CF577A4C5AC6A0 /* Pods_RecipesUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CAFF210BA0C48F271B66FF16 /* Pods_RecipesUITests.framework */; };
11 | 456A010A541FE67397DD3F07 /* Pods_RecipesTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C603ABB2DDC033874DEEFEB5 /* Pods_RecipesTests.framework */; };
12 | D2144AA2204212DD005CD925 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AA1204212DD005CD925 /* UIColor+Extensions.swift */; };
13 | D2144AA420421328005CD925 /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AA320421328005CD925 /* NSLayoutConstraint+Extensions.swift */; };
14 | D2144AAA2042146D005CD925 /* RecipeDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AA92042146D005CD925 /* RecipeDetailViewController.swift */; };
15 | D2144AAD204214D4005CD925 /* RecipeListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AAC204214D4005CD925 /* RecipeListViewController.swift */; };
16 | D2144AB3204216CB005CD925 /* AppFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AB2204216CB005CD925 /* AppFlowController.swift */; };
17 | D2144AB5204216FC005CD925 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AB4204216FC005CD925 /* UIViewController+Extensions.swift */; };
18 | D2144AB72042173F005CD925 /* RecipeFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AB62042173F005CD925 /* RecipeFlowController.swift */; };
19 | D2144ABE20421C16005CD925 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144ABD20421C16005CD925 /* Adapter.swift */; };
20 | D2144AC020421E4C005CD925 /* RecipeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144ABF20421E4C005CD925 /* RecipeCell.swift */; };
21 | D2144AC42042267A005CD925 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AC32042267A005CD925 /* AppDelegate.swift */; };
22 | D2144AC620422686005CD925 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AC520422686005CD925 /* AppConfig.swift */; };
23 | D2144AC8204226F6005CD925 /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AC7204226F6005CD925 /* UICollectionView+Extensions.swift */; };
24 | D2144ACA20422E19005CD925 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AC920422E19005CD925 /* Color.swift */; };
25 | D2144ACD20422EA5005CD925 /* ScrollableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144ACC20422EA5005CD925 /* ScrollableView.swift */; };
26 | D2144ACF20422EAE005CD925 /* BaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144ACE20422EAE005CD925 /* BaseController.swift */; };
27 | D2144AD120422ED5005CD925 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AD020422ED5005CD925 /* RecipeDetailView.swift */; };
28 | D2144AD32042F218005CD925 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AD22042F218005CD925 /* UIView+Extensions.swift */; };
29 | D2144ADA2042FA12005CD925 /* SearchComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144AD92042FA12005CD925 /* SearchComponent.swift */; };
30 | D2144ADD2042FD1E005CD925 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2144ADC2042FD1E005CD925 /* HomeViewController.swift */; };
31 | D21D3DC82041C22F00EF97BC /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21D3DC72041C22F00EF97BC /* NetworkService.swift */; };
32 | D21D3DCA2041CD4600EF97BC /* RecipesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21D3DC92041CD4600EF97BC /* RecipesService.swift */; };
33 | D21D3DCC2041D59B00EF97BC /* CacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21D3DCB2041D59B00EF97BC /* CacheService.swift */; };
34 | D21D3DCE2041D5C200EF97BC /* ImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21D3DCD2041D5C200EF97BC /* ImageService.swift */; };
35 | D21D3DD02041D5DC00EF97BC /* UIImageView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21D3DCF2041D5DC00EF97BC /* UIImageView+Extensions.swift */; };
36 | D2261FC2204163AE0072956E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2261FBC204163AE0072956E /* Assets.xcassets */; };
37 | D2261FC3204163AE0072956E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2261FBE204163AE0072956E /* LaunchScreen.storyboard */; };
38 | D2261FCB204171690072956E /* Recipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2261FCA204171690072956E /* Recipe.swift */; };
39 | D28E1F83EB9D0A42FEAE58E6 /* Pods_Recipes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D0989C260791A5450C452E1 /* Pods_Recipes.framework */; };
40 | D29C8B752041622A00F6DFC0 /* RecipesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C8B742041622A00F6DFC0 /* RecipesUITests.swift */; };
41 | D2D14552204186B600B31280 /* RecipeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D14551204186B600B31280 /* RecipeTests.swift */; };
42 | D2D5ED3D2043140200BF46BC /* R.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED3C2043140200BF46BC /* R.generated.swift */; };
43 | D2D5ED3F2043178800BF46BC /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED3E2043178800BF46BC /* EmptyView.swift */; };
44 | D2D5ED4220431B7B00BF46BC /* CacheServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED4120431B7B00BF46BC /* CacheServiceTests.swift */; };
45 | D2D5ED4820431FB900BF46BC /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED4720431FB900BF46BC /* Debouncer.swift */; };
46 | D2D5ED4B2043201F00BF46BC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED4A2043201F00BF46BC /* DebouncerTests.swift */; };
47 | D2D5ED4E2043234000BF46BC /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED4D2043234000BF46BC /* Networking.swift */; };
48 | D2D5ED502043236E00BF46BC /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED4F2043236E00BF46BC /* Resource.swift */; };
49 | D2D5ED522043241C00BF46BC /* RecipesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED512043241C00BF46BC /* RecipesService.swift */; };
50 | D2D5ED562043244900BF46BC /* recipes.json in Resources */ = {isa = PBXBuildFile; fileRef = D2D5ED552043244900BF46BC /* recipes.json */; };
51 | D2D5ED582043245E00BF46BC /* singleRecipe.json in Resources */ = {isa = PBXBuildFile; fileRef = D2D5ED572043245E00BF46BC /* singleRecipe.json */; };
52 | D2D5ED5A2043246700BF46BC /* MockNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D5ED592043246700BF46BC /* MockNetworkService.swift */; };
53 | /* End PBXBuildFile section */
54 |
55 | /* Begin PBXContainerItemProxy section */
56 | D29C8B662041622900F6DFC0 /* PBXContainerItemProxy */ = {
57 | isa = PBXContainerItemProxy;
58 | containerPortal = D29C8B492041622900F6DFC0 /* Project object */;
59 | proxyType = 1;
60 | remoteGlobalIDString = D29C8B502041622900F6DFC0;
61 | remoteInfo = Recipes;
62 | };
63 | D29C8B712041622A00F6DFC0 /* PBXContainerItemProxy */ = {
64 | isa = PBXContainerItemProxy;
65 | containerPortal = D29C8B492041622900F6DFC0 /* Project object */;
66 | proxyType = 1;
67 | remoteGlobalIDString = D29C8B502041622900F6DFC0;
68 | remoteInfo = Recipes;
69 | };
70 | /* End PBXContainerItemProxy section */
71 |
72 | /* Begin PBXFileReference section */
73 | 0B7FB436DB3ED6792BB13CF2 /* Pods-Recipes.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Recipes.release.xcconfig"; path = "Pods/Target Support Files/Pods-Recipes/Pods-Recipes.release.xcconfig"; sourceTree = ""; };
74 | 1D0989C260791A5450C452E1 /* Pods_Recipes.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Recipes.framework; sourceTree = BUILT_PRODUCTS_DIR; };
75 | 3E29D2FB9B154AD6EA94130B /* Pods-RecipesUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RecipesUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RecipesUITests/Pods-RecipesUITests.debug.xcconfig"; sourceTree = ""; };
76 | 55CB896310501C0C274522CF /* Pods-RecipesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RecipesTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RecipesTests/Pods-RecipesTests.debug.xcconfig"; sourceTree = ""; };
77 | 67AB69A190C809D9EB64DDAE /* Pods-Recipes.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Recipes.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Recipes/Pods-Recipes.debug.xcconfig"; sourceTree = ""; };
78 | 79900A81992AB62795E12ACA /* Pods-RecipesUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RecipesUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RecipesUITests/Pods-RecipesUITests.release.xcconfig"; sourceTree = ""; };
79 | A63E1C4979821F120A7D2DFA /* Pods-RecipesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RecipesTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RecipesTests/Pods-RecipesTests.release.xcconfig"; sourceTree = ""; };
80 | C603ABB2DDC033874DEEFEB5 /* Pods_RecipesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RecipesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
81 | CAFF210BA0C48F271B66FF16 /* Pods_RecipesUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RecipesUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
82 | D2144AA1204212DD005CD925 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; };
83 | D2144AA320421328005CD925 /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = ""; };
84 | D2144AA92042146D005CD925 /* RecipeDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailViewController.swift; sourceTree = ""; };
85 | D2144AAC204214D4005CD925 /* RecipeListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeListViewController.swift; sourceTree = ""; };
86 | D2144AB2204216CB005CD925 /* AppFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlowController.swift; sourceTree = ""; };
87 | D2144AB4204216FC005CD925 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; };
88 | D2144AB62042173F005CD925 /* RecipeFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeFlowController.swift; sourceTree = ""; };
89 | D2144ABD20421C16005CD925 /* Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = ""; };
90 | D2144ABF20421E4C005CD925 /* RecipeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCell.swift; sourceTree = ""; };
91 | D2144AC32042267A005CD925 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
92 | D2144AC520422686005CD925 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; };
93 | D2144AC7204226F6005CD925 /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = ""; };
94 | D2144AC920422E19005CD925 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; };
95 | D2144ACC20422EA5005CD925 /* ScrollableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableView.swift; sourceTree = ""; };
96 | D2144ACE20422EAE005CD925 /* BaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseController.swift; sourceTree = ""; };
97 | D2144AD020422ED5005CD925 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = ""; };
98 | D2144AD22042F218005CD925 /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; };
99 | D2144AD92042FA12005CD925 /* SearchComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchComponent.swift; sourceTree = ""; };
100 | D2144ADC2042FD1E005CD925 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; };
101 | D21D3DC72041C22F00EF97BC /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; };
102 | D21D3DC92041CD4600EF97BC /* RecipesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipesService.swift; sourceTree = ""; };
103 | D21D3DCB2041D59B00EF97BC /* CacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheService.swift; sourceTree = ""; };
104 | D21D3DCD2041D5C200EF97BC /* ImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageService.swift; sourceTree = ""; };
105 | D21D3DCF2041D5DC00EF97BC /* UIImageView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extensions.swift"; sourceTree = ""; };
106 | D2261FAD204162D50072956E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
107 | D2261FBC204163AE0072956E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
108 | D2261FBF204163AE0072956E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = LaunchScreen.storyboard; sourceTree = ""; };
109 | D2261FCA204171690072956E /* Recipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recipe.swift; sourceTree = ""; };
110 | D29C8B512041622900F6DFC0 /* Recipes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Recipes.app; sourceTree = BUILT_PRODUCTS_DIR; };
111 | D29C8B652041622900F6DFC0 /* RecipesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RecipesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
112 | D29C8B6B2041622A00F6DFC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
113 | D29C8B702041622A00F6DFC0 /* RecipesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RecipesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
114 | D29C8B742041622A00F6DFC0 /* RecipesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipesUITests.swift; sourceTree = ""; };
115 | D29C8B762041622A00F6DFC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
116 | D2D14551204186B600B31280 /* RecipeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeTests.swift; sourceTree = ""; };
117 | D2D5ED3C2043140200BF46BC /* R.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R.generated.swift; sourceTree = ""; };
118 | D2D5ED3E2043178800BF46BC /* EmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; };
119 | D2D5ED4120431B7B00BF46BC /* CacheServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheServiceTests.swift; sourceTree = ""; };
120 | D2D5ED4720431FB900BF46BC /* Debouncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; };
121 | D2D5ED4A2043201F00BF46BC /* DebouncerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; };
122 | D2D5ED4D2043234000BF46BC /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; };
123 | D2D5ED4F2043236E00BF46BC /* Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; };
124 | D2D5ED512043241C00BF46BC /* RecipesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipesService.swift; sourceTree = ""; };
125 | D2D5ED552043244900BF46BC /* recipes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = recipes.json; sourceTree = ""; };
126 | D2D5ED572043245E00BF46BC /* singleRecipe.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = singleRecipe.json; sourceTree = ""; };
127 | D2D5ED592043246700BF46BC /* MockNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkService.swift; sourceTree = ""; };
128 | /* End PBXFileReference section */
129 |
130 | /* Begin PBXFrameworksBuildPhase section */
131 | D29C8B4E2041622900F6DFC0 /* Frameworks */ = {
132 | isa = PBXFrameworksBuildPhase;
133 | buildActionMask = 2147483647;
134 | files = (
135 | D28E1F83EB9D0A42FEAE58E6 /* Pods_Recipes.framework in Frameworks */,
136 | );
137 | runOnlyForDeploymentPostprocessing = 0;
138 | };
139 | D29C8B622041622900F6DFC0 /* Frameworks */ = {
140 | isa = PBXFrameworksBuildPhase;
141 | buildActionMask = 2147483647;
142 | files = (
143 | 456A010A541FE67397DD3F07 /* Pods_RecipesTests.framework in Frameworks */,
144 | );
145 | runOnlyForDeploymentPostprocessing = 0;
146 | };
147 | D29C8B6D2041622A00F6DFC0 /* Frameworks */ = {
148 | isa = PBXFrameworksBuildPhase;
149 | buildActionMask = 2147483647;
150 | files = (
151 | 2DB1405B29CF577A4C5AC6A0 /* Pods_RecipesUITests.framework in Frameworks */,
152 | );
153 | runOnlyForDeploymentPostprocessing = 0;
154 | };
155 | /* End PBXFrameworksBuildPhase section */
156 |
157 | /* Begin PBXGroup section */
158 | 279AB6ABA3DA7E2475179DA4 /* Frameworks */ = {
159 | isa = PBXGroup;
160 | children = (
161 | 1D0989C260791A5450C452E1 /* Pods_Recipes.framework */,
162 | C603ABB2DDC033874DEEFEB5 /* Pods_RecipesTests.framework */,
163 | CAFF210BA0C48F271B66FF16 /* Pods_RecipesUITests.framework */,
164 | );
165 | name = Frameworks;
166 | sourceTree = "";
167 | };
168 | 96E44F5BA420117C22B3E0EE /* Pods */ = {
169 | isa = PBXGroup;
170 | children = (
171 | 67AB69A190C809D9EB64DDAE /* Pods-Recipes.debug.xcconfig */,
172 | 0B7FB436DB3ED6792BB13CF2 /* Pods-Recipes.release.xcconfig */,
173 | 55CB896310501C0C274522CF /* Pods-RecipesTests.debug.xcconfig */,
174 | A63E1C4979821F120A7D2DFA /* Pods-RecipesTests.release.xcconfig */,
175 | 3E29D2FB9B154AD6EA94130B /* Pods-RecipesUITests.debug.xcconfig */,
176 | 79900A81992AB62795E12ACA /* Pods-RecipesUITests.release.xcconfig */,
177 | );
178 | name = Pods;
179 | sourceTree = "";
180 | };
181 | D2144AA62042138D005CD925 /* Detail */ = {
182 | isa = PBXGroup;
183 | children = (
184 | D2144AA92042146D005CD925 /* RecipeDetailViewController.swift */,
185 | D2144AD020422ED5005CD925 /* RecipeDetailView.swift */,
186 | );
187 | path = Detail;
188 | sourceTree = "";
189 | };
190 | D2144AAB204214D4005CD925 /* List */ = {
191 | isa = PBXGroup;
192 | children = (
193 | D2144AAC204214D4005CD925 /* RecipeListViewController.swift */,
194 | D2144ABF20421E4C005CD925 /* RecipeCell.swift */,
195 | );
196 | path = List;
197 | sourceTree = "";
198 | };
199 | D2144AB0204216C4005CD925 /* Flow */ = {
200 | isa = PBXGroup;
201 | children = (
202 | D2144AB2204216CB005CD925 /* AppFlowController.swift */,
203 | D2144AB62042173F005CD925 /* RecipeFlowController.swift */,
204 | );
205 | path = Flow;
206 | sourceTree = "";
207 | };
208 | D2144ABC20421C0F005CD925 /* Adapter */ = {
209 | isa = PBXGroup;
210 | children = (
211 | D2144ABD20421C16005CD925 /* Adapter.swift */,
212 | );
213 | path = Adapter;
214 | sourceTree = "";
215 | };
216 | D2144AC120421FD9005CD925 /* Base */ = {
217 | isa = PBXGroup;
218 | children = (
219 | D2144ACE20422EAE005CD925 /* BaseController.swift */,
220 | );
221 | path = Base;
222 | sourceTree = "";
223 | };
224 | D2144AC22042267A005CD925 /* App */ = {
225 | isa = PBXGroup;
226 | children = (
227 | D2144AC32042267A005CD925 /* AppDelegate.swift */,
228 | D2144AC520422686005CD925 /* AppConfig.swift */,
229 | D2144AC920422E19005CD925 /* Color.swift */,
230 | );
231 | path = App;
232 | sourceTree = "";
233 | };
234 | D2144ACB20422E8F005CD925 /* View */ = {
235 | isa = PBXGroup;
236 | children = (
237 | D2144ACC20422EA5005CD925 /* ScrollableView.swift */,
238 | D2D5ED3E2043178800BF46BC /* EmptyView.swift */,
239 | );
240 | path = View;
241 | sourceTree = "";
242 | };
243 | D2144AD82042F9E1005CD925 /* Search */ = {
244 | isa = PBXGroup;
245 | children = (
246 | D2144AD92042FA12005CD925 /* SearchComponent.swift */,
247 | );
248 | path = Search;
249 | sourceTree = "";
250 | };
251 | D2144ADB2042FD17005CD925 /* Home */ = {
252 | isa = PBXGroup;
253 | children = (
254 | D2144ADC2042FD1E005CD925 /* HomeViewController.swift */,
255 | );
256 | path = Home;
257 | sourceTree = "";
258 | };
259 | D2261FAC204162D50072956E /* Resource */ = {
260 | isa = PBXGroup;
261 | children = (
262 | D2261FBC204163AE0072956E /* Assets.xcassets */,
263 | D2261FBD204163AE0072956E /* Base.lproj */,
264 | D2261FAD204162D50072956E /* Info.plist */,
265 | );
266 | path = Resource;
267 | sourceTree = "";
268 | };
269 | D2261FAE204162D50072956E /* Source */ = {
270 | isa = PBXGroup;
271 | children = (
272 | D2261FC920416C7B0072956E /* Feature */,
273 | D2261FC520416C7B0072956E /* Library */,
274 | );
275 | path = Source;
276 | sourceTree = "";
277 | };
278 | D2261FBD204163AE0072956E /* Base.lproj */ = {
279 | isa = PBXGroup;
280 | children = (
281 | D2261FBE204163AE0072956E /* LaunchScreen.storyboard */,
282 | );
283 | path = Base.lproj;
284 | sourceTree = "";
285 | };
286 | D2261FC520416C7B0072956E /* Library */ = {
287 | isa = PBXGroup;
288 | children = (
289 | D2D5ED4C2043233600BF46BC /* Networking */,
290 | D2D5ED4620431FB900BF46BC /* Utils */,
291 | D2D5ED3B2043140200BF46BC /* Constants */,
292 | D2144ACB20422E8F005CD925 /* View */,
293 | D2144AC22042267A005CD925 /* App */,
294 | D2144AC120421FD9005CD925 /* Base */,
295 | D2144ABC20421C0F005CD925 /* Adapter */,
296 | D2144AB0204216C4005CD925 /* Flow */,
297 | D2261FC620416C7B0072956E /* Extensions */,
298 | D2261FC720416C7B0072956E /* Model */,
299 | D2261FC820416C7B0072956E /* Service */,
300 | );
301 | path = Library;
302 | sourceTree = "";
303 | };
304 | D2261FC620416C7B0072956E /* Extensions */ = {
305 | isa = PBXGroup;
306 | children = (
307 | D21D3DCF2041D5DC00EF97BC /* UIImageView+Extensions.swift */,
308 | D2144AA1204212DD005CD925 /* UIColor+Extensions.swift */,
309 | D2144AA320421328005CD925 /* NSLayoutConstraint+Extensions.swift */,
310 | D2144AB4204216FC005CD925 /* UIViewController+Extensions.swift */,
311 | D2144AC7204226F6005CD925 /* UICollectionView+Extensions.swift */,
312 | D2144AD22042F218005CD925 /* UIView+Extensions.swift */,
313 | );
314 | path = Extensions;
315 | sourceTree = "";
316 | };
317 | D2261FC720416C7B0072956E /* Model */ = {
318 | isa = PBXGroup;
319 | children = (
320 | D2261FCA204171690072956E /* Recipe.swift */,
321 | );
322 | path = Model;
323 | sourceTree = "";
324 | };
325 | D2261FC820416C7B0072956E /* Service */ = {
326 | isa = PBXGroup;
327 | children = (
328 | D21D3DC72041C22F00EF97BC /* NetworkService.swift */,
329 | D21D3DC92041CD4600EF97BC /* RecipesService.swift */,
330 | D21D3DCB2041D59B00EF97BC /* CacheService.swift */,
331 | D21D3DCD2041D5C200EF97BC /* ImageService.swift */,
332 | );
333 | path = Service;
334 | sourceTree = "";
335 | };
336 | D2261FC920416C7B0072956E /* Feature */ = {
337 | isa = PBXGroup;
338 | children = (
339 | D2144ADB2042FD17005CD925 /* Home */,
340 | D2144AD82042F9E1005CD925 /* Search */,
341 | D2144AAB204214D4005CD925 /* List */,
342 | D2144AA62042138D005CD925 /* Detail */,
343 | );
344 | path = Feature;
345 | sourceTree = "";
346 | };
347 | D29C8B482041622900F6DFC0 = {
348 | isa = PBXGroup;
349 | children = (
350 | D2261FAC204162D50072956E /* Resource */,
351 | D2261FAE204162D50072956E /* Source */,
352 | D29C8B682041622900F6DFC0 /* RecipesTests */,
353 | D29C8B732041622A00F6DFC0 /* RecipesUITests */,
354 | D29C8B522041622900F6DFC0 /* Products */,
355 | 96E44F5BA420117C22B3E0EE /* Pods */,
356 | 279AB6ABA3DA7E2475179DA4 /* Frameworks */,
357 | );
358 | sourceTree = "";
359 | };
360 | D29C8B522041622900F6DFC0 /* Products */ = {
361 | isa = PBXGroup;
362 | children = (
363 | D29C8B512041622900F6DFC0 /* Recipes.app */,
364 | D29C8B652041622900F6DFC0 /* RecipesTests.xctest */,
365 | D29C8B702041622A00F6DFC0 /* RecipesUITests.xctest */,
366 | );
367 | name = Products;
368 | sourceTree = "";
369 | };
370 | D29C8B682041622900F6DFC0 /* RecipesTests */ = {
371 | isa = PBXGroup;
372 | children = (
373 | D2D5ED542043243D00BF46BC /* Fixture */,
374 | D2D5ED532043243D00BF46BC /* Mock */,
375 | D2D1454F204186AB00B31280 /* Library */,
376 | D29C8B6B2041622A00F6DFC0 /* Info.plist */,
377 | );
378 | path = RecipesTests;
379 | sourceTree = "";
380 | };
381 | D29C8B732041622A00F6DFC0 /* RecipesUITests */ = {
382 | isa = PBXGroup;
383 | children = (
384 | D29C8B742041622A00F6DFC0 /* RecipesUITests.swift */,
385 | D29C8B762041622A00F6DFC0 /* Info.plist */,
386 | );
387 | path = RecipesUITests;
388 | sourceTree = "";
389 | };
390 | D2D1454F204186AB00B31280 /* Library */ = {
391 | isa = PBXGroup;
392 | children = (
393 | D2D5ED492043201F00BF46BC /* Utils */,
394 | D2D5ED4020431A5A00BF46BC /* Service */,
395 | D2D14550204186AB00B31280 /* Model */,
396 | );
397 | path = Library;
398 | sourceTree = "";
399 | };
400 | D2D14550204186AB00B31280 /* Model */ = {
401 | isa = PBXGroup;
402 | children = (
403 | D2D14551204186B600B31280 /* RecipeTests.swift */,
404 | );
405 | path = Model;
406 | sourceTree = "";
407 | };
408 | D2D5ED3B2043140200BF46BC /* Constants */ = {
409 | isa = PBXGroup;
410 | children = (
411 | D2D5ED3C2043140200BF46BC /* R.generated.swift */,
412 | );
413 | path = Constants;
414 | sourceTree = "";
415 | };
416 | D2D5ED4020431A5A00BF46BC /* Service */ = {
417 | isa = PBXGroup;
418 | children = (
419 | D2D5ED4120431B7B00BF46BC /* CacheServiceTests.swift */,
420 | D2D5ED512043241C00BF46BC /* RecipesService.swift */,
421 | );
422 | path = Service;
423 | sourceTree = "";
424 | };
425 | D2D5ED4620431FB900BF46BC /* Utils */ = {
426 | isa = PBXGroup;
427 | children = (
428 | D2D5ED4720431FB900BF46BC /* Debouncer.swift */,
429 | );
430 | path = Utils;
431 | sourceTree = "";
432 | };
433 | D2D5ED492043201F00BF46BC /* Utils */ = {
434 | isa = PBXGroup;
435 | children = (
436 | D2D5ED4A2043201F00BF46BC /* DebouncerTests.swift */,
437 | );
438 | path = Utils;
439 | sourceTree = "";
440 | };
441 | D2D5ED4C2043233600BF46BC /* Networking */ = {
442 | isa = PBXGroup;
443 | children = (
444 | D2D5ED4D2043234000BF46BC /* Networking.swift */,
445 | D2D5ED4F2043236E00BF46BC /* Resource.swift */,
446 | );
447 | path = Networking;
448 | sourceTree = "";
449 | };
450 | D2D5ED532043243D00BF46BC /* Mock */ = {
451 | isa = PBXGroup;
452 | children = (
453 | D2D5ED592043246700BF46BC /* MockNetworkService.swift */,
454 | );
455 | path = Mock;
456 | sourceTree = "";
457 | };
458 | D2D5ED542043243D00BF46BC /* Fixture */ = {
459 | isa = PBXGroup;
460 | children = (
461 | D2D5ED552043244900BF46BC /* recipes.json */,
462 | D2D5ED572043245E00BF46BC /* singleRecipe.json */,
463 | );
464 | path = Fixture;
465 | sourceTree = "";
466 | };
467 | /* End PBXGroup section */
468 |
469 | /* Begin PBXNativeTarget section */
470 | D29C8B502041622900F6DFC0 /* Recipes */ = {
471 | isa = PBXNativeTarget;
472 | buildConfigurationList = D29C8B792041622A00F6DFC0 /* Build configuration list for PBXNativeTarget "Recipes" */;
473 | buildPhases = (
474 | 3ACF734DB59FDA177E1AF598 /* [CP] Check Pods Manifest.lock */,
475 | D2D5ED3A204313D800BF46BC /* R.swift */,
476 | D29C8B4D2041622900F6DFC0 /* Sources */,
477 | D29C8B4E2041622900F6DFC0 /* Frameworks */,
478 | D29C8B4F2041622900F6DFC0 /* Resources */,
479 | D2D1454E204182F600B31280 /* SwiftLint */,
480 | D3627BD33A06E5114D95C50A /* [CP] Embed Pods Frameworks */,
481 | 1AC5414FD7DC6417186C42A8 /* [CP] Copy Pods Resources */,
482 | );
483 | buildRules = (
484 | );
485 | dependencies = (
486 | );
487 | name = Recipes;
488 | productName = Recipes;
489 | productReference = D29C8B512041622900F6DFC0 /* Recipes.app */;
490 | productType = "com.apple.product-type.application";
491 | };
492 | D29C8B642041622900F6DFC0 /* RecipesTests */ = {
493 | isa = PBXNativeTarget;
494 | buildConfigurationList = D29C8B7C2041622A00F6DFC0 /* Build configuration list for PBXNativeTarget "RecipesTests" */;
495 | buildPhases = (
496 | 6FF0F054028C8515D88FBDBF /* [CP] Check Pods Manifest.lock */,
497 | D29C8B612041622900F6DFC0 /* Sources */,
498 | D29C8B622041622900F6DFC0 /* Frameworks */,
499 | D29C8B632041622900F6DFC0 /* Resources */,
500 | 6A71297EDC420C872BE9B4AD /* [CP] Embed Pods Frameworks */,
501 | 20108EEAAE22BECAFC47B11B /* [CP] Copy Pods Resources */,
502 | );
503 | buildRules = (
504 | );
505 | dependencies = (
506 | D29C8B672041622900F6DFC0 /* PBXTargetDependency */,
507 | );
508 | name = RecipesTests;
509 | productName = RecipesTests;
510 | productReference = D29C8B652041622900F6DFC0 /* RecipesTests.xctest */;
511 | productType = "com.apple.product-type.bundle.unit-test";
512 | };
513 | D29C8B6F2041622A00F6DFC0 /* RecipesUITests */ = {
514 | isa = PBXNativeTarget;
515 | buildConfigurationList = D29C8B7F2041622A00F6DFC0 /* Build configuration list for PBXNativeTarget "RecipesUITests" */;
516 | buildPhases = (
517 | 62602227C717698D72AFCCDF /* [CP] Check Pods Manifest.lock */,
518 | D29C8B6C2041622A00F6DFC0 /* Sources */,
519 | D29C8B6D2041622A00F6DFC0 /* Frameworks */,
520 | D29C8B6E2041622A00F6DFC0 /* Resources */,
521 | 94068D3026872CC2B8F89FC2 /* [CP] Embed Pods Frameworks */,
522 | 15D757B398228501FC0D4448 /* [CP] Copy Pods Resources */,
523 | );
524 | buildRules = (
525 | );
526 | dependencies = (
527 | D29C8B722041622A00F6DFC0 /* PBXTargetDependency */,
528 | );
529 | name = RecipesUITests;
530 | productName = RecipesUITests;
531 | productReference = D29C8B702041622A00F6DFC0 /* RecipesUITests.xctest */;
532 | productType = "com.apple.product-type.bundle.ui-testing";
533 | };
534 | /* End PBXNativeTarget section */
535 |
536 | /* Begin PBXProject section */
537 | D29C8B492041622900F6DFC0 /* Project object */ = {
538 | isa = PBXProject;
539 | attributes = {
540 | LastSwiftUpdateCheck = 0920;
541 | LastUpgradeCheck = 0920;
542 | ORGANIZATIONNAME = "Khoa Pham";
543 | TargetAttributes = {
544 | D29C8B502041622900F6DFC0 = {
545 | CreatedOnToolsVersion = 9.2;
546 | ProvisioningStyle = Automatic;
547 | };
548 | D29C8B642041622900F6DFC0 = {
549 | CreatedOnToolsVersion = 9.2;
550 | ProvisioningStyle = Automatic;
551 | TestTargetID = D29C8B502041622900F6DFC0;
552 | };
553 | D29C8B6F2041622A00F6DFC0 = {
554 | CreatedOnToolsVersion = 9.2;
555 | ProvisioningStyle = Automatic;
556 | TestTargetID = D29C8B502041622900F6DFC0;
557 | };
558 | };
559 | };
560 | buildConfigurationList = D29C8B4C2041622900F6DFC0 /* Build configuration list for PBXProject "Recipes" */;
561 | compatibilityVersion = "Xcode 8.0";
562 | developmentRegion = en;
563 | hasScannedForEncodings = 0;
564 | knownRegions = (
565 | en,
566 | Base,
567 | );
568 | mainGroup = D29C8B482041622900F6DFC0;
569 | productRefGroup = D29C8B522041622900F6DFC0 /* Products */;
570 | projectDirPath = "";
571 | projectRoot = "";
572 | targets = (
573 | D29C8B502041622900F6DFC0 /* Recipes */,
574 | D29C8B642041622900F6DFC0 /* RecipesTests */,
575 | D29C8B6F2041622A00F6DFC0 /* RecipesUITests */,
576 | );
577 | };
578 | /* End PBXProject section */
579 |
580 | /* Begin PBXResourcesBuildPhase section */
581 | D29C8B4F2041622900F6DFC0 /* Resources */ = {
582 | isa = PBXResourcesBuildPhase;
583 | buildActionMask = 2147483647;
584 | files = (
585 | D2261FC2204163AE0072956E /* Assets.xcassets in Resources */,
586 | D2261FC3204163AE0072956E /* LaunchScreen.storyboard in Resources */,
587 | );
588 | runOnlyForDeploymentPostprocessing = 0;
589 | };
590 | D29C8B632041622900F6DFC0 /* Resources */ = {
591 | isa = PBXResourcesBuildPhase;
592 | buildActionMask = 2147483647;
593 | files = (
594 | D2D5ED562043244900BF46BC /* recipes.json in Resources */,
595 | D2D5ED582043245E00BF46BC /* singleRecipe.json in Resources */,
596 | );
597 | runOnlyForDeploymentPostprocessing = 0;
598 | };
599 | D29C8B6E2041622A00F6DFC0 /* Resources */ = {
600 | isa = PBXResourcesBuildPhase;
601 | buildActionMask = 2147483647;
602 | files = (
603 | );
604 | runOnlyForDeploymentPostprocessing = 0;
605 | };
606 | /* End PBXResourcesBuildPhase section */
607 |
608 | /* Begin PBXShellScriptBuildPhase section */
609 | 15D757B398228501FC0D4448 /* [CP] Copy Pods Resources */ = {
610 | isa = PBXShellScriptBuildPhase;
611 | buildActionMask = 2147483647;
612 | files = (
613 | );
614 | inputPaths = (
615 | );
616 | name = "[CP] Copy Pods Resources";
617 | outputPaths = (
618 | );
619 | runOnlyForDeploymentPostprocessing = 0;
620 | shellPath = /bin/sh;
621 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RecipesUITests/Pods-RecipesUITests-resources.sh\"\n";
622 | showEnvVarsInLog = 0;
623 | };
624 | 1AC5414FD7DC6417186C42A8 /* [CP] Copy Pods Resources */ = {
625 | isa = PBXShellScriptBuildPhase;
626 | buildActionMask = 2147483647;
627 | files = (
628 | );
629 | inputPaths = (
630 | );
631 | name = "[CP] Copy Pods Resources";
632 | outputPaths = (
633 | );
634 | runOnlyForDeploymentPostprocessing = 0;
635 | shellPath = /bin/sh;
636 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Recipes/Pods-Recipes-resources.sh\"\n";
637 | showEnvVarsInLog = 0;
638 | };
639 | 20108EEAAE22BECAFC47B11B /* [CP] Copy Pods Resources */ = {
640 | isa = PBXShellScriptBuildPhase;
641 | buildActionMask = 2147483647;
642 | files = (
643 | );
644 | inputPaths = (
645 | );
646 | name = "[CP] Copy Pods Resources";
647 | outputPaths = (
648 | );
649 | runOnlyForDeploymentPostprocessing = 0;
650 | shellPath = /bin/sh;
651 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RecipesTests/Pods-RecipesTests-resources.sh\"\n";
652 | showEnvVarsInLog = 0;
653 | };
654 | 3ACF734DB59FDA177E1AF598 /* [CP] Check Pods Manifest.lock */ = {
655 | isa = PBXShellScriptBuildPhase;
656 | buildActionMask = 2147483647;
657 | files = (
658 | );
659 | inputPaths = (
660 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
661 | "${PODS_ROOT}/Manifest.lock",
662 | );
663 | name = "[CP] Check Pods Manifest.lock";
664 | outputPaths = (
665 | "$(DERIVED_FILE_DIR)/Pods-Recipes-checkManifestLockResult.txt",
666 | );
667 | runOnlyForDeploymentPostprocessing = 0;
668 | shellPath = /bin/sh;
669 | 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";
670 | showEnvVarsInLog = 0;
671 | };
672 | 62602227C717698D72AFCCDF /* [CP] Check Pods Manifest.lock */ = {
673 | isa = PBXShellScriptBuildPhase;
674 | buildActionMask = 2147483647;
675 | files = (
676 | );
677 | inputPaths = (
678 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
679 | "${PODS_ROOT}/Manifest.lock",
680 | );
681 | name = "[CP] Check Pods Manifest.lock";
682 | outputPaths = (
683 | "$(DERIVED_FILE_DIR)/Pods-RecipesUITests-checkManifestLockResult.txt",
684 | );
685 | runOnlyForDeploymentPostprocessing = 0;
686 | shellPath = /bin/sh;
687 | 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";
688 | showEnvVarsInLog = 0;
689 | };
690 | 6A71297EDC420C872BE9B4AD /* [CP] Embed Pods Frameworks */ = {
691 | isa = PBXShellScriptBuildPhase;
692 | buildActionMask = 2147483647;
693 | files = (
694 | );
695 | inputPaths = (
696 | );
697 | name = "[CP] Embed Pods Frameworks";
698 | outputPaths = (
699 | );
700 | runOnlyForDeploymentPostprocessing = 0;
701 | shellPath = /bin/sh;
702 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RecipesTests/Pods-RecipesTests-frameworks.sh\"\n";
703 | showEnvVarsInLog = 0;
704 | };
705 | 6FF0F054028C8515D88FBDBF /* [CP] Check Pods Manifest.lock */ = {
706 | isa = PBXShellScriptBuildPhase;
707 | buildActionMask = 2147483647;
708 | files = (
709 | );
710 | inputPaths = (
711 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
712 | "${PODS_ROOT}/Manifest.lock",
713 | );
714 | name = "[CP] Check Pods Manifest.lock";
715 | outputPaths = (
716 | "$(DERIVED_FILE_DIR)/Pods-RecipesTests-checkManifestLockResult.txt",
717 | );
718 | runOnlyForDeploymentPostprocessing = 0;
719 | shellPath = /bin/sh;
720 | 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";
721 | showEnvVarsInLog = 0;
722 | };
723 | 94068D3026872CC2B8F89FC2 /* [CP] Embed Pods Frameworks */ = {
724 | isa = PBXShellScriptBuildPhase;
725 | buildActionMask = 2147483647;
726 | files = (
727 | );
728 | inputPaths = (
729 | );
730 | name = "[CP] Embed Pods Frameworks";
731 | outputPaths = (
732 | );
733 | runOnlyForDeploymentPostprocessing = 0;
734 | shellPath = /bin/sh;
735 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RecipesUITests/Pods-RecipesUITests-frameworks.sh\"\n";
736 | showEnvVarsInLog = 0;
737 | };
738 | D2D1454E204182F600B31280 /* SwiftLint */ = {
739 | isa = PBXShellScriptBuildPhase;
740 | buildActionMask = 2147483647;
741 | files = (
742 | );
743 | inputPaths = (
744 | );
745 | name = SwiftLint;
746 | outputPaths = (
747 | );
748 | runOnlyForDeploymentPostprocessing = 0;
749 | shellPath = /bin/sh;
750 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
751 | };
752 | D2D5ED3A204313D800BF46BC /* R.swift */ = {
753 | isa = PBXShellScriptBuildPhase;
754 | buildActionMask = 2147483647;
755 | files = (
756 | );
757 | inputPaths = (
758 | );
759 | name = R.swift;
760 | outputPaths = (
761 | );
762 | runOnlyForDeploymentPostprocessing = 0;
763 | shellPath = /bin/sh;
764 | shellScript = "\"$PODS_ROOT/R.swift/rswift\" generate \"$SRCROOT/Source/Library/Constants\"";
765 | };
766 | D3627BD33A06E5114D95C50A /* [CP] Embed Pods Frameworks */ = {
767 | isa = PBXShellScriptBuildPhase;
768 | buildActionMask = 2147483647;
769 | files = (
770 | );
771 | inputPaths = (
772 | "${SRCROOT}/Pods/Target Support Files/Pods-Recipes/Pods-Recipes-frameworks.sh",
773 | "${BUILT_PRODUCTS_DIR}/R.swift.Library/Rswift.framework",
774 | "${BUILT_PRODUCTS_DIR}/SwiftHash/SwiftHash.framework",
775 | );
776 | name = "[CP] Embed Pods Frameworks";
777 | outputPaths = (
778 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Rswift.framework",
779 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftHash.framework",
780 | );
781 | runOnlyForDeploymentPostprocessing = 0;
782 | shellPath = /bin/sh;
783 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Recipes/Pods-Recipes-frameworks.sh\"\n";
784 | showEnvVarsInLog = 0;
785 | };
786 | /* End PBXShellScriptBuildPhase section */
787 |
788 | /* Begin PBXSourcesBuildPhase section */
789 | D29C8B4D2041622900F6DFC0 /* Sources */ = {
790 | isa = PBXSourcesBuildPhase;
791 | buildActionMask = 2147483647;
792 | files = (
793 | D2144ADD2042FD1E005CD925 /* HomeViewController.swift in Sources */,
794 | D21D3DCA2041CD4600EF97BC /* RecipesService.swift in Sources */,
795 | D2144ADA2042FA12005CD925 /* SearchComponent.swift in Sources */,
796 | D2D5ED502043236E00BF46BC /* Resource.swift in Sources */,
797 | D2144AB5204216FC005CD925 /* UIViewController+Extensions.swift in Sources */,
798 | D2144AC020421E4C005CD925 /* RecipeCell.swift in Sources */,
799 | D2144AB72042173F005CD925 /* RecipeFlowController.swift in Sources */,
800 | D2261FCB204171690072956E /* Recipe.swift in Sources */,
801 | D2144ACD20422EA5005CD925 /* ScrollableView.swift in Sources */,
802 | D2144AB3204216CB005CD925 /* AppFlowController.swift in Sources */,
803 | D2D5ED4820431FB900BF46BC /* Debouncer.swift in Sources */,
804 | D2144AD120422ED5005CD925 /* RecipeDetailView.swift in Sources */,
805 | D2144AAD204214D4005CD925 /* RecipeListViewController.swift in Sources */,
806 | D2144AA420421328005CD925 /* NSLayoutConstraint+Extensions.swift in Sources */,
807 | D2D5ED3F2043178800BF46BC /* EmptyView.swift in Sources */,
808 | D2144AC42042267A005CD925 /* AppDelegate.swift in Sources */,
809 | D21D3DC82041C22F00EF97BC /* NetworkService.swift in Sources */,
810 | D2144AC620422686005CD925 /* AppConfig.swift in Sources */,
811 | D2144AD32042F218005CD925 /* UIView+Extensions.swift in Sources */,
812 | D2144ACA20422E19005CD925 /* Color.swift in Sources */,
813 | D21D3DCC2041D59B00EF97BC /* CacheService.swift in Sources */,
814 | D2144AAA2042146D005CD925 /* RecipeDetailViewController.swift in Sources */,
815 | D2D5ED3D2043140200BF46BC /* R.generated.swift in Sources */,
816 | D21D3DD02041D5DC00EF97BC /* UIImageView+Extensions.swift in Sources */,
817 | D2144ACF20422EAE005CD925 /* BaseController.swift in Sources */,
818 | D2144AC8204226F6005CD925 /* UICollectionView+Extensions.swift in Sources */,
819 | D2144AA2204212DD005CD925 /* UIColor+Extensions.swift in Sources */,
820 | D2D5ED4E2043234000BF46BC /* Networking.swift in Sources */,
821 | D2144ABE20421C16005CD925 /* Adapter.swift in Sources */,
822 | D21D3DCE2041D5C200EF97BC /* ImageService.swift in Sources */,
823 | );
824 | runOnlyForDeploymentPostprocessing = 0;
825 | };
826 | D29C8B612041622900F6DFC0 /* Sources */ = {
827 | isa = PBXSourcesBuildPhase;
828 | buildActionMask = 2147483647;
829 | files = (
830 | D2D5ED4220431B7B00BF46BC /* CacheServiceTests.swift in Sources */,
831 | D2D14552204186B600B31280 /* RecipeTests.swift in Sources */,
832 | D2D5ED5A2043246700BF46BC /* MockNetworkService.swift in Sources */,
833 | D2D5ED4B2043201F00BF46BC /* DebouncerTests.swift in Sources */,
834 | D2D5ED522043241C00BF46BC /* RecipesService.swift in Sources */,
835 | );
836 | runOnlyForDeploymentPostprocessing = 0;
837 | };
838 | D29C8B6C2041622A00F6DFC0 /* Sources */ = {
839 | isa = PBXSourcesBuildPhase;
840 | buildActionMask = 2147483647;
841 | files = (
842 | D29C8B752041622A00F6DFC0 /* RecipesUITests.swift in Sources */,
843 | );
844 | runOnlyForDeploymentPostprocessing = 0;
845 | };
846 | /* End PBXSourcesBuildPhase section */
847 |
848 | /* Begin PBXTargetDependency section */
849 | D29C8B672041622900F6DFC0 /* PBXTargetDependency */ = {
850 | isa = PBXTargetDependency;
851 | target = D29C8B502041622900F6DFC0 /* Recipes */;
852 | targetProxy = D29C8B662041622900F6DFC0 /* PBXContainerItemProxy */;
853 | };
854 | D29C8B722041622A00F6DFC0 /* PBXTargetDependency */ = {
855 | isa = PBXTargetDependency;
856 | target = D29C8B502041622900F6DFC0 /* Recipes */;
857 | targetProxy = D29C8B712041622A00F6DFC0 /* PBXContainerItemProxy */;
858 | };
859 | /* End PBXTargetDependency section */
860 |
861 | /* Begin PBXVariantGroup section */
862 | D2261FBE204163AE0072956E /* LaunchScreen.storyboard */ = {
863 | isa = PBXVariantGroup;
864 | children = (
865 | D2261FBF204163AE0072956E /* Base */,
866 | );
867 | name = LaunchScreen.storyboard;
868 | sourceTree = "";
869 | };
870 | /* End PBXVariantGroup section */
871 |
872 | /* Begin XCBuildConfiguration section */
873 | D29C8B772041622A00F6DFC0 /* Debug */ = {
874 | isa = XCBuildConfiguration;
875 | buildSettings = {
876 | ALWAYS_SEARCH_USER_PATHS = NO;
877 | CLANG_ANALYZER_NONNULL = YES;
878 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
879 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
880 | CLANG_CXX_LIBRARY = "libc++";
881 | CLANG_ENABLE_MODULES = YES;
882 | CLANG_ENABLE_OBJC_ARC = YES;
883 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
884 | CLANG_WARN_BOOL_CONVERSION = YES;
885 | CLANG_WARN_COMMA = YES;
886 | CLANG_WARN_CONSTANT_CONVERSION = YES;
887 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
888 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
889 | CLANG_WARN_EMPTY_BODY = YES;
890 | CLANG_WARN_ENUM_CONVERSION = YES;
891 | CLANG_WARN_INFINITE_RECURSION = YES;
892 | CLANG_WARN_INT_CONVERSION = YES;
893 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
894 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
895 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
896 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
897 | CLANG_WARN_STRICT_PROTOTYPES = YES;
898 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
899 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
900 | CLANG_WARN_UNREACHABLE_CODE = YES;
901 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
902 | CODE_SIGN_IDENTITY = "iPhone Developer";
903 | COPY_PHASE_STRIP = NO;
904 | DEBUG_INFORMATION_FORMAT = dwarf;
905 | ENABLE_STRICT_OBJC_MSGSEND = YES;
906 | ENABLE_TESTABILITY = YES;
907 | GCC_C_LANGUAGE_STANDARD = gnu11;
908 | GCC_DYNAMIC_NO_PIC = NO;
909 | GCC_NO_COMMON_BLOCKS = YES;
910 | GCC_OPTIMIZATION_LEVEL = 0;
911 | GCC_PREPROCESSOR_DEFINITIONS = (
912 | "DEBUG=1",
913 | "$(inherited)",
914 | );
915 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
916 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
917 | GCC_WARN_UNDECLARED_SELECTOR = YES;
918 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
919 | GCC_WARN_UNUSED_FUNCTION = YES;
920 | GCC_WARN_UNUSED_VARIABLE = YES;
921 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
922 | MTL_ENABLE_DEBUG_INFO = YES;
923 | ONLY_ACTIVE_ARCH = YES;
924 | SDKROOT = iphoneos;
925 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
926 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
927 | SWIFT_VERSION = 4.0;
928 | };
929 | name = Debug;
930 | };
931 | D29C8B782041622A00F6DFC0 /* Release */ = {
932 | isa = XCBuildConfiguration;
933 | buildSettings = {
934 | ALWAYS_SEARCH_USER_PATHS = NO;
935 | CLANG_ANALYZER_NONNULL = YES;
936 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
937 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
938 | CLANG_CXX_LIBRARY = "libc++";
939 | CLANG_ENABLE_MODULES = YES;
940 | CLANG_ENABLE_OBJC_ARC = YES;
941 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
942 | CLANG_WARN_BOOL_CONVERSION = YES;
943 | CLANG_WARN_COMMA = YES;
944 | CLANG_WARN_CONSTANT_CONVERSION = YES;
945 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
946 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
947 | CLANG_WARN_EMPTY_BODY = YES;
948 | CLANG_WARN_ENUM_CONVERSION = YES;
949 | CLANG_WARN_INFINITE_RECURSION = YES;
950 | CLANG_WARN_INT_CONVERSION = YES;
951 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
952 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
953 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
954 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
955 | CLANG_WARN_STRICT_PROTOTYPES = YES;
956 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
957 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
958 | CLANG_WARN_UNREACHABLE_CODE = YES;
959 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
960 | CODE_SIGN_IDENTITY = "iPhone Developer";
961 | COPY_PHASE_STRIP = NO;
962 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
963 | ENABLE_NS_ASSERTIONS = NO;
964 | ENABLE_STRICT_OBJC_MSGSEND = YES;
965 | GCC_C_LANGUAGE_STANDARD = gnu11;
966 | GCC_NO_COMMON_BLOCKS = YES;
967 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
968 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
969 | GCC_WARN_UNDECLARED_SELECTOR = YES;
970 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
971 | GCC_WARN_UNUSED_FUNCTION = YES;
972 | GCC_WARN_UNUSED_VARIABLE = YES;
973 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
974 | MTL_ENABLE_DEBUG_INFO = NO;
975 | SDKROOT = iphoneos;
976 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
977 | SWIFT_VERSION = 4.0;
978 | VALIDATE_PRODUCT = YES;
979 | };
980 | name = Release;
981 | };
982 | D29C8B7A2041622A00F6DFC0 /* Debug */ = {
983 | isa = XCBuildConfiguration;
984 | baseConfigurationReference = 67AB69A190C809D9EB64DDAE /* Pods-Recipes.debug.xcconfig */;
985 | buildSettings = {
986 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
987 | CODE_SIGN_STYLE = Automatic;
988 | DEVELOPMENT_TEAM = T78DK947F2;
989 | INFOPLIST_FILE = "$(SRCROOT)/Resource/Info.plist";
990 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
991 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
992 | PRODUCT_BUNDLE_IDENTIFIER = no.onmyway133.Recipes;
993 | PRODUCT_NAME = "$(TARGET_NAME)";
994 | TARGETED_DEVICE_FAMILY = "1,2";
995 | };
996 | name = Debug;
997 | };
998 | D29C8B7B2041622A00F6DFC0 /* Release */ = {
999 | isa = XCBuildConfiguration;
1000 | baseConfigurationReference = 0B7FB436DB3ED6792BB13CF2 /* Pods-Recipes.release.xcconfig */;
1001 | buildSettings = {
1002 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
1003 | CODE_SIGN_STYLE = Automatic;
1004 | DEVELOPMENT_TEAM = T78DK947F2;
1005 | INFOPLIST_FILE = "$(SRCROOT)/Resource/Info.plist";
1006 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
1007 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
1008 | PRODUCT_BUNDLE_IDENTIFIER = no.onmyway133.Recipes;
1009 | PRODUCT_NAME = "$(TARGET_NAME)";
1010 | TARGETED_DEVICE_FAMILY = "1,2";
1011 | };
1012 | name = Release;
1013 | };
1014 | D29C8B7D2041622A00F6DFC0 /* Debug */ = {
1015 | isa = XCBuildConfiguration;
1016 | baseConfigurationReference = 55CB896310501C0C274522CF /* Pods-RecipesTests.debug.xcconfig */;
1017 | buildSettings = {
1018 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
1019 | BUNDLE_LOADER = "$(TEST_HOST)";
1020 | CODE_SIGN_STYLE = Automatic;
1021 | DEVELOPMENT_TEAM = T78DK947F2;
1022 | INFOPLIST_FILE = RecipesTests/Info.plist;
1023 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
1024 | PRODUCT_BUNDLE_IDENTIFIER = no.onmyway133.RecipesTests;
1025 | PRODUCT_NAME = "$(TARGET_NAME)";
1026 | SWIFT_VERSION = 4.0;
1027 | TARGETED_DEVICE_FAMILY = "1,2";
1028 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Recipes.app/Recipes";
1029 | };
1030 | name = Debug;
1031 | };
1032 | D29C8B7E2041622A00F6DFC0 /* Release */ = {
1033 | isa = XCBuildConfiguration;
1034 | baseConfigurationReference = A63E1C4979821F120A7D2DFA /* Pods-RecipesTests.release.xcconfig */;
1035 | buildSettings = {
1036 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
1037 | BUNDLE_LOADER = "$(TEST_HOST)";
1038 | CODE_SIGN_STYLE = Automatic;
1039 | DEVELOPMENT_TEAM = T78DK947F2;
1040 | INFOPLIST_FILE = RecipesTests/Info.plist;
1041 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
1042 | PRODUCT_BUNDLE_IDENTIFIER = no.onmyway133.RecipesTests;
1043 | PRODUCT_NAME = "$(TARGET_NAME)";
1044 | SWIFT_VERSION = 4.0;
1045 | TARGETED_DEVICE_FAMILY = "1,2";
1046 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Recipes.app/Recipes";
1047 | };
1048 | name = Release;
1049 | };
1050 | D29C8B802041622A00F6DFC0 /* Debug */ = {
1051 | isa = XCBuildConfiguration;
1052 | baseConfigurationReference = 3E29D2FB9B154AD6EA94130B /* Pods-RecipesUITests.debug.xcconfig */;
1053 | buildSettings = {
1054 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
1055 | CODE_SIGN_STYLE = Automatic;
1056 | DEVELOPMENT_TEAM = T78DK947F2;
1057 | INFOPLIST_FILE = RecipesUITests/Info.plist;
1058 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
1059 | PRODUCT_BUNDLE_IDENTIFIER = no.onmyway133.RecipesUITests;
1060 | PRODUCT_NAME = "$(TARGET_NAME)";
1061 | SWIFT_VERSION = 4.0;
1062 | TARGETED_DEVICE_FAMILY = "1,2";
1063 | TEST_TARGET_NAME = Recipes;
1064 | };
1065 | name = Debug;
1066 | };
1067 | D29C8B812041622A00F6DFC0 /* Release */ = {
1068 | isa = XCBuildConfiguration;
1069 | baseConfigurationReference = 79900A81992AB62795E12ACA /* Pods-RecipesUITests.release.xcconfig */;
1070 | buildSettings = {
1071 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
1072 | CODE_SIGN_STYLE = Automatic;
1073 | DEVELOPMENT_TEAM = T78DK947F2;
1074 | INFOPLIST_FILE = RecipesUITests/Info.plist;
1075 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
1076 | PRODUCT_BUNDLE_IDENTIFIER = no.onmyway133.RecipesUITests;
1077 | PRODUCT_NAME = "$(TARGET_NAME)";
1078 | SWIFT_VERSION = 4.0;
1079 | TARGETED_DEVICE_FAMILY = "1,2";
1080 | TEST_TARGET_NAME = Recipes;
1081 | };
1082 | name = Release;
1083 | };
1084 | /* End XCBuildConfiguration section */
1085 |
1086 | /* Begin XCConfigurationList section */
1087 | D29C8B4C2041622900F6DFC0 /* Build configuration list for PBXProject "Recipes" */ = {
1088 | isa = XCConfigurationList;
1089 | buildConfigurations = (
1090 | D29C8B772041622A00F6DFC0 /* Debug */,
1091 | D29C8B782041622A00F6DFC0 /* Release */,
1092 | );
1093 | defaultConfigurationIsVisible = 0;
1094 | defaultConfigurationName = Release;
1095 | };
1096 | D29C8B792041622A00F6DFC0 /* Build configuration list for PBXNativeTarget "Recipes" */ = {
1097 | isa = XCConfigurationList;
1098 | buildConfigurations = (
1099 | D29C8B7A2041622A00F6DFC0 /* Debug */,
1100 | D29C8B7B2041622A00F6DFC0 /* Release */,
1101 | );
1102 | defaultConfigurationIsVisible = 0;
1103 | defaultConfigurationName = Release;
1104 | };
1105 | D29C8B7C2041622A00F6DFC0 /* Build configuration list for PBXNativeTarget "RecipesTests" */ = {
1106 | isa = XCConfigurationList;
1107 | buildConfigurations = (
1108 | D29C8B7D2041622A00F6DFC0 /* Debug */,
1109 | D29C8B7E2041622A00F6DFC0 /* Release */,
1110 | );
1111 | defaultConfigurationIsVisible = 0;
1112 | defaultConfigurationName = Release;
1113 | };
1114 | D29C8B7F2041622A00F6DFC0 /* Build configuration list for PBXNativeTarget "RecipesUITests" */ = {
1115 | isa = XCConfigurationList;
1116 | buildConfigurations = (
1117 | D29C8B802041622A00F6DFC0 /* Debug */,
1118 | D29C8B812041622A00F6DFC0 /* Release */,
1119 | );
1120 | defaultConfigurationIsVisible = 0;
1121 | defaultConfigurationName = Release;
1122 | };
1123 | /* End XCConfigurationList section */
1124 | };
1125 | rootObject = D29C8B492041622900F6DFC0 /* Project object */;
1126 | }
1127 |
--------------------------------------------------------------------------------
/RecipesTests/Fixture/recipes.json:
--------------------------------------------------------------------------------
1 | {
2 | "count": 30,
3 | "recipes": [
4 | {
5 | "publisher": "The Pioneer Woman",
6 | "f2f_url": "http://food2fork.com/view/47024",
7 | "title": "Perfect Iced Coffee",
8 | "source_url": "http://thepioneerwoman.com/cooking/2011/06/perfect-iced-coffee/",
9 | "recipe_id": "47024",
10 | "image_url": "http://static.food2fork.com/icedcoffee5766.jpg",
11 | "social_rank": 100.0,
12 | "publisher_url": "http://thepioneerwoman.com"
13 | },
14 | {
15 | "publisher": "Closet Cooking",
16 | "f2f_url": "http://food2fork.com/view/35382",
17 | "title": "Jalapeno Popper Grilled Cheese Sandwich",
18 | "source_url": "http://www.closetcooking.com/2011/04/jalapeno-popper-grilled-cheese-sandwich.html",
19 | "recipe_id": "35382",
20 | "image_url": "http://static.food2fork.com/Jalapeno2BPopper2BGrilled2BCheese2BSandwich2B12B500fd186186.jpg",
21 | "social_rank": 100.0,
22 | "publisher_url": "http://closetcooking.com"
23 | },
24 | {
25 | "publisher": "The Pioneer Woman",
26 | "f2f_url": "http://food2fork.com/view/47319",
27 | "title": "Crash Hot Potatoes",
28 | "source_url": "http://thepioneerwoman.com/cooking/2008/06/crash-hot-potatoes/",
29 | "recipe_id": "47319",
30 | "image_url": "http://static.food2fork.com/CrashHotPotatoes5736.jpg",
31 | "social_rank": 100.0,
32 | "publisher_url": "http://thepioneerwoman.com"
33 | },
34 | {
35 | "publisher": "Two Peas and Their Pod",
36 | "f2f_url": "http://food2fork.com/view/54384",
37 | "title": "Stovetop Avocado Mac and Cheese",
38 | "source_url": "http://www.twopeasandtheirpod.com/stovetop-avocado-mac-and-cheese/",
39 | "recipe_id": "54384",
40 | "image_url": "http://static.food2fork.com/avocadomacandcheesedc99.jpg",
41 | "social_rank": 100.0,
42 | "publisher_url": "http://www.twopeasandtheirpod.com"
43 | },
44 | {
45 | "publisher": "Closet Cooking",
46 | "f2f_url": "http://food2fork.com/view/35171",
47 | "title": "Buffalo Chicken Grilled Cheese Sandwich",
48 | "source_url": "http://www.closetcooking.com/2011/08/buffalo-chicken-grilled-cheese-sandwich.html",
49 | "recipe_id": "35171",
50 | "image_url": "http://static.food2fork.com/Buffalo2BChicken2BGrilled2BCheese2BSandwich2B5002B4983f2702fe4.jpg",
51 | "social_rank": 100.0,
52 | "publisher_url": "http://closetcooking.com"
53 | },
54 | {
55 | "publisher": "The Pioneer Woman",
56 | "f2f_url": "http://food2fork.com/view/d9a5e8",
57 | "title": "Cinnamon Rolls",
58 | "source_url": "http://thepioneerwoman.com/cooking/2007/06/cinammon_rolls_/",
59 | "recipe_id": "d9a5e8",
60 | "image_url": "http://static.food2fork.com/333323997_04bd8d6c53da11.jpg",
61 | "social_rank": 100.0,
62 | "publisher_url": "http://thepioneerwoman.com"
63 | },
64 | {
65 | "publisher": "101 Cookbooks",
66 | "f2f_url": "http://food2fork.com/view/47746",
67 | "title": "Best Pizza Dough Ever",
68 | "source_url": "http://www.101cookbooks.com/archives/001199.html",
69 | "recipe_id": "47746",
70 | "image_url": "http://static.food2fork.com/best_pizza_dough_recipe1b20.jpg",
71 | "social_rank": 100.0,
72 | "publisher_url": "http://www.101cookbooks.com"
73 | },
74 | {
75 | "publisher": "101 Cookbooks",
76 | "f2f_url": "http://food2fork.com/view/47899",
77 | "title": "Magic Sauce",
78 | "source_url": "http://www.101cookbooks.com/archives/magic-sauce-recipe.html",
79 | "recipe_id": "47899",
80 | "image_url": "http://static.food2fork.com/magic_sauce_recipeece9.jpg",
81 | "social_rank": 100.0,
82 | "publisher_url": "http://www.101cookbooks.com"
83 | },
84 | {
85 | "publisher": "The Pioneer Woman",
86 | "f2f_url": "http://food2fork.com/view/47042",
87 | "title": "Spicy Dr. Pepper Shredded Pork",
88 | "source_url": "http://thepioneerwoman.com/cooking/2011/03/spicy-dr-pepper-shredded-pork/",
89 | "recipe_id": "47042",
90 | "image_url": "http://static.food2fork.com/5551711173_dc42f7fc4b_zbd8a.jpg",
91 | "social_rank": 100.0,
92 | "publisher_url": "http://thepioneerwoman.com"
93 | },
94 | {
95 | "publisher": "Whats Gaby Cooking",
96 | "f2f_url": "http://food2fork.com/view/713134",
97 | "title": "Parmesan Roasted Potatoes",
98 | "source_url": "http://whatsgabycooking.com/parmesan-roasted-potatoes/",
99 | "recipe_id": "713134",
100 | "image_url": "http://static.food2fork.com/ParmesanRoastedPotatoes11985a.jpg",
101 | "social_rank": 100.0,
102 | "publisher_url": "http://whatsgabycooking.com"
103 | },
104 | {
105 | "publisher": "Closet Cooking",
106 | "f2f_url": "http://food2fork.com/view/35120",
107 | "title": "Bacon Wrapped Jalapeno Popper Stuffed Chicken",
108 | "source_url": "http://www.closetcooking.com/2012/11/bacon-wrapped-jalapeno-popper-stuffed.html",
109 | "recipe_id": "35120",
110 | "image_url": "http://static.food2fork.com/Bacon2BWrapped2BJalapeno2BPopper2BStuffed2BChicken2B5002B5909939b0e65.jpg",
111 | "social_rank": 100.0,
112 | "publisher_url": "http://closetcooking.com"
113 | },
114 | {
115 | "publisher": "My Baking Addiction",
116 | "f2f_url": "http://food2fork.com/view/035865",
117 | "title": "The Best Chocolate Cake",
118 | "source_url": "http://www.mybakingaddiction.com/the-best-chocolate-cake-recipe/",
119 | "recipe_id": "035865",
120 | "image_url": "http://static.food2fork.com/BlackMagicCakeSlice1of18c68.jpg",
121 | "social_rank": 100.0,
122 | "publisher_url": "http://www.mybakingaddiction.com"
123 | },
124 | {
125 | "publisher": "Closet Cooking",
126 | "f2f_url": "http://food2fork.com/view/35368",
127 | "title": "Hot Spinach and Artichoke Dip",
128 | "source_url": "http://www.closetcooking.com/2008/11/hot-spinach-and-artichoke-dip.html",
129 | "recipe_id": "35368",
130 | "image_url": "http://static.food2fork.com/HotSpinachandArtichokeDip5007579cdf5.jpg",
131 | "social_rank": 100.0,
132 | "publisher_url": "http://closetcooking.com"
133 | },
134 | {
135 | "publisher": "Two Peas and Their Pod",
136 | "f2f_url": "http://food2fork.com/view/54400",
137 | "title": "Smashed Chickpea & Avocado Salad Sandwich",
138 | "source_url": "http://www.twopeasandtheirpod.com/smashed-chickpea-avocado-salad-sandwich/",
139 | "recipe_id": "54400",
140 | "image_url": "http://static.food2fork.com/smashedchickpeaavocadosaladsandwich29c5b.jpg",
141 | "social_rank": 100.0,
142 | "publisher_url": "http://www.twopeasandtheirpod.com"
143 | },
144 | {
145 | "publisher": "The Pioneer Woman",
146 | "f2f_url": "http://food2fork.com/view/47166",
147 | "title": "Restaurant Style Salsa",
148 | "source_url": "http://thepioneerwoman.com/cooking/2010/01/restaurant-style-salsa/",
149 | "recipe_id": "47166",
150 | "image_url": "http://static.food2fork.com/4307514771_c089bbd71017f42.jpg",
151 | "social_rank": 100.0,
152 | "publisher_url": "http://thepioneerwoman.com"
153 | },
154 | {
155 | "publisher": "The Pioneer Woman",
156 | "f2f_url": "http://food2fork.com/view/8f3e73",
157 | "title": "The Best Lasagna Ever",
158 | "source_url": "http://thepioneerwoman.com/cooking/2007/06/the_best_lasagn/",
159 | "recipe_id": "8f3e73",
160 | "image_url": "http://static.food2fork.com/387114468_aafd1be3404a2f.jpg",
161 | "social_rank": 100.0,
162 | "publisher_url": "http://thepioneerwoman.com"
163 | },
164 | {
165 | "publisher": "My Baking Addiction",
166 | "f2f_url": "http://food2fork.com/view/e7fdb2",
167 | "title": "Mac and Cheese with Roasted Chicken, Goat Cheese, and Rosemary",
168 | "source_url": "http://www.mybakingaddiction.com/mac-and-cheese-roasted-chicken-and-goat-cheese/",
169 | "recipe_id": "e7fdb2",
170 | "image_url": "http://static.food2fork.com/MacandCheese1122b.jpg",
171 | "social_rank": 100.0,
172 | "publisher_url": "http://www.mybakingaddiction.com"
173 | },
174 | {
175 | "publisher": "Closet Cooking",
176 | "f2f_url": "http://food2fork.com/view/35354",
177 | "title": "Guinness Chocolate Cheesecake",
178 | "source_url": "http://www.closetcooking.com/2011/03/guinness-chocolate-cheesecake.html",
179 | "recipe_id": "35354",
180 | "image_url": "http://static.food2fork.com/Guinness2BChocolate2BCheesecake2B12B5002af4b6b4.jpg",
181 | "social_rank": 100.0,
182 | "publisher_url": "http://closetcooking.com"
183 | },
184 | {
185 | "publisher": "Simply Recipes",
186 | "f2f_url": "http://food2fork.com/view/35760",
187 | "title": "Banana Bread",
188 | "source_url": "http://www.simplyrecipes.com/recipes/banana_bread/",
189 | "recipe_id": "35760",
190 | "image_url": "http://static.food2fork.com/banana_bread300x2000a14c8c5.jpeg",
191 | "social_rank": 100.0,
192 | "publisher_url": "http://simplyrecipes.com"
193 | },
194 | {
195 | "publisher": "All Recipes",
196 | "f2f_url": "http://food2fork.com/view/32478",
197 | "title": "The Best Rolled Sugar Cookies",
198 | "source_url": "http://allrecipes.com/Recipe/The-Best-Rolled-Sugar-Cookies/Detail.aspx",
199 | "recipe_id": "32478",
200 | "image_url": "http://static.food2fork.com/9956913c10.jpg",
201 | "social_rank": 100.0,
202 | "publisher_url": "http://allrecipes.com"
203 | },
204 | {
205 | "publisher": "Two Peas and Their Pod",
206 | "f2f_url": "http://food2fork.com/view/54427",
207 | "title": "Guacamole Grilled Cheese Sandwich",
208 | "source_url": "http://www.twopeasandtheirpod.com/guacamole-grilled-cheese-sandwich/",
209 | "recipe_id": "54427",
210 | "image_url": "http://static.food2fork.com/GuacamoleGrilledCheese6019.jpg",
211 | "social_rank": 100.0,
212 | "publisher_url": "http://www.twopeasandtheirpod.com"
213 | },
214 | {
215 | "publisher": "Two Peas and Their Pod",
216 | "f2f_url": "http://food2fork.com/view/54489",
217 | "title": "Two-Ingredient Banana Peanut Butter Ice Cream",
218 | "source_url": "http://www.twopeasandtheirpod.com/two-ingredient-banana-peanut-butter-ice-cream/",
219 | "recipe_id": "54489",
220 | "image_url": "http://static.food2fork.com/bananapeanutbuttericecream5c16d.jpg",
221 | "social_rank": 100.0,
222 | "publisher_url": "http://www.twopeasandtheirpod.com"
223 | },
224 | {
225 | "publisher": "Simply Recipes",
226 | "f2f_url": "http://food2fork.com/view/36259",
227 | "title": "Easy Shepherd’s Pie",
228 | "source_url": "http://www.simplyrecipes.com/recipes/easy_shepherds_pie/",
229 | "recipe_id": "36259",
230 | "image_url": "http://static.food2fork.com/shepherdspie300x2003d240a98.jpg",
231 | "social_rank": 100.0,
232 | "publisher_url": "http://simplyrecipes.com"
233 | },
234 | {
235 | "publisher": "Closet Cooking",
236 | "f2f_url": "http://food2fork.com/view/35169",
237 | "title": "Buffalo Chicken Chowder",
238 | "source_url": "http://www.closetcooking.com/2011/11/buffalo-chicken-chowder.html",
239 | "recipe_id": "35169",
240 | "image_url": "http://static.food2fork.com/Buffalo2BChicken2BChowder2B5002B0075c131caa8.jpg",
241 | "social_rank": 100.0,
242 | "publisher_url": "http://closetcooking.com"
243 | },
244 | {
245 | "publisher": "All Recipes",
246 | "f2f_url": "http://food2fork.com/view/3620",
247 | "title": "Best Brownies",
248 | "source_url": "http://allrecipes.com/Recipe/Best-Brownies/Detail.aspx",
249 | "recipe_id": "3620",
250 | "image_url": "http://static.food2fork.com/720553ee26.jpg",
251 | "social_rank": 100.0,
252 | "publisher_url": "http://allrecipes.com"
253 | },
254 | {
255 | "publisher": "All Recipes",
256 | "f2f_url": "http://food2fork.com/view/29159",
257 | "title": "Slow Cooker Chicken Tortilla Soup",
258 | "source_url": "http://allrecipes.com/Recipe/Slow-Cooker-Chicken-Tortilla-Soup/Detail.aspx",
259 | "recipe_id": "29159",
260 | "image_url": "http://static.food2fork.com/19321150c4.jpg",
261 | "social_rank": 100.0,
262 | "publisher_url": "http://allrecipes.com"
263 | },
264 | {
265 | "publisher": "All Recipes",
266 | "f2f_url": "http://food2fork.com/view/2803",
267 | "title": "Banana Crumb Muffins",
268 | "source_url": "http://allrecipes.com/Recipe/Banana-Crumb-Muffins/Detail.aspx",
269 | "recipe_id": "2803",
270 | "image_url": "http://static.food2fork.com/124030cedd.jpg",
271 | "social_rank": 100.0,
272 | "publisher_url": "http://allrecipes.com"
273 | },
274 | {
275 | "publisher": "All Recipes",
276 | "f2f_url": "http://food2fork.com/view/2734",
277 | "title": "Banana Banana Bread",
278 | "source_url": "http://allrecipes.com/Recipe/Banana-Banana-Bread/Detail.aspx",
279 | "recipe_id": "2734",
280 | "image_url": "http://static.food2fork.com/254186ea50.jpg",
281 | "social_rank": 100.0,
282 | "publisher_url": "http://allrecipes.com"
283 | },
284 | {
285 | "publisher": "101 Cookbooks",
286 | "f2f_url": "http://food2fork.com/view/47692",
287 | "title": "Nikki",
288 | "source_url": "http://www.101cookbooks.com/archives/nikkis-healthy-cookies-recipe.html",
289 | "recipe_id": "47692",
290 | "image_url": "http://static.food2fork.com/healthy_cookies4ee3.jpg",
291 | "social_rank": 100.0,
292 | "publisher_url": "http://www.101cookbooks.com"
293 | },
294 | {
295 | "publisher": "All Recipes",
296 | "f2f_url": "http://food2fork.com/view/34889",
297 | "title": "Zesty Slow Cooker Chicken Barbeque",
298 | "source_url": "http://allrecipes.com/Recipe/Zesty-Slow-Cooker-Chicken-Barbecue/Detail.aspx",
299 | "recipe_id": "34889",
300 | "image_url": "http://static.food2fork.com/4515542dbb.jpg",
301 | "social_rank": 100.0,
302 | "publisher_url": "http://allrecipes.com"
303 | }
304 | ]
305 | }
--------------------------------------------------------------------------------
/RecipesTests/Fixture/singleRecipe.json:
--------------------------------------------------------------------------------
1 | {
2 | "recipe": {
3 | "publisher": "My Baking Addiction",
4 | "f2f_url": "http://food2fork.com/view/8061c3",
5 | "ingredients": [
6 | " cup plus 2 tablespoons granulated sugar",
7 | "zest of one lemon",
8 | "2 packages cream cheese, 8 oz each; room temperature",
9 | "2 large eggs; room temperature",
10 | " cup heavy cream"
11 | ],
12 | "source_url": "http://www.mybakingaddiction.com/cheesecake-in-a-jar-recipe/",
13 | "recipe_id": "8061c3",
14 | "image_url": "http://static.food2fork.com/CheesecakeJars2Crop1of1eedb.jpg",
15 | "social_rank": 100.0,
16 | "publisher_url": "http://www.mybakingaddiction.com",
17 | "title": "Virtual Picnic- Cheesecake in a Jar"
18 | }
19 | }
--------------------------------------------------------------------------------
/RecipesTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RecipesTests/Library/Model/RecipeTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Recipes
3 |
4 | class RecipesTests: XCTestCase {
5 | func testParsing() throws {
6 | let json: [String: Any] = [
7 | "publisher": "Two Peas and Their Pod",
8 | "f2f_url": "http://food2fork.com/view/975e33",
9 | "title": "No-Bake Chocolate Peanut Butter Pretzel Cookies",
10 | "source_url": "http://www.twopeasandtheirpod.com/no-bake-chocolate-peanut-butter-pretzel-cookies/",
11 | "recipe_id": "975e33",
12 | "image_url": "http://static.food2fork.com/NoBakeChocolatePeanutButterPretzelCookies44147.jpg",
13 | "social_rank": 99.99999999999974,
14 | "publisher_url": "http://www.twopeasandtheirpod.com"
15 | ]
16 |
17 | let data = try JSONSerialization.data(withJSONObject: json, options: [])
18 | let decoder = JSONDecoder()
19 | let recipe = try decoder.decode(Recipe.self, from: data)
20 |
21 | XCTAssertEqual(recipe.title, "No-Bake Chocolate Peanut Butter Pretzel Cookies")
22 | XCTAssertEqual(recipe.id, "975e33")
23 | XCTAssertEqual(recipe.url, URL(string: "http://food2fork.com/view/975e33")!)
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/RecipesTests/Library/Service/CacheServiceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CacheServiceTests.swift
3 | // RecipesTests
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Recipes
11 |
12 | class CacheServiceTests: XCTestCase {
13 | let service = CacheService()
14 |
15 | override func setUp() {
16 | super.setUp()
17 |
18 | try? service.clear()
19 | }
20 |
21 | func testClear() {
22 | let expectation = self.expectation(description: #function)
23 | let string = "Hello world"
24 | let data = string.data(using: .utf8)!
25 |
26 | service.save(data: data, key: "key", completion: {
27 | try? self.service.clear()
28 | self.service.load(key: "key", completion: {
29 | XCTAssertNil($0)
30 | expectation.fulfill()
31 | })
32 | })
33 |
34 | wait(for: [expectation], timeout: 1)
35 | }
36 |
37 | func testSave() {
38 | let expectation = self.expectation(description: #function)
39 | let string = "Hello world"
40 | let data = string.data(using: .utf8)!
41 |
42 | service.save(data: data, key: "key")
43 | service.load(key: "key", completion: {
44 | XCTAssertEqual($0, data)
45 | expectation.fulfill()
46 | })
47 |
48 | wait(for: [expectation], timeout: 1)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/RecipesTests/Library/Service/RecipesService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipesService.swift
3 | // RecipesTests
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Recipes
11 |
12 | class RecipesServiceTests: XCTestCase {
13 | func testFetchTopRating() {
14 | let expectation = self.expectation(description: #function)
15 | let mockNetworkService = MockNetworkService(fileName: "recipes")
16 | let recipesService = RecipesService(networking: mockNetworkService)
17 | recipesService.fetchTopRating(completion: { recipes in
18 | XCTAssertEqual(recipes.count, 30)
19 | expectation.fulfill()
20 | })
21 |
22 | wait(for: [expectation], timeout: 1)
23 | }
24 |
25 | func testFetchSingleRecipe() {
26 | let expectation = self.expectation(description: #function)
27 | let mockNetworkService = MockNetworkService(fileName: "singleRecipe")
28 | let recipesService = RecipesService(networking: mockNetworkService)
29 | recipesService.fetch(recipeId: "", completion: { recipe in
30 | XCTAssertEqual(recipe?.ingredients?.count, 5)
31 | expectation.fulfill()
32 | })
33 |
34 | wait(for: [expectation], timeout: 1)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/RecipesTests/Library/Utils/DebouncerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DebouncerTests.swift
3 | // RecipesTests
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Recipes
11 |
12 | class DebouncerTests: XCTestCase {
13 | func testDebouncing() {
14 | let cancelExpectation = self.expectation(description: "cancel")
15 | cancelExpectation.isInverted = true
16 |
17 | let completeExpectation = self.expectation(description: "complete")
18 | let debouncer = Debouncer(delay: 0.3)
19 |
20 | debouncer.schedule {
21 | cancelExpectation.fulfill()
22 | }
23 |
24 | debouncer.schedule {
25 | completeExpectation.fulfill()
26 | }
27 |
28 | wait(for: [cancelExpectation, completeExpectation], timeout: 1)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RecipesTests/Mock/MockNetworkService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockNetworkService.swift
3 | // RecipesTests
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import Recipes
11 |
12 | final class MockNetworkService: Networking {
13 | let data: Data
14 | init(fileName: String) {
15 | let bundle = Bundle(for: MockNetworkService.self)
16 | let url = bundle.url(forResource: fileName, withExtension: "json")!
17 | self.data = try! Data(contentsOf: url)
18 | }
19 |
20 | func fetch(resource: Resource, completion: @escaping (Data?) -> Void) -> URLSessionTask? {
21 | completion(data)
22 | return nil
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/RecipesUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RecipesUITests/RecipesUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipesUITests.swift
3 | // RecipesUITests
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RecipesUITests: XCTestCase {
12 | var app: XCUIApplication!
13 |
14 | override func setUp() {
15 | super.setUp()
16 | continueAfterFailure = false
17 |
18 | app = XCUIApplication()
19 | }
20 |
21 | func testScrolling() {
22 | app.launch()
23 |
24 | let collectionView = app.collectionViews.element(boundBy: 0)
25 | collectionView.swipeUp()
26 | collectionView.swipeUp()
27 | }
28 |
29 | func testGoToDetail() {
30 | app.launch()
31 |
32 | let collectionView = app.collectionViews.element(boundBy: 0)
33 | let firstCell = collectionView.cells.element(boundBy: 0)
34 | firstCell.tap()
35 | }
36 |
37 | func testOpenInstructionInExternalWebPage() {
38 | testGoToDetail()
39 |
40 | let button = app.buttons["View instructions"]
41 | button.tap()
42 | }
43 |
44 | func testSearch() {
45 | app.launch()
46 |
47 | let searchField = app.searchFields.element(boundBy: 0)
48 | searchField.tap()
49 |
50 | searchField.typeText("banana")
51 |
52 | let collectionView = app.collectionViews.element(boundBy: 1)
53 | _ = collectionView.waitForExistence(timeout: 8)
54 | let firstCell = collectionView.cells.element(boundBy: 0)
55 | firstCell.tap()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "icon_20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "icon_20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "icon_29@2x.png",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "icon_29@3x.png",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "icon_40@2x.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "icon_40@3x.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "icon_60@2x.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "icon_60@3x.png",
49 | "scale" : "3x"
50 | },
51 | {
52 | "size" : "20x20",
53 | "idiom" : "ipad",
54 | "filename" : "icon_20@1x.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "icon_20@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "ipad",
66 | "filename" : "icon_29@1x.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "icon_29@2x.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "40x40",
77 | "idiom" : "ipad",
78 | "filename" : "icon_40@1x.png",
79 | "scale" : "1x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "icon_40@2x.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "icon_76@1x.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "icon_76@2x.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "icon_83.5@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "icon_1024@1x.png",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_1024@1x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_20@1x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_20@2x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_20@3x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_29@1x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_29@2x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_29@3x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_40@1x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_40@2x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_40@3x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_60@2x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_60@3x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_76@1x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_76@2x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/launchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "Spoon.jpg",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/launchImage.imageset/Spoon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/launchImage.imageset/Spoon.jpg
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/notFound.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "notFound.png",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/notFound.imageset/notFound.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/notFound.imageset/notFound.png
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/recipePlaceholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "recipePlaceholder.jpg",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Resource/Assets.xcassets/recipePlaceholder.imageset/recipePlaceholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Resource/Assets.xcassets/recipePlaceholder.imageset/recipePlaceholder.jpg
--------------------------------------------------------------------------------
/Resource/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Resource/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 |
33 | UISupportedInterfaceOrientations~ipad
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationPortraitUpsideDown
37 | UIInterfaceOrientationLandscapeLeft
38 | UIInterfaceOrientationLandscapeRight
39 |
40 | NSAppTransportSecurity
41 |
42 | NSExceptionDomains
43 |
44 | food2fork.com
45 |
46 | NSIncludesSubdomains
47 |
48 | NSExceptionAllowsInsecureHTTPLoads
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Screenshots/AppIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/AppIcon.png
--------------------------------------------------------------------------------
/Screenshots/AppStore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/AppStore.png
--------------------------------------------------------------------------------
/Screenshots/Detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/Detail.png
--------------------------------------------------------------------------------
/Screenshots/Home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/Home.png
--------------------------------------------------------------------------------
/Screenshots/Insomnia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/Insomnia.png
--------------------------------------------------------------------------------
/Screenshots/LaunchScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/LaunchScreen.png
--------------------------------------------------------------------------------
/Screenshots/MARK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/MARK.png
--------------------------------------------------------------------------------
/Screenshots/MainGuard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/MainGuard.png
--------------------------------------------------------------------------------
/Screenshots/Measurement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/Measurement.png
--------------------------------------------------------------------------------
/Screenshots/Project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/Project.png
--------------------------------------------------------------------------------
/Screenshots/SwiftLint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/Recipes/a20b0ef6077e3ff9be84e74a1676ee0aed3f7e40/Screenshots/SwiftLint.png
--------------------------------------------------------------------------------
/Source/Feature/Detail/RecipeDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipeDetailView.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class RecipeDetailView: UIView {
12 | private let scrollableView = ScrollableView()
13 | private(set) lazy var imageView: UIImageView = self.makeImageView()
14 | private(set) lazy var ingredientLabel: UILabel = self.makeIngredientLabel()
15 | private(set) lazy var instructionButton: UIButton = self.makeButton()
16 | private(set) lazy var originalButton: UIButton = self.makeButton()
17 | private(set) lazy var infoView: InfoView = InfoView()
18 |
19 | override init(frame: CGRect) {
20 | super.init(frame: frame)
21 |
22 | setupConstraints()
23 | }
24 |
25 | required init?(coder aDecoder: NSCoder) {
26 | fatalError()
27 | }
28 |
29 | private func setupConstraints() {
30 | let ingredientHeaderView = HeaderView(text: "Ingredients")
31 | let infoHeaderView = HeaderView(text: "Info")
32 | instructionButton.setTitle("View instructions", for: .normal)
33 | originalButton.setTitle("View original", for: .normal)
34 |
35 | addSubview(scrollableView)
36 | NSLayoutConstraint.pin(view: scrollableView, toEdgesOf: self)
37 |
38 | scrollableView.setup(pairs: [
39 | ScrollableView.Pair(view: imageView, inset: UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)),
40 | ScrollableView.Pair(view: ingredientHeaderView, inset: UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)),
41 | ScrollableView.Pair(view: ingredientLabel, inset: UIEdgeInsets(top: 4, left: 8, bottom: 0, right: 0)),
42 | ScrollableView.Pair(view: infoHeaderView, inset: UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0)),
43 | ScrollableView.Pair(view: instructionButton, inset: UIEdgeInsets(top: 8, left: 20, bottom: 0, right: 20)),
44 | ScrollableView.Pair(view: originalButton, inset: UIEdgeInsets(top: 8, left: 20, bottom: 0, right: 20)),
45 | ScrollableView.Pair(view: infoView, inset: UIEdgeInsets(top: 16, left: 0, bottom: 20, right: 0))
46 | ])
47 |
48 | NSLayoutConstraint.activate([
49 | imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 9.0/16),
50 |
51 | ingredientHeaderView.heightAnchor.constraint(equalToConstant: 30),
52 | infoHeaderView.heightAnchor.constraint(equalToConstant: 30),
53 |
54 | instructionButton.heightAnchor.constraint(equalToConstant: 50),
55 | originalButton.heightAnchor.constraint(equalToConstant: 50)
56 | ])
57 | }
58 |
59 | // MARK: - Make
60 |
61 | private func makeImageView() -> UIImageView {
62 | let imageView = UIImageView()
63 | imageView.contentMode = .scaleAspectFill
64 | imageView.clipsToBounds = true
65 | return imageView
66 | }
67 |
68 | private func makeIngredientLabel() -> UILabel {
69 | let label = UILabel()
70 | label.numberOfLines = 0
71 | return label
72 | }
73 |
74 | private func makeButton() -> UIButton {
75 | let button = UIButton()
76 | button.backgroundColor = Color.main
77 | button.layer.cornerRadius = 10
78 | button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
79 | button.setTitleColor(.darkGray, for: .highlighted)
80 |
81 | return button
82 | }
83 | }
84 |
85 | private final class HeaderView: UIView {
86 | private(set) lazy var label: UILabel = self.makeLabel()
87 |
88 | required init(text: String) {
89 | super.init(frame: .zero)
90 | label.text = text
91 | addSubview(label)
92 | backgroundColor = Color.main.withAlphaComponent(0.8)
93 |
94 | setupConstraints()
95 | }
96 |
97 | required init?(coder aDecoder: NSCoder) {
98 | fatalError()
99 | }
100 |
101 | override func layoutSubviews() {
102 | super.layoutSubviews()
103 |
104 | round(corners: .topRight)
105 | }
106 |
107 | // MARK: - Make
108 |
109 | private func makeLabel() -> UILabel {
110 | let label = UILabel()
111 | label.font = UIFont.preferredFont(forTextStyle: .headline)
112 | return label
113 | }
114 |
115 | private func setupConstraints() {
116 | NSLayoutConstraint.activate([
117 | label.centerYAnchor.constraint(equalTo: centerYAnchor),
118 | label.leftAnchor.constraint(equalTo: leftAnchor, constant: 8)
119 | ])
120 | }
121 | }
122 |
123 | final class InfoView: UIView {
124 | private(set) lazy var leftLabel: UILabel = self.makeLabel()
125 | private(set) lazy var rightLabel: UILabel = self.makeLabel()
126 |
127 | override init(frame: CGRect) {
128 | super.init(frame: frame)
129 |
130 | addSubviews([leftLabel, rightLabel])
131 | rightLabel.textAlignment = .right
132 | setupConstraints()
133 | }
134 |
135 | required init?(coder aDecoder: NSCoder) {
136 | fatalError()
137 | }
138 |
139 | // MARK: - Make
140 |
141 | private func makeLabel() -> UILabel {
142 | let label = UILabel()
143 | label.font = UIFont.preferredFont(forTextStyle: .footnote)
144 |
145 | return label
146 | }
147 |
148 | private func setupConstraints() {
149 | NSLayoutConstraint.activate([
150 | leftLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 8),
151 | rightLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
152 |
153 | leftLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
154 | rightLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
155 | ])
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Source/Feature/Detail/RecipeDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipeDetailViewController.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Show detail information for a recipe
12 | final class RecipeDetailViewController: BaseController {
13 | private let recipe: Recipe
14 | private let recipesService: RecipesService
15 |
16 | var selectInstruction: ((URL) -> Void)?
17 | var selectOriginal: ((URL) -> Void)?
18 |
19 | // MARK: - Init
20 |
21 | required init(recipe: Recipe, recipesService: RecipesService) {
22 | self.recipe = recipe
23 | self.recipesService = recipesService
24 | super.init(nibName: nil, bundle: nil)
25 | self.title = recipe.title
26 | }
27 |
28 | required init?(coder aDecoder: NSCoder) {
29 | fatalError()
30 | }
31 |
32 | // MARK: - Life cycle
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 |
37 | view.backgroundColor = Color.background
38 | setup()
39 | update(recipe: recipe)
40 | loadData()
41 | }
42 |
43 | private func setup() {
44 | root.instructionButton.addTarget(self, action: #selector(instructionButtonTouched), for: .touchUpInside)
45 | root.originalButton.addTarget(self, action: #selector(originalButtonTouched), for: .touchUpInside)
46 | }
47 |
48 | private func update(recipe: Recipe) {
49 | root.imageView.setImage(url: recipe.imageUrl, placeholder: R.image.recipePlaceholder())
50 | root.infoView.leftLabel.text = recipe.publisher
51 | root.infoView.rightLabel.text = "Social rank: \(Int(recipe.socialRank))"
52 |
53 | if let ingredients = recipe.ingredients {
54 | let text = ingredients
55 | .map(({ "🍭 \($0)" }))
56 | .joined(separator: "\n")
57 |
58 | UIView.transition(
59 | with: root,
60 | duration: 0.25,
61 | options: .transitionCrossDissolve,
62 | animations: {
63 | self.root.ingredientLabel.text = text
64 | },
65 | completion: nil
66 | )
67 | }
68 | }
69 |
70 | // MARK: - Action
71 |
72 | @objc private func instructionButtonTouched() {
73 | selectInstruction?(recipe.sourceUrl)
74 | }
75 |
76 | @objc private func originalButtonTouched() {
77 | selectOriginal?(recipe.publisherUrl)
78 | }
79 |
80 | // MARK: - Data
81 |
82 | private func loadData() {
83 | recipesService.fetch(recipeId: recipe.id, completion: { [weak self] recipe in
84 | if let recipe = recipe {
85 | self?.update(recipe: recipe)
86 | }
87 | })
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Source/Feature/Home/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Show a list of recipes
12 | final class HomeViewController: UIViewController {
13 |
14 | /// When a recipe get select
15 | var select: ((Recipe) -> Void)?
16 |
17 | private var refreshControl = UIRefreshControl()
18 | private let recipesService: RecipesService
19 | private let searchComponent: SearchComponent
20 | private let recipeListViewController = RecipeListViewController()
21 |
22 | // MARK: - Init
23 |
24 | required init(recipesService: RecipesService) {
25 | self.recipesService = recipesService
26 | self.searchComponent = SearchComponent(recipesService: recipesService)
27 | super.init(nibName: nil, bundle: nil)
28 | self.title = "Recipes"
29 | }
30 |
31 | required init?(coder aDecoder: NSCoder) {
32 | fatalError()
33 | }
34 |
35 | // MARK: - View life Cycle
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 |
40 | setup()
41 | setupSearch()
42 | loadData()
43 | }
44 |
45 | // MARK: - Setup
46 |
47 | private func setup() {
48 | recipeListViewController.adapter.select = select
49 | add(childViewController: recipeListViewController)
50 | recipeListViewController.collectionView.addSubview(refreshControl)
51 | refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
52 | }
53 |
54 | private func setupSearch() {
55 | searchComponent.add(to: self)
56 | searchComponent.recipeListViewController.adapter.select = select
57 | }
58 |
59 | // MARK: - Action
60 |
61 | @objc private func refresh() {
62 | loadData()
63 | }
64 |
65 | // MARK: - Data
66 |
67 | private func loadData() {
68 | refreshControl.beginRefreshing()
69 | recipesService.fetchTopRating(completion: { [weak self] recipes in
70 | self?.recipeListViewController.handle(recipes: recipes)
71 | self?.refreshControl.endRefreshing()
72 | })
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Source/Feature/List/RecipeCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipeCell.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class RecipeCell: UICollectionViewCell {
12 | private(set) lazy var containerView: UIView = {
13 | let view = UIView()
14 | view.clipsToBounds = true
15 | view.layer.cornerRadius = 5
16 | view.backgroundColor = Color.main.withAlphaComponent(0.4)
17 |
18 | return view
19 | }()
20 |
21 | private(set) lazy var imageView: UIImageView = {
22 | let imageView = UIImageView()
23 | imageView.contentMode = .scaleAspectFill
24 | imageView.clipsToBounds = true
25 |
26 | return imageView
27 | }()
28 |
29 | private(set) lazy var label: UILabel = {
30 | let label = UILabel()
31 | label.numberOfLines = 2
32 | label.font = UIFont.preferredFont(forTextStyle: .headline)
33 |
34 | return label
35 | }()
36 |
37 | override init(frame: CGRect) {
38 | super.init(frame: frame)
39 |
40 | addSubview(containerView)
41 | containerView.addSubview(imageView)
42 | containerView.addSubview(label)
43 |
44 | setupConstraints()
45 | }
46 |
47 | required init?(coder aDecoder: NSCoder) {
48 | fatalError()
49 | }
50 |
51 | private func setupConstraints() {
52 | NSLayoutConstraint.activate([
53 | containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 8),
54 | containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
55 | containerView.topAnchor.constraint(equalTo: topAnchor, constant: 1),
56 | containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1),
57 |
58 | imageView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 4),
59 | imageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
60 | imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4),
61 | imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1),
62 |
63 | label.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
64 | label.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: 8),
65 | label.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -8)
66 | ])
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Source/Feature/List/RecipeListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipeListViewController
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Show a list of recipes
12 | final class RecipeListViewController: UIViewController {
13 | private(set) var collectionView: UICollectionView!
14 | let adapter = Adapter()
15 | private let emptyView = EmptyView(text: "No recipes found!")
16 |
17 | // MARK: - Life Cycle
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 |
22 | setup()
23 | }
24 |
25 | private func setup() {
26 | view.backgroundColor = .white
27 |
28 | let layout = UICollectionViewFlowLayout()
29 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
30 | collectionView.dataSource = adapter
31 | collectionView.delegate = adapter
32 | collectionView.register(cellType: RecipeCell.self)
33 | collectionView.backgroundColor = Color.background
34 | collectionView.contentInset.top = 8
35 | collectionView.alwaysBounceVertical = true
36 |
37 | adapter.configure = { recipe, cell in
38 | cell.imageView.setImage(url: recipe.imageUrl, placeholder: R.image.recipePlaceholder())
39 | cell.label.text = recipe.title
40 | }
41 |
42 | view.addSubview(collectionView)
43 | NSLayoutConstraint.pin(view: collectionView, toEdgesOf: view)
44 |
45 | view.addSubview(emptyView)
46 | NSLayoutConstraint.pin(view: emptyView, toEdgesOf: view)
47 | emptyView.alpha = 0
48 | }
49 |
50 | func handle(recipes: [Recipe]) {
51 | adapter.items = recipes
52 | collectionView.reloadData()
53 |
54 | UIView.animate(withDuration: 0.25, animations: {
55 | self.emptyView.alpha = recipes.isEmpty ? 1 : 0
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Source/Feature/Search/SearchComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchComponent.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A reusable component to add search functionality via search bar
12 | final class SearchComponent: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
13 | let recipesService: RecipesService
14 | let searchController: UISearchController
15 | let recipeListViewController = RecipeListViewController()
16 | var task: URLSessionTask?
17 | private lazy var loadingIndicator: UIActivityIndicatorView = self.makeLoadingIndicator()
18 |
19 | /// Avoid making lots of requests when user types fast
20 | /// This throttles the search requests
21 | let debouncer = Debouncer(delay: 2)
22 |
23 | required init(recipesService: RecipesService) {
24 | self.recipesService = recipesService
25 | self.searchController = UISearchController(searchResultsController: recipeListViewController)
26 | super.init()
27 | searchController.searchResultsUpdater = self
28 | searchController.searchBar.delegate = self
29 | searchController.dimsBackgroundDuringPresentation = true
30 | searchController.hidesNavigationBarDuringPresentation = false
31 | searchController.searchBar.placeholder = "Search recipe"
32 |
33 | recipeListViewController.view.addSubview(loadingIndicator)
34 | NSLayoutConstraint.activate([
35 | loadingIndicator.centerXAnchor.constraint(equalTo: recipeListViewController.view.centerXAnchor),
36 | loadingIndicator.centerYAnchor.constraint(equalTo: recipeListViewController.view.centerYAnchor, constant: -100)
37 | ])
38 | }
39 |
40 | /// Add search bar to view controller
41 | func add(to viewController: UIViewController) {
42 | if #available(iOS 11, *) {
43 | viewController.navigationItem.searchController = searchController
44 | viewController.navigationItem.hidesSearchBarWhenScrolling = false
45 | } else {
46 | viewController.navigationItem.titleView = searchController.searchBar
47 | }
48 |
49 | viewController.definesPresentationContext = true
50 | }
51 |
52 | // MARK: - UISearchResultsUpdating
53 |
54 | func updateSearchResults(for searchController: UISearchController) {
55 | // No op
56 | }
57 |
58 | // MARK: - UISearchBarDelegate
59 |
60 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
61 | debouncer.schedule { [weak self] in
62 | self?.performSearch()
63 | }
64 | }
65 |
66 | // MARK: - Logic
67 |
68 | private func performSearch() {
69 | guard let text = searchController.searchBar.text, !text.isEmpty else {
70 | return
71 | }
72 |
73 | search(query: text)
74 | }
75 |
76 | private func search(query: String) {
77 | task?.cancel()
78 | loadingIndicator.startAnimating()
79 | task = recipesService.search(query: query, completion: { [weak self] recipes in
80 | self?.loadingIndicator.stopAnimating()
81 | self?.recipeListViewController.handle(recipes: recipes)
82 | })
83 | }
84 |
85 | // MARK: - Make
86 |
87 | private func makeLoadingIndicator() -> UIActivityIndicatorView {
88 | let view = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
89 | view.color = .darkGray
90 | view.hidesWhenStopped = true
91 | return view
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Source/Library/Adapter/Adapter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Adapter.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A generic adapter to act as convenient DataSource and Delegate for UICollectionView
12 | final class Adapter: NSObject,
13 | UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
14 | var items: [T] = []
15 | var configure: ((T, Cell) -> Void)?
16 | var select: ((T) -> Void)?
17 | var cellHeight: CGFloat = 60
18 |
19 | func numberOfSections(in collectionView: UICollectionView) -> Int {
20 | return 1
21 | }
22 |
23 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
24 | return items.count
25 | }
26 |
27 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
28 | let item = items[indexPath.item]
29 |
30 | let cell: Cell = collectionView.dequeue(indexPath: indexPath)
31 | configure?(item, cell)
32 | return cell
33 | }
34 |
35 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
36 | let item = items[indexPath.item]
37 | select?(item)
38 | }
39 |
40 | func collectionView(
41 | _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
42 | sizeForItemAt indexPath: IndexPath) -> CGSize {
43 |
44 | return CGSize(width: collectionView.frame.size.width, height: cellHeight)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Source/Library/App/AppConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppConfig.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Configurations and keys for this app
12 | struct AppConfig {
13 |
14 | /// API key for food2fork
15 | static let apiKey = "f1c105dd473738d96661a5a644ba4815"
16 | }
17 |
--------------------------------------------------------------------------------
/Source/Library/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 | let appFlowController = AppFlowController()
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 |
19 | window = UIWindow(frame: UIScreen.main.bounds)
20 | window?.rootViewController = appFlowController
21 | window?.makeKeyAndVisible()
22 |
23 | appFlowController.start()
24 |
25 | return true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Source/Library/App/Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// App color
12 | struct Color {
13 | static let background = UIColor(hex: "#ecf0f1")
14 | static let main = UIColor(hex: "#9E6BE0")
15 | }
16 |
--------------------------------------------------------------------------------
/Source/Library/Base/BaseController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseController.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Used to separate between controller and view
12 | class BaseController: UIViewController {
13 | let root = T()
14 |
15 | override func loadView() {
16 | view = root
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Source/Library/Constants/R.generated.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This is a generated file, do not edit!
3 | // Generated by R.swift, see https://github.com/mac-cain13/R.swift
4 | //
5 |
6 | import Foundation
7 | import Rswift
8 | import UIKit
9 |
10 | /// This `R` struct is generated and contains references to static resources.
11 | struct R: Rswift.Validatable {
12 | fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap(Locale.init) ?? Locale.current
13 | fileprivate static let hostingBundle = Bundle(for: R.Class.self)
14 |
15 | static func validate() throws {
16 | try intern.validate()
17 | }
18 |
19 | /// This `R.color` struct is generated, and contains static references to 0 colors.
20 | struct color {
21 | fileprivate init() {}
22 | }
23 |
24 | /// This `R.file` struct is generated, and contains static references to 0 files.
25 | struct file {
26 | fileprivate init() {}
27 | }
28 |
29 | /// This `R.font` struct is generated, and contains static references to 0 fonts.
30 | struct font {
31 | fileprivate init() {}
32 | }
33 |
34 | /// This `R.image` struct is generated, and contains static references to 3 images.
35 | struct image {
36 | /// Image `launchImage`.
37 | static let launchImage = Rswift.ImageResource(bundle: R.hostingBundle, name: "launchImage")
38 | /// Image `notFound`.
39 | static let notFound = Rswift.ImageResource(bundle: R.hostingBundle, name: "notFound")
40 | /// Image `recipePlaceholder`.
41 | static let recipePlaceholder = Rswift.ImageResource(bundle: R.hostingBundle, name: "recipePlaceholder")
42 |
43 | /// `UIImage(named: "launchImage", bundle: ..., traitCollection: ...)`
44 | static func launchImage(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
45 | return UIKit.UIImage(resource: R.image.launchImage, compatibleWith: traitCollection)
46 | }
47 |
48 | /// `UIImage(named: "notFound", bundle: ..., traitCollection: ...)`
49 | static func notFound(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
50 | return UIKit.UIImage(resource: R.image.notFound, compatibleWith: traitCollection)
51 | }
52 |
53 | /// `UIImage(named: "recipePlaceholder", bundle: ..., traitCollection: ...)`
54 | static func recipePlaceholder(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
55 | return UIKit.UIImage(resource: R.image.recipePlaceholder, compatibleWith: traitCollection)
56 | }
57 |
58 | fileprivate init() {}
59 | }
60 |
61 | /// This `R.nib` struct is generated, and contains static references to 0 nibs.
62 | struct nib {
63 | fileprivate init() {}
64 | }
65 |
66 | /// This `R.reuseIdentifier` struct is generated, and contains static references to 0 reuse identifiers.
67 | struct reuseIdentifier {
68 | fileprivate init() {}
69 | }
70 |
71 | /// This `R.segue` struct is generated, and contains static references to 0 view controllers.
72 | struct segue {
73 | fileprivate init() {}
74 | }
75 |
76 | /// This `R.storyboard` struct is generated, and contains static references to 1 storyboards.
77 | struct storyboard {
78 | /// Storyboard `LaunchScreen`.
79 | static let launchScreen = _R.storyboard.launchScreen()
80 |
81 | /// `UIStoryboard(name: "LaunchScreen", bundle: ...)`
82 | static func launchScreen(_: Void = ()) -> UIKit.UIStoryboard {
83 | return UIKit.UIStoryboard(resource: R.storyboard.launchScreen)
84 | }
85 |
86 | fileprivate init() {}
87 | }
88 |
89 | /// This `R.string` struct is generated, and contains static references to 0 localization tables.
90 | struct string {
91 | fileprivate init() {}
92 | }
93 |
94 | fileprivate struct intern: Rswift.Validatable {
95 | fileprivate static func validate() throws {
96 | try _R.validate()
97 | }
98 |
99 | fileprivate init() {}
100 | }
101 |
102 | fileprivate class Class {}
103 |
104 | fileprivate init() {}
105 | }
106 |
107 | struct _R: Rswift.Validatable {
108 | static func validate() throws {
109 | try storyboard.validate()
110 | }
111 |
112 | struct nib {
113 | fileprivate init() {}
114 | }
115 |
116 | struct storyboard: Rswift.Validatable {
117 | static func validate() throws {
118 | try launchScreen.validate()
119 | }
120 |
121 | struct launchScreen: Rswift.StoryboardResourceWithInitialControllerType, Rswift.Validatable {
122 | typealias InitialController = UIKit.UIViewController
123 |
124 | let bundle = R.hostingBundle
125 | let name = "LaunchScreen"
126 |
127 | static func validate() throws {
128 | if UIKit.UIImage(named: "launchImage") == nil { throw Rswift.ValidationError(description: "[R.swift] Image named 'launchImage' is used in storyboard 'LaunchScreen', but couldn't be loaded.") }
129 | }
130 |
131 | fileprivate init() {}
132 | }
133 |
134 | fileprivate init() {}
135 | }
136 |
137 | fileprivate init() {}
138 | }
139 |
--------------------------------------------------------------------------------
/Source/Library/Extensions/NSLayoutConstraint+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSLayoutConstraint+Extensions.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension NSLayoutConstraint {
12 | /// Activate constraints
13 | ///
14 | /// - Parameter constraints: An array of constraints
15 | static func activate(_ constraints: [NSLayoutConstraint]) {
16 | constraints.forEach {
17 | ($0.firstItem as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
18 | $0.isActive = true
19 | }
20 | }
21 |
22 | static func pin(view: UIView, toEdgesOf anotherView: UIView) {
23 | activate([
24 | view.topAnchor.constraint(equalTo: anotherView.topAnchor),
25 | view.leftAnchor.constraint(equalTo: anotherView.leftAnchor),
26 | view.rightAnchor.constraint(equalTo: anotherView.rightAnchor),
27 | view.bottomAnchor.constraint(equalTo: anotherView.bottomAnchor)
28 | ])
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Source/Library/Extensions/UICollectionView+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UICollectionView+Extensions.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UICollectionView {
12 |
13 | /// Generic function to dequeue cell
14 | func dequeue(indexPath: IndexPath) -> Cell {
15 | // swiftlint:disable force_cast
16 | return dequeueReusableCell(withReuseIdentifier: String(describing: Cell.self), for: indexPath) as! Cell
17 | }
18 |
19 | /// Generic function to register cell
20 | func register(cellType: UICollectionViewCell.Type) {
21 | register(cellType, forCellWithReuseIdentifier: String(describing: cellType.self))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Source/Library/Extensions/UIColor+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Extensions.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIColor {
12 | /// Init color from hex string
13 | ///
14 | /// - Parameter hex: A hex string, can start with or without #
15 | convenience init(hex: String) {
16 | let hex = hex.replacingOccurrences(of: "#", with: "")
17 |
18 | // Need 6 characters
19 | guard hex.count == 6 else {
20 | self.init(white: 1.0, alpha: 1.0)
21 | return
22 | }
23 |
24 | self.init(
25 | red: CGFloat((Int(hex, radix: 16)! >> 16) & 0xFF) / 255.0,
26 | green: CGFloat((Int(hex, radix: 16)! >> 8) & 0xFF) / 255.0,
27 | blue: CGFloat((Int(hex, radix: 16)!) & 0xFF) / 255.0, alpha: 1.0
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Source/Library/Extensions/UIImageView+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageView+Extensions.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIImageView {
12 | /// Used to set image from an url
13 | ///
14 | /// - Parameter url: url for download
15 | func setImage(url: URL, placeholder: UIImage? = nil) {
16 | if imageService == nil {
17 | imageService = ImageService(networkService: NetworkService(), cacheService: CacheService())
18 | }
19 |
20 | self.image = placeholder
21 | self.imageService?.fetch(url: url, completion: { [weak self] image in
22 | self?.image = image
23 | })
24 | }
25 |
26 | /// Use for store property
27 | private var imageService: ImageService? {
28 | get {
29 | return objc_getAssociatedObject(self, &AssociateKey.imageService) as? ImageService
30 | }
31 | set {
32 | objc_setAssociatedObject(
33 | self,
34 | &AssociateKey.imageService,
35 | newValue,
36 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
37 | )
38 | }
39 | }
40 | }
41 |
42 | fileprivate struct AssociateKey {
43 | static var imageService = 0
44 | }
45 |
--------------------------------------------------------------------------------
/Source/Library/Extensions/UIView+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Extensions.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 |
13 | /// Convenient method to add a list of subviews
14 | func addSubviews(_ views: [UIView]) {
15 | views.forEach({
16 | addSubview($0)
17 | })
18 | }
19 |
20 | /// Apply mask to round corners
21 | func round(corners: UIRectCorner) {
22 | let raddi = bounds.size.height / 2
23 | let path = UIBezierPath(
24 | roundedRect: bounds,
25 | byRoundingCorners: corners,
26 | cornerRadii: CGSize(width: raddi, height: raddi)
27 | )
28 |
29 | let maskLayer = CAShapeLayer()
30 |
31 | maskLayer.path = path.cgPath
32 | layer.mask = maskLayer
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Source/Library/Extensions/UIViewController+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+Extensions.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIViewController {
12 | /// Add child view controller and its view
13 | func add(childViewController: UIViewController) {
14 | addChildViewController(childViewController)
15 | view.addSubview(childViewController.view)
16 | childViewController.didMove(toParentViewController: self)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Source/Library/Flow/AppFlowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppFlowController.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Manage app flow
12 | final class AppFlowController: UIViewController {
13 |
14 | /// Start the flow
15 | func start() {
16 | let flowController = RecipeFlowController()
17 | add(childViewController: flowController)
18 | flowController.start()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Source/Library/Flow/RecipeFlowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipeFlowController.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SafariServices
11 |
12 | /// Manage list and detail screens for recipe
13 | final class RecipeFlowController: UINavigationController {
14 | /// Start the flow
15 | func start() {
16 | let service = RecipesService(networking: NetworkService())
17 | let controller = HomeViewController(recipesService: service)
18 | viewControllers = [controller]
19 | controller.select = { [weak self] recipe in
20 | self?.startDetail(recipe: recipe)
21 | }
22 | }
23 |
24 | private func startDetail(recipe: Recipe) {
25 | let service = RecipesService(networking: NetworkService())
26 | let controller = RecipeDetailViewController(recipe: recipe, recipesService: service)
27 |
28 | controller.selectInstruction = { [weak self] url in
29 | self?.startWeb(url: url)
30 | }
31 |
32 | controller.selectOriginal = { [weak self] url in
33 | self?.startWeb(url: url)
34 | }
35 |
36 | pushViewController(controller, animated: true)
37 | }
38 |
39 | private func startWeb(url: URL) {
40 | let controller = SFSafariViewController(url: url)
41 | pushViewController(controller, animated: true)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Source/Library/Model/Recipe.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Represent a recipe from API response
4 | struct Recipe: Codable {
5 | /// Name of the Publisher
6 | let publisher: String
7 |
8 | /// Url of the recipe on Food2Fork.com
9 | let url: URL
10 |
11 | /// URL of the image
12 | let sourceUrl: URL
13 |
14 | /// id of recipe
15 | let id: String
16 |
17 | /// Title of the recipe
18 | let title: String
19 |
20 | /// URL of the image
21 | let imageUrl: URL
22 |
23 | /// The Social Ranking of the Recipe
24 | let socialRank: Double
25 |
26 | /// Base url of the publisher
27 | let publisherUrl: URL
28 |
29 | let ingredients: [String]?
30 |
31 | enum CodingKeys: String, CodingKey {
32 | case publisher
33 | case url = "f2f_url"
34 | case sourceUrl = "source_url"
35 | case id = "recipe_id"
36 | case title
37 | case imageUrl = "image_url"
38 | case socialRank = "social_rank"
39 | case publisherUrl = "publisher_url"
40 | case ingredients
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Source/Library/Networking/Networking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Networking.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol Networking {
12 |
13 | /// Fetch data from url and parameters query
14 | ///
15 | /// - Parameters:
16 | /// - url: The url
17 | /// - parameters: Parameters as query items
18 | /// - completion: Called when operation finishes
19 | /// - Returns: The data task
20 | @discardableResult func fetch(resource: Resource, completion: @escaping (Data?) -> Void) -> URLSessionTask?
21 | }
22 |
--------------------------------------------------------------------------------
/Source/Library/Networking/Resource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Resource.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // A network resource, identified by url and parameters
12 | struct Resource {
13 | let url: URL
14 | let path: String?
15 | let httpMethod: String
16 | let parameters: [String: String]
17 |
18 | init(url: URL, path: String? = nil, httpMethod: String = "GET", parameters: [String: String] = [:]) {
19 | self.url = url
20 | self.path = path
21 | self.httpMethod = httpMethod
22 | self.parameters = parameters
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Library/Service/CacheService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageService.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftHash
11 |
12 | /// Save and load data to memory and disk cache
13 | final class CacheService {
14 |
15 | /// For get or load data in memory
16 | private let memory = NSCache()
17 |
18 | /// The path url that contain cached files (mp3 files and image files)
19 | private let diskPath: URL
20 |
21 | /// For checking file or directory exists in a specified path
22 | private let fileManager: FileManager
23 |
24 | /// Make sure all operation are executed serially
25 | private let serialQueue = DispatchQueue(label: "Recipes")
26 |
27 | init(fileManager: FileManager = FileManager.default) {
28 | self.fileManager = fileManager
29 | do {
30 | let documentDirectory = try fileManager.url(
31 | for: .documentDirectory,
32 | in: .userDomainMask,
33 | appropriateFor: nil,
34 | create: true
35 | )
36 | diskPath = documentDirectory.appendingPathComponent("Recipes")
37 | try createDirectoryIfNeeded()
38 | } catch {
39 | fatalError()
40 | }
41 | }
42 |
43 | /// Save data
44 | ///
45 | /// - Parameters:
46 | /// - data: The data to save
47 | /// - key: Key to identify cached item
48 | func save(data: Data, key: String, completion: (() -> Void)? = nil) {
49 | let key = MD5(key)
50 |
51 | serialQueue.async {
52 | self.memory.setObject(data as NSData, forKey: key as NSString)
53 | do {
54 | try data.write(to: self.filePath(key: key))
55 | completion?()
56 | } catch {
57 | print(error)
58 | }
59 | }
60 | }
61 |
62 | /// Load data specified by key
63 | ///
64 | /// - Parameters:
65 | /// - key: Key to identify cached item
66 | /// - completion: Called when operation finishes
67 | func load(key: String, completion: @escaping (Data?) -> Void) {
68 | let key = MD5(key)
69 |
70 | serialQueue.async {
71 | // If object is in memory
72 | if let data = self.memory.object(forKey: key as NSString) {
73 | completion(data as Data)
74 | return
75 | }
76 |
77 | // If object is in disk
78 | if let data = try? Data(contentsOf: self.filePath(key: key)) {
79 | // Set back to memory
80 | self.memory.setObject(data as NSData, forKey: key as NSString)
81 | completion(data)
82 | return
83 | }
84 |
85 | completion(nil)
86 | }
87 | }
88 |
89 | /// Convenient method to access file named key in diskPath directory
90 | ///
91 | /// - Parameter key: The key, this is the file name
92 | /// - Returns: The file url
93 | private func filePath(key: String) -> URL {
94 | return diskPath.appendingPathComponent(key)
95 | }
96 |
97 | private func createDirectoryIfNeeded() throws {
98 | if !fileManager.fileExists(atPath: diskPath.path) {
99 | try fileManager.createDirectory(at: diskPath, withIntermediateDirectories: false, attributes: nil)
100 | }
101 | }
102 |
103 | /// Clear all items in memory and disk cache
104 | func clear() throws {
105 | memory.removeAllObjects()
106 | try fileManager.removeItem(at: diskPath)
107 | try createDirectoryIfNeeded()
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Source/Library/Service/ImageService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageService.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Check local cache and fetch remote image
12 | final class ImageService {
13 |
14 | private let networkService: Networking
15 | private let cacheService: CacheService
16 | private var task: URLSessionTask?
17 |
18 | init(networkService: Networking, cacheService: CacheService) {
19 | self.networkService = networkService
20 | self.cacheService = cacheService
21 | }
22 |
23 | /// Fetch image from url
24 | ///
25 | /// - Parameters:
26 | /// - url: The remote url for image
27 | /// - completion: Called when operation finishes
28 | func fetch(url: URL, completion: @escaping (UIImage?) -> Void) {
29 | // Cancel existing task if any
30 | task?.cancel()
31 |
32 | // Try load from cache
33 | cacheService.load(key: url.absoluteString, completion: { [weak self] cachedData in
34 | if let data = cachedData, let image = UIImage(data: data) {
35 | DispatchQueue.main.async {
36 | completion(image)
37 | }
38 | } else {
39 | // Try to request from network
40 | let resource = Resource(url: url)
41 | self?.task = self?.networkService.fetch(resource: resource, completion: { networkData in
42 | if let data = networkData, let image = UIImage(data: data) {
43 | // Save to cache
44 | self?.cacheService.save(data: data, key: url.absoluteString)
45 | DispatchQueue.main.async {
46 | completion(image)
47 | }
48 | } else {
49 | print("Error loading image at \(url)")
50 | }
51 | })
52 |
53 | self?.task?.resume()
54 | }
55 | })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Source/Library/Service/NetworkService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkService.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Used to fetch data from network
12 | final class NetworkService: Networking {
13 | private let session: URLSession
14 |
15 | init(configuration: URLSessionConfiguration = URLSessionConfiguration.default) {
16 | self.session = URLSession(configuration: configuration)
17 | }
18 |
19 | @discardableResult func fetch(resource: Resource, completion: @escaping (Data?) -> Void) -> URLSessionTask? {
20 | guard let request = makeRequest(resource: resource) else {
21 | completion(nil)
22 | return nil
23 | }
24 |
25 | let task = session.dataTask(with: request, completionHandler: { data, _, error in
26 | guard let data = data, error == nil else {
27 | completion(nil)
28 | return
29 | }
30 |
31 | completion(data)
32 | })
33 |
34 | task.resume()
35 | return task
36 | }
37 |
38 | /// Convenient method to make request
39 | ///
40 | /// - Parameters:
41 | /// - resource: Network resource
42 | /// - Returns: Constructed URL request
43 | private func makeRequest(resource: Resource) -> URLRequest? {
44 | let url = resource.path.map({ resource.url.appendingPathComponent($0) }) ?? resource.url
45 | guard var component = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
46 | assertionFailure()
47 | return nil
48 | }
49 |
50 | component.queryItems = resource.parameters.map({
51 | return URLQueryItem(name: $0, value: $1)
52 | })
53 |
54 | guard let resolvedUrl = component.url else {
55 | assertionFailure()
56 | return nil
57 | }
58 |
59 | var request = URLRequest(url: resolvedUrl)
60 | request.httpMethod = resource.httpMethod
61 | return request
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Source/Library/Service/RecipesService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecipesService.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 24.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class RecipesService {
12 | private let baseUrl = URL(string: "https://food2fork.com/api")!
13 | private let networking: Networking
14 |
15 | init(networking: Networking) {
16 | self.networking = networking
17 | }
18 |
19 | /// Fetch recipes with highest rating
20 | ///
21 | /// - Parameter completion: Called when operation finishes
22 | func fetchTopRating(completion: @escaping ([Recipe]) -> Void) {
23 | let resource = Resource(url: baseUrl, path: "search", parameters: [
24 | "key": AppConfig.apiKey
25 | ])
26 |
27 | _ = networking.fetch(resource: resource, completion: { data in
28 | DispatchQueue.main.async {
29 | completion(data.flatMap({ RecipeListResponse.make(data: $0)?.recipes }) ?? [])
30 | }
31 | })
32 | }
33 |
34 | /// Fetch single entity based on recipe id
35 | ///
36 | /// - Parameters:
37 | /// - recipeId: The recipe id
38 | /// - completion: Called when operation finishes
39 | func fetch(recipeId: String, completion: @escaping (Recipe?) -> Void) {
40 | let resource = Resource(url: baseUrl, path: "get", parameters: [
41 | "key": AppConfig.apiKey,
42 | "rId": recipeId
43 | ])
44 |
45 | _ = networking.fetch(resource: resource, completion: { data in
46 | DispatchQueue.main.async {
47 | completion(data.flatMap({ RecipeResponse.make(data: $0)?.recipe }))
48 | }
49 | })
50 | }
51 |
52 | /// Search recipes based on query
53 | ///
54 | /// - Parameters:
55 | /// - query: The search query
56 | /// - completion: Called when operation finishes
57 | /// - Returns: The network task
58 | @discardableResult func search(query: String, completion: @escaping ([Recipe]) -> Void) -> URLSessionTask? {
59 | let resource = Resource(url: baseUrl, path: "search", parameters: [
60 | "key": AppConfig.apiKey,
61 | "q": query
62 | ])
63 |
64 | return networking.fetch(resource: resource, completion: { data in
65 | DispatchQueue.main.async {
66 | completion(data.flatMap({ RecipeListResponse.make(data: $0)?.recipes }) ?? [])
67 | }
68 | })
69 | }
70 | }
71 |
72 | private class RecipeListResponse: Decodable {
73 | let count: Int
74 | let recipes: [Recipe]
75 |
76 | static func make(data: Data) -> RecipeListResponse? {
77 | return try? JSONDecoder().decode(RecipeListResponse.self, from: data)
78 | }
79 | }
80 |
81 | private class RecipeResponse: Decodable {
82 | let recipe: Recipe
83 |
84 | static func make(data: Data) -> RecipeResponse? {
85 | return try? JSONDecoder().decode(RecipeResponse.self, from: data)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Source/Library/Utils/Debouncer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThrottleHandler.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Throttle action, allow action to be performed after some delay
12 | final class Debouncer {
13 | private let delay: TimeInterval
14 | private var workItem: DispatchWorkItem?
15 |
16 | init(delay: TimeInterval) {
17 | self.delay = delay
18 | }
19 |
20 | /// Trigger the action after some delay
21 | func schedule(action: @escaping () -> Void) {
22 | workItem?.cancel()
23 | workItem = DispatchWorkItem(block: action)
24 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem!)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Source/Library/View/EmptyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyView.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Used to show when there's no data
12 | final class EmptyView: UIView {
13 | private lazy var imageView: UIImageView = self.makeImageView()
14 | private lazy var label: UILabel = self.makeLabel()
15 |
16 | required init(text: String) {
17 | super.init(frame: .zero)
18 |
19 | isUserInteractionEnabled = false
20 | addSubviews([imageView, label])
21 | label.text = text
22 | setupConstraints()
23 | }
24 |
25 | required init?(coder aDecoder: NSCoder) {
26 | fatalError()
27 | }
28 |
29 | private func setupConstraints() {
30 | NSLayoutConstraint.activate([
31 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
32 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -50),
33 | imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
34 | imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1),
35 |
36 | label.centerXAnchor.constraint(equalTo: centerXAnchor),
37 | label.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10)
38 | ])
39 | }
40 |
41 | // MARK: - Make
42 |
43 | private func makeImageView() -> UIImageView {
44 | let imageView = UIImageView()
45 | imageView.contentMode = .scaleAspectFill
46 | imageView.clipsToBounds = true
47 | imageView.image = R.image.notFound()
48 | return imageView
49 | }
50 |
51 | private func makeLabel() -> UILabel {
52 | let label = UILabel()
53 | label.textAlignment = .center
54 | label.font = UIFont.preferredFont(forTextStyle: .headline)
55 | return label
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Source/Library/View/ScrollableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollableView.swift
3 | // Recipes
4 | //
5 | // Created by Khoa Pham on 25.02.2018.
6 | // Copyright © 2018 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Vertically layout view using Auto Layout in UIScrollView
12 | final class ScrollableView: UIView {
13 | private let scrollView = UIScrollView()
14 | private let contentView = UIView()
15 |
16 | struct Pair {
17 | let view: UIView
18 | let inset: UIEdgeInsets
19 | }
20 |
21 | override init(frame: CGRect) {
22 | super.init(frame: frame)
23 |
24 | scrollView.showsHorizontalScrollIndicator = false
25 | scrollView.alwaysBounceHorizontal = false
26 | addSubview(scrollView)
27 |
28 | scrollView.addSubview(contentView)
29 |
30 | NSLayoutConstraint.activate([
31 | scrollView.topAnchor.constraint(equalTo: topAnchor),
32 | scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
33 | scrollView.leftAnchor.constraint(equalTo: leftAnchor),
34 | scrollView.rightAnchor.constraint(equalTo: rightAnchor),
35 |
36 | contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
37 | contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
38 | contentView.leftAnchor.constraint(equalTo: leftAnchor),
39 | contentView.rightAnchor.constraint(equalTo: rightAnchor)
40 | ])
41 | }
42 |
43 | required init?(coder aDecoder: NSCoder) {
44 | fatalError()
45 | }
46 |
47 | func setup(pairs: [Pair]) {
48 | pairs.enumerated().forEach({ tuple in
49 | let view = tuple.element.view
50 | let inset = tuple.element.inset
51 | let offset = tuple.offset
52 |
53 | scrollView.addSubview(view)
54 |
55 | if offset == 0 {
56 | NSLayoutConstraint.activate([
57 | view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top)
58 | ])
59 | } else {
60 | NSLayoutConstraint.activate([
61 | view.topAnchor.constraint(equalTo: pairs[offset - 1].view.bottomAnchor, constant: inset.top)
62 | ])
63 | }
64 |
65 | if offset == pairs.count - 1 {
66 | NSLayoutConstraint.activate([
67 | view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom)
68 | ])
69 | }
70 |
71 | NSLayoutConstraint.activate([
72 | view.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: inset.left),
73 | view.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -inset.right)
74 | ])
75 | })
76 | }
77 | }
78 |
--------------------------------------------------------------------------------