Browse Source

initial commit

Mike M 3 years ago
commit
8994f365b7
27 changed files with 1862 additions and 0 deletions
  1. 634 0
      LiveLikeiOSAssessmentV1.xcodeproj/project.pbxproj
  2. 7 0
      LiveLikeiOSAssessmentV1.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  3. 8 0
      LiveLikeiOSAssessmentV1.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  4. BIN
      LiveLikeiOSAssessmentV1.xcodeproj/project.xcworkspace/xcuserdata/mike.xcuserdatad/UserInterfaceState.xcuserstate
  5. 98 0
      LiveLikeiOSAssessmentV1.xcodeproj/xcshareddata/xcschemes/LiveLikeiOSAssessmentV1.xcscheme
  6. 27 0
      LiveLikeiOSAssessmentV1.xcodeproj/xcuserdata/mike.xcuserdatad/xcschemes/xcschememanagement.plist
  7. 34 0
      LiveLikeiOSAssessmentV1/AppDelegate.swift
  8. 11 0
      LiveLikeiOSAssessmentV1/Assets.xcassets/AccentColor.colorset/Contents.json
  9. 98 0
      LiveLikeiOSAssessmentV1/Assets.xcassets/AppIcon.appiconset/Contents.json
  10. 6 0
      LiveLikeiOSAssessmentV1/Assets.xcassets/Contents.json
  11. 25 0
      LiveLikeiOSAssessmentV1/Base.lproj/LaunchScreen.storyboard
  12. 19 0
      LiveLikeiOSAssessmentV1/Constants.swift
  13. 102 0
      LiveLikeiOSAssessmentV1/GiphyApi/GiphyApiManager.swift
  14. 23 0
      LiveLikeiOSAssessmentV1/GiphyApi/Models/GifImage.swift
  15. 36 0
      LiveLikeiOSAssessmentV1/GiphyApi/Models/GifListPagination.swift
  16. 12 0
      LiveLikeiOSAssessmentV1/GiphyApi/Models/GifObject.swift
  17. 11 0
      LiveLikeiOSAssessmentV1/GiphyApi/Response/GiphyListResponse.swift
  18. 23 0
      LiveLikeiOSAssessmentV1/Info.plist
  19. 55 0
      LiveLikeiOSAssessmentV1/SceneDelegate.swift
  20. 65 0
      LiveLikeiOSAssessmentV1/UIElements/CollectionViewCells/GifCollectionViewCell/GifCollectionViewCell.swift
  21. 22 0
      LiveLikeiOSAssessmentV1/UIElements/CollectionViewFlows/GifListCollectionViewFlowLayout.swift
  22. 67 0
      LiveLikeiOSAssessmentV1/UIElements/ImageViews/LLAnimatableImageView.swift
  23. 89 0
      LiveLikeiOSAssessmentV1/Utilities/ImageChache/LLCache.swift
  24. 153 0
      LiveLikeiOSAssessmentV1/ViewControllers/Presenters/GifListPresenter.swift
  25. 177 0
      LiveLikeiOSAssessmentV1/ViewControllers/Views/GifListViewController.swift
  26. 29 0
      LiveLikeiOSAssessmentV1Tests/LiveLikeGiphyChallengeTests.swift
  27. 31 0
      README.md

+ 634 - 0
LiveLikeiOSAssessmentV1.xcodeproj/project.pbxproj

@@ -0,0 +1,634 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 55;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		2707607F276409C000064F59 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2707607E276409C000064F59 /* AppDelegate.swift */; };
+		27076081276409C000064F59 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27076080276409C000064F59 /* SceneDelegate.swift */; };
+		27076088276409C200064F59 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 27076087276409C200064F59 /* Assets.xcassets */; };
+		2707608B276409C200064F59 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 27076089276409C200064F59 /* LaunchScreen.storyboard */; };
+		27076096276409C300064F59 /* LiveLikeGiphyChallengeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27076095276409C300064F59 /* LiveLikeGiphyChallengeTests.swift */; };
+		5F35B00527A6B6E7002C1111 /* GifListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B00427A6B6E7002C1111 /* GifListViewController.swift */; };
+		5F35B00727A6B700002C1111 /* GifListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B00627A6B700002C1111 /* GifListPresenter.swift */; };
+		5F35B01027A6BEF1002C1111 /* GifObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B00F27A6BEF1002C1111 /* GifObject.swift */; };
+		5F35B01427A6BF76002C1111 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B01327A6BF76002C1111 /* Constants.swift */; };
+		5F35B01727A6C041002C1111 /* GiphyApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B01627A6C041002C1111 /* GiphyApiManager.swift */; };
+		5F35B01B27A6C0C8002C1111 /* GifListPagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B01A27A6C0C8002C1111 /* GifListPagination.swift */; };
+		5F35B01D27A6C693002C1111 /* GiphyListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B01C27A6C693002C1111 /* GiphyListResponse.swift */; };
+		5F35B02127A6E3BB002C1111 /* GifCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B02027A6E3BB002C1111 /* GifCollectionViewCell.swift */; };
+		5F35B02327A6F257002C1111 /* GifImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F35B02227A6F257002C1111 /* GifImage.swift */; };
+		5F84EADF27A7136B0008CA2B /* LLCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F84EADE27A7136B0008CA2B /* LLCache.swift */; };
+		5F84EAE427A721980008CA2B /* LLAnimatableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F84EAE327A721980008CA2B /* LLAnimatableImageView.swift */; };
+		5F84EAE627A880540008CA2B /* GifListCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F84EAE527A880540008CA2B /* GifListCollectionViewFlowLayout.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		27076092276409C300064F59 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 27076073276409C000064F59 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 2707607A276409C000064F59;
+			remoteInfo = LiveLikeGiphyChallenge;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		2707607B276409C000064F59 /* LiveLikeiOSAssessmentV1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveLikeiOSAssessmentV1.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		2707607E276409C000064F59 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		27076080276409C000064F59 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
+		27076087276409C200064F59 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		2707608A276409C200064F59 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		2707608C276409C200064F59 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		27076091276409C300064F59 /* LiveLikeiOSAssessmentV1Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LiveLikeiOSAssessmentV1Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		27076095276409C300064F59 /* LiveLikeGiphyChallengeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLikeGiphyChallengeTests.swift; sourceTree = "<group>"; };
+		5F35B00427A6B6E7002C1111 /* GifListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifListViewController.swift; sourceTree = "<group>"; };
+		5F35B00627A6B700002C1111 /* GifListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifListPresenter.swift; sourceTree = "<group>"; };
+		5F35B00F27A6BEF1002C1111 /* GifObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifObject.swift; sourceTree = "<group>"; };
+		5F35B01327A6BF76002C1111 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
+		5F35B01627A6C041002C1111 /* GiphyApiManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiphyApiManager.swift; sourceTree = "<group>"; };
+		5F35B01A27A6C0C8002C1111 /* GifListPagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifListPagination.swift; sourceTree = "<group>"; };
+		5F35B01C27A6C693002C1111 /* GiphyListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiphyListResponse.swift; sourceTree = "<group>"; };
+		5F35B02027A6E3BB002C1111 /* GifCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifCollectionViewCell.swift; sourceTree = "<group>"; };
+		5F35B02227A6F257002C1111 /* GifImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifImage.swift; sourceTree = "<group>"; };
+		5F84EADE27A7136B0008CA2B /* LLCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLCache.swift; sourceTree = "<group>"; };
+		5F84EAE327A721980008CA2B /* LLAnimatableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLAnimatableImageView.swift; sourceTree = "<group>"; };
+		5F84EAE527A880540008CA2B /* GifListCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifListCollectionViewFlowLayout.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		27076078276409C000064F59 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2707608E276409C300064F59 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		27076072276409C000064F59 = {
+			isa = PBXGroup;
+			children = (
+				2707607D276409C000064F59 /* LiveLikeiOSAssessmentV1 */,
+				27076094276409C300064F59 /* LiveLikeiOSAssessmentV1Tests */,
+				2707607C276409C000064F59 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		2707607C276409C000064F59 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				2707607B276409C000064F59 /* LiveLikeiOSAssessmentV1.app */,
+				27076091276409C300064F59 /* LiveLikeiOSAssessmentV1Tests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		2707607D276409C000064F59 /* LiveLikeiOSAssessmentV1 */ = {
+			isa = PBXGroup;
+			children = (
+				2707607E276409C000064F59 /* AppDelegate.swift */,
+				27076080276409C000064F59 /* SceneDelegate.swift */,
+				5F35B00C27A6BAB6002C1111 /* ViewControllers */,
+				5F35AFFF27A6B659002C1111 /* UIElements */,
+				5F35B00027A6B668002C1111 /* GiphyApi */,
+				5F35B00827A6BA60002C1111 /* Utilities */,
+				27076089276409C200064F59 /* LaunchScreen.storyboard */,
+				5F35B01327A6BF76002C1111 /* Constants.swift */,
+				27076087276409C200064F59 /* Assets.xcassets */,
+				2707608C276409C200064F59 /* Info.plist */,
+			);
+			path = LiveLikeiOSAssessmentV1;
+			sourceTree = "<group>";
+		};
+		27076094276409C300064F59 /* LiveLikeiOSAssessmentV1Tests */ = {
+			isa = PBXGroup;
+			children = (
+				27076095276409C300064F59 /* LiveLikeGiphyChallengeTests.swift */,
+			);
+			path = LiveLikeiOSAssessmentV1Tests;
+			sourceTree = "<group>";
+		};
+		5F35AFFD27A6B646002C1111 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B00427A6B6E7002C1111 /* GifListViewController.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
+		5F35AFFE27A6B64E002C1111 /* Presenters */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B00627A6B700002C1111 /* GifListPresenter.swift */,
+			);
+			path = Presenters;
+			sourceTree = "<group>";
+		};
+		5F35AFFF27A6B659002C1111 /* UIElements */ = {
+			isa = PBXGroup;
+			children = (
+				5F84EAE227A7217C0008CA2B /* ImageViews */,
+				5F35B02427A6F590002C1111 /* CollectionViewFlows */,
+				5F35B01E27A6E38A002C1111 /* CollectionViewCells */,
+			);
+			path = UIElements;
+			sourceTree = "<group>";
+		};
+		5F35B00027A6B668002C1111 /* GiphyApi */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B00E27A6BED0002C1111 /* Models */,
+				5F35B00D27A6BEC4002C1111 /* Response */,
+				5F35B01627A6C041002C1111 /* GiphyApiManager.swift */,
+			);
+			path = GiphyApi;
+			sourceTree = "<group>";
+		};
+		5F35B00827A6BA60002C1111 /* Utilities */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B00927A6BA75002C1111 /* ImageChache */,
+			);
+			path = Utilities;
+			sourceTree = "<group>";
+		};
+		5F35B00927A6BA75002C1111 /* ImageChache */ = {
+			isa = PBXGroup;
+			children = (
+				5F84EADE27A7136B0008CA2B /* LLCache.swift */,
+			);
+			path = ImageChache;
+			sourceTree = "<group>";
+		};
+		5F35B00C27A6BAB6002C1111 /* ViewControllers */ = {
+			isa = PBXGroup;
+			children = (
+				5F35AFFD27A6B646002C1111 /* Views */,
+				5F35AFFE27A6B64E002C1111 /* Presenters */,
+			);
+			path = ViewControllers;
+			sourceTree = "<group>";
+		};
+		5F35B00D27A6BEC4002C1111 /* Response */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B01C27A6C693002C1111 /* GiphyListResponse.swift */,
+			);
+			path = Response;
+			sourceTree = "<group>";
+		};
+		5F35B00E27A6BED0002C1111 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B02227A6F257002C1111 /* GifImage.swift */,
+				5F35B00F27A6BEF1002C1111 /* GifObject.swift */,
+				5F35B01A27A6C0C8002C1111 /* GifListPagination.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
+		5F35B01E27A6E38A002C1111 /* CollectionViewCells */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B01F27A6E395002C1111 /* GifCollectionViewCell */,
+			);
+			path = CollectionViewCells;
+			sourceTree = "<group>";
+		};
+		5F35B01F27A6E395002C1111 /* GifCollectionViewCell */ = {
+			isa = PBXGroup;
+			children = (
+				5F35B02027A6E3BB002C1111 /* GifCollectionViewCell.swift */,
+			);
+			path = GifCollectionViewCell;
+			sourceTree = "<group>";
+		};
+		5F35B02427A6F590002C1111 /* CollectionViewFlows */ = {
+			isa = PBXGroup;
+			children = (
+				5F84EAE527A880540008CA2B /* GifListCollectionViewFlowLayout.swift */,
+			);
+			path = CollectionViewFlows;
+			sourceTree = "<group>";
+		};
+		5F84EAE227A7217C0008CA2B /* ImageViews */ = {
+			isa = PBXGroup;
+			children = (
+				5F84EAE327A721980008CA2B /* LLAnimatableImageView.swift */,
+			);
+			path = ImageViews;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		2707607A276409C000064F59 /* LiveLikeiOSAssessmentV1 */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 270760A5276409C300064F59 /* Build configuration list for PBXNativeTarget "LiveLikeiOSAssessmentV1" */;
+			buildPhases = (
+				27076077276409C000064F59 /* Sources */,
+				27076078276409C000064F59 /* Frameworks */,
+				27076079276409C000064F59 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = LiveLikeiOSAssessmentV1;
+			productName = LiveLikeGiphyChallenge;
+			productReference = 2707607B276409C000064F59 /* LiveLikeiOSAssessmentV1.app */;
+			productType = "com.apple.product-type.application";
+		};
+		27076090276409C300064F59 /* LiveLikeiOSAssessmentV1Tests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 270760A8276409C300064F59 /* Build configuration list for PBXNativeTarget "LiveLikeiOSAssessmentV1Tests" */;
+			buildPhases = (
+				2707608D276409C300064F59 /* Sources */,
+				2707608E276409C300064F59 /* Frameworks */,
+				2707608F276409C300064F59 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				27076093276409C300064F59 /* PBXTargetDependency */,
+			);
+			name = LiveLikeiOSAssessmentV1Tests;
+			productName = LiveLikeGiphyChallengeTests;
+			productReference = 27076091276409C300064F59 /* LiveLikeiOSAssessmentV1Tests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		27076073276409C000064F59 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1310;
+				LastUpgradeCheck = 1310;
+				TargetAttributes = {
+					2707607A276409C000064F59 = {
+						CreatedOnToolsVersion = 13.1;
+					};
+					27076090276409C300064F59 = {
+						CreatedOnToolsVersion = 13.1;
+						TestTargetID = 2707607A276409C000064F59;
+					};
+				};
+			};
+			buildConfigurationList = 27076076276409C000064F59 /* Build configuration list for PBXProject "LiveLikeiOSAssessmentV1" */;
+			compatibilityVersion = "Xcode 13.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 27076072276409C000064F59;
+			productRefGroup = 2707607C276409C000064F59 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				2707607A276409C000064F59 /* LiveLikeiOSAssessmentV1 */,
+				27076090276409C300064F59 /* LiveLikeiOSAssessmentV1Tests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		27076079276409C000064F59 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2707608B276409C200064F59 /* LaunchScreen.storyboard in Resources */,
+				27076088276409C200064F59 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2707608F276409C300064F59 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		27076077276409C000064F59 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2707607F276409C000064F59 /* AppDelegate.swift in Sources */,
+				5F35B01027A6BEF1002C1111 /* GifObject.swift in Sources */,
+				5F35B02127A6E3BB002C1111 /* GifCollectionViewCell.swift in Sources */,
+				27076081276409C000064F59 /* SceneDelegate.swift in Sources */,
+				5F35B01B27A6C0C8002C1111 /* GifListPagination.swift in Sources */,
+				5F84EAE627A880540008CA2B /* GifListCollectionViewFlowLayout.swift in Sources */,
+				5F35B00527A6B6E7002C1111 /* GifListViewController.swift in Sources */,
+				5F35B02327A6F257002C1111 /* GifImage.swift in Sources */,
+				5F84EADF27A7136B0008CA2B /* LLCache.swift in Sources */,
+				5F35B01D27A6C693002C1111 /* GiphyListResponse.swift in Sources */,
+				5F35B00727A6B700002C1111 /* GifListPresenter.swift in Sources */,
+				5F35B01727A6C041002C1111 /* GiphyApiManager.swift in Sources */,
+				5F84EAE427A721980008CA2B /* LLAnimatableImageView.swift in Sources */,
+				5F35B01427A6BF76002C1111 /* Constants.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		2707608D276409C300064F59 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				27076096276409C300064F59 /* LiveLikeGiphyChallengeTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		27076093276409C300064F59 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 2707607A276409C000064F59 /* LiveLikeiOSAssessmentV1 */;
+			targetProxy = 27076092276409C300064F59 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		27076089276409C200064F59 /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				2707608A276409C200064F59 /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		270760A3276409C300064F59 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		270760A4276409C300064F59 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		270760A6276409C300064F59 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "";
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = LiveLikeiOSAssessmentV1/Info.plist;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.livelike.iOSAssessmentV1;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 1;
+			};
+			name = Debug;
+		};
+		270760A7276409C300064F59 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "";
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = LiveLikeiOSAssessmentV1/Info.plist;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.livelike.iOSAssessmentV1;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 1;
+			};
+			name = Release;
+		};
+		270760A9276409C300064F59 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.livelike.LiveLikeGiphyChallengeTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LiveLikeiOSAssessmentV1.app/LiveLikeiOSAssessmentV1";
+			};
+			name = Debug;
+		};
+		270760AA276409C300064F59 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.livelike.LiveLikeGiphyChallengeTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LiveLikeiOSAssessmentV1.app/LiveLikeiOSAssessmentV1";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		27076076276409C000064F59 /* Build configuration list for PBXProject "LiveLikeiOSAssessmentV1" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				270760A3276409C300064F59 /* Debug */,
+				270760A4276409C300064F59 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		270760A5276409C300064F59 /* Build configuration list for PBXNativeTarget "LiveLikeiOSAssessmentV1" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				270760A6276409C300064F59 /* Debug */,
+				270760A7276409C300064F59 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		270760A8276409C300064F59 /* Build configuration list for PBXNativeTarget "LiveLikeiOSAssessmentV1Tests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				270760A9276409C300064F59 /* Debug */,
+				270760AA276409C300064F59 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 27076073276409C000064F59 /* Project object */;
+}

+ 7 - 0
LiveLikeiOSAssessmentV1.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:/Users/mike/Documents/LiveLike/iOS/iOSAssessmentV1/LiveLikeiOSAssessmentV1.xcodeproj">
+   </FileRef>
+</Workspace>

+ 8 - 0
LiveLikeiOSAssessmentV1.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

BIN
LiveLikeiOSAssessmentV1.xcodeproj/project.xcworkspace/xcuserdata/mike.xcuserdatad/UserInterfaceState.xcuserstate


+ 98 - 0
LiveLikeiOSAssessmentV1.xcodeproj/xcshareddata/xcschemes/LiveLikeiOSAssessmentV1.xcscheme

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1310"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "2707607A276409C000064F59"
+               BuildableName = "LiveLikeiOSAssessmentV1.app"
+               BlueprintName = "LiveLikeiOSAssessmentV1"
+               ReferencedContainer = "container:LiveLikeiOSAssessmentV1.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "27076090276409C300064F59"
+               BuildableName = "LiveLikeiOSAssessmentV1Tests.xctest"
+               BlueprintName = "LiveLikeiOSAssessmentV1Tests"
+               ReferencedContainer = "container:LiveLikeiOSAssessmentV1.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "2707609A276409C300064F59"
+               BuildableName = "LiveLikeGiphyChallengeUITests.xctest"
+               BlueprintName = "LiveLikeiOSAssessmentV1UITests"
+               ReferencedContainer = "container:LiveLikeiOSAssessmentV1.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "2707607A276409C000064F59"
+            BuildableName = "LiveLikeiOSAssessmentV1.app"
+            BlueprintName = "LiveLikeiOSAssessmentV1"
+            ReferencedContainer = "container:LiveLikeiOSAssessmentV1.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "2707607A276409C000064F59"
+            BuildableName = "LiveLikeiOSAssessmentV1.app"
+            BlueprintName = "LiveLikeiOSAssessmentV1"
+            ReferencedContainer = "container:LiveLikeiOSAssessmentV1.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 27 - 0
LiveLikeiOSAssessmentV1.xcodeproj/xcuserdata/mike.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>LiveLikeiOSAssessmentV1.xcscheme_^#shared#^_</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>2707607A276409C000064F59</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+		<key>27076090276409C300064F59</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 34 - 0
LiveLikeiOSAssessmentV1/AppDelegate.swift

@@ -0,0 +1,34 @@
+//
+//  AppDelegate.swift
+//
+//
+
+import UIKit
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+
+
+    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+        // Override point for customization after application launch.
+        return true
+    }
+
+    // MARK: UISceneSession Lifecycle
+
+    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+        // Called when a new scene session is being created.
+        // Use this method to select a configuration to create the new scene with.
+        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+    }
+
+    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
+        // Called when the user discards a scene session.
+        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
+        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
+    }
+
+
+}
+

+ 11 - 0
LiveLikeiOSAssessmentV1/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 98 - 0
LiveLikeiOSAssessmentV1/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,98 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "60x60"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "60x60"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "76x76"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "76x76"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "83.5x83.5"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "scale" : "1x",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
LiveLikeiOSAssessmentV1/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 25 - 0
LiveLikeiOSAssessmentV1/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 19 - 0
LiveLikeiOSAssessmentV1/Constants.swift

@@ -0,0 +1,19 @@
+//
+//  Constants.swift
+//  LiveLikeGiphyChallenge
+//
+//  Created by Ljupco Nastevski on 30.1.22.
+//
+
+import Foundation
+
+
+struct Constants {
+    
+    static let GiphyApiKey = "JUMJggLCBhTudfXShZ6fsFED1l4WV7Wa"
+    
+    static let GiphyPaginationLimit = 50
+    
+    static let GiphyBaseUrl = "https://api.giphy.com"
+
+}

+ 102 - 0
LiveLikeiOSAssessmentV1/GiphyApi/GiphyApiManager.swift

@@ -0,0 +1,102 @@
+//
+//  GiphyApiManager.swift
+
+import Foundation
+
+enum GiphyApiError: Error {
+    case canceledByUser
+    case requestFailed
+    case decodingFailed
+}
+
+fileprivate struct RequestParamKeys {
+    static let ApiKey = "api_key"
+    static let QueryKey = "q"
+    static let OffsetKey = "offset"
+    static let LimitKey = "limit"
+}
+
+fileprivate struct Versions {
+    static let v1 = "/v1"
+}
+
+fileprivate struct Endpoints {
+    
+    static let Gifs = Versions.v1 + "/gifs"
+    
+    static let Trending = Gifs + "/trending"
+    static let Search = Gifs + "/search"
+    
+}
+
+
+class GiphyApiManager {
+    
+    static let shared = GiphyApiManager()
+    
+    private var baseUrl = URLComponents(string: Constants.GiphyBaseUrl)!
+        
+    private let session = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: nil)
+    
+    // Last searchable task
+    private var lastSearchableTask: URLSessionDataTask?
+    
+    init() {
+        
+        // Default query items
+        baseUrl.queryItems = [
+            URLQueryItem(name: RequestParamKeys.ApiKey, value: Constants.GiphyApiKey),
+            URLQueryItem(name: RequestParamKeys.LimitKey, value: String(Constants.GiphyPaginationLimit))
+        ]
+        
+    }
+    
+    func cancelLastSearchableTask() {
+        lastSearchableTask?.cancel()
+        lastSearchableTask = nil
+    }
+    
+    func cancelDataTaskForUrl(url: URL) {
+        
+        session.getAllTasks { tasks in
+              tasks.filter { $0.state == .running }.filter { $0.originalRequest?.url == url}.first?.cancel()
+            }
+        
+    }
+    
+    func getTrendingGifs(offset: Int, completion: @escaping (Result<GiphyListResponse, Error>) -> Void) {
+        
+        let offset = URLQueryItem(name: RequestParamKeys.OffsetKey, value: String(offset))
+        self.lastSearchableTask = self.getCodable(endpoint: Endpoints.Trending, queries: [offset], completion: completion)
+    }
+    
+    func getSearchGifs(offset: Int, query: String, completion: @escaping (Result<GiphyListResponse, Error>) -> Void) {
+        
+        let offset = URLQueryItem(name: RequestParamKeys.OffsetKey, value: String(offset))
+        let query = URLQueryItem(name: RequestParamKeys.QueryKey, value: query)
+
+        self.lastSearchableTask = self.getCodable(endpoint: Endpoints.Search, queries: [offset, query], completion: completion)
+    }
+    
+    func getData(url: URL, completion: @escaping (Data?) -> Void) -> URLSessionDataTask {
+        let task = session.dataTask(with: url) { data, _ , _ in
+            completion(data)
+        }
+        task.resume()
+        return task
+    }
+
+
+    private func getCodable<U:Codable>(
+        endpoint: String,
+        queries: [URLQueryItem]? = nil,
+        completion: @escaping (Result<U, Error>) -> Void
+    ) -> URLSessionDataTask {
+        
+        // TODO: Build a network request
+        // Build a network request using the `self.baseUrl` that receives data, decodes it or
+        // returns a `GiphyApiError.decodingFailed` error
+        
+    }
+
+}

+ 23 - 0
LiveLikeiOSAssessmentV1/GiphyApi/Models/GifImage.swift

@@ -0,0 +1,23 @@
+//
+//  GifImage.swift
+
+import Foundation
+
+struct GifImage: Codable {
+    
+    let url: String
+    let width: String
+    let height: String
+    let size: String?
+
+}
+
+struct GifImages: Codable {
+    
+    let fixedWidth: GifImage
+
+    enum CodingKeys: String, CodingKey {
+        case fixedWidth = "fixed_width"
+    }
+    
+}

+ 36 - 0
LiveLikeiOSAssessmentV1/GiphyApi/Models/GifListPagination.swift

@@ -0,0 +1,36 @@
+//
+//  GifListPagination.swift
+
+import Foundation
+
+struct GifListPagination: Codable {
+    
+    let offset: Int
+    let totalCount: Int
+    let count: Int
+    
+    var nextOffset: Int {
+        return offset + count
+    }
+    
+    var canLoadMore: Bool {
+        return offset == 0 || self.count == Constants.GiphyPaginationLimit
+    }
+
+    init() {
+        offset = 0
+        totalCount = 0
+        count = 0
+    }
+    
+}
+
+extension GifListPagination {
+    
+    enum CodingKeys: String, CodingKey {
+        case offset = "offset"
+        case totalCount = "total_count"
+        case count = "count"
+    }
+    
+}

+ 12 - 0
LiveLikeiOSAssessmentV1/GiphyApi/Models/GifObject.swift

@@ -0,0 +1,12 @@
+//
+//  GifObject.swift
+
+import Foundation
+
+struct GifObject: Codable {
+    
+    let id: String
+    let title: String
+    let images: GifImages
+    
+}

+ 11 - 0
LiveLikeiOSAssessmentV1/GiphyApi/Response/GiphyListResponse.swift

@@ -0,0 +1,11 @@
+//
+//  GiphyListResponse.swift
+
+import Foundation
+
+struct GiphyListResponse: Codable {
+    
+    let data: [GifObject]
+    let pagination: GifListPagination
+    
+}

+ 23 - 0
LiveLikeiOSAssessmentV1/Info.plist

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>UIApplicationSceneManifest</key>
+	<dict>
+		<key>UIApplicationSupportsMultipleScenes</key>
+		<false/>
+		<key>UISceneConfigurations</key>
+		<dict>
+			<key>UIWindowSceneSessionRoleApplication</key>
+			<array>
+				<dict>
+					<key>UISceneConfigurationName</key>
+					<string>Default Configuration</string>
+					<key>UISceneDelegateClassName</key>
+					<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
+				</dict>
+			</array>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 55 - 0
LiveLikeiOSAssessmentV1/SceneDelegate.swift

@@ -0,0 +1,55 @@
+//
+//  SceneDelegate.swift
+//
+
+import UIKit
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+
+    var window: UIWindow?
+
+
+    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
+        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
+        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
+        guard let _ = (scene as? UIWindowScene) else { return }
+        
+        guard let windowScene = (scene as? UIWindowScene) else { return }
+                window = UIWindow(frame: windowScene.coordinateSpace.bounds)
+                window?.windowScene = windowScene
+                window?.rootViewController = UINavigationController(rootViewController: GifListViewController())
+                window?.makeKeyAndVisible()
+    }
+
+    func sceneDidDisconnect(_ scene: UIScene) {
+        // Called as the scene is being released by the system.
+        // This occurs shortly after the scene enters the background, or when its session is discarded.
+        // Release any resources associated with this scene that can be re-created the next time the scene connects.
+        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
+    }
+
+    func sceneDidBecomeActive(_ scene: UIScene) {
+        // Called when the scene has moved from an inactive state to an active state.
+        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+    }
+
+    func sceneWillResignActive(_ scene: UIScene) {
+        // Called when the scene will move from an active state to an inactive state.
+        // This may occur due to temporary interruptions (ex. an incoming phone call).
+    }
+
+    func sceneWillEnterForeground(_ scene: UIScene) {
+        // Called as the scene transitions from the background to the foreground.
+        // Use this method to undo the changes made on entering the background.
+    }
+
+    func sceneDidEnterBackground(_ scene: UIScene) {
+        // Called as the scene transitions from the foreground to the background.
+        // Use this method to save data, release shared resources, and store enough scene-specific state information
+        // to restore the scene back to its current state.
+    }
+
+
+}
+

+ 65 - 0
LiveLikeiOSAssessmentV1/UIElements/CollectionViewCells/GifCollectionViewCell/GifCollectionViewCell.swift

@@ -0,0 +1,65 @@
+//
+//  GifCollectionViewCell.swift
+
+import UIKit
+
+class GifCollectionViewCell: UICollectionViewCell {
+    
+    static let reuseIdentifier = NSStringFromClass(GifCollectionViewCell.self)
+    
+    private lazy var imageView: LLAnimatableImageView = {
+        let imageView = LLAnimatableImageView()
+        imageView.contentMode = .center
+        imageView.clipsToBounds = true
+        return imageView
+    }()
+    
+    private lazy var titleLabel: UILabel = {
+        let lbl = UILabel()
+        lbl.numberOfLines = 0
+        return lbl
+    }()
+    
+    var gif: GifObject? {
+        didSet{
+            self.didSetGif()
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.setupSelf()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+        
+    private func setupSelf() {
+        
+        self.contentView.addSubview(self.titleLabel)
+        
+        // TODO: Add an Image to the UICollectionViewCell
+        // Use `self.imageView` variable to add an image to the cell.
+        // Please make the image height and width conform to the cell's bounds
+        
+        self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        NSLayoutConstraint.activate([
+            self.titleLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
+            self.titleLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
+            self.titleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
+            self.titleLabel.heightAnchor.constraint(lessThanOrEqualTo: self.contentView.heightAnchor, multiplier: 0.5)
+        ])
+    }
+    
+    private func didSetGif() {
+        
+        if let gif = gif {
+            self.titleLabel.text = gif.title
+            self.imageView.loadGifFromUrl(urlString: gif.images.fixedWidth.url)
+        }
+        
+    }
+    
+}

+ 22 - 0
LiveLikeiOSAssessmentV1/UIElements/CollectionViewFlows/GifListCollectionViewFlowLayout.swift

@@ -0,0 +1,22 @@
+//
+//  GifListCollectionViewFlowLayout.swift
+
+import UIKit
+
+class GifListCollectionViewFlowLayout: UICollectionViewFlowLayout {
+    
+    override func prepare() {
+        
+        super.prepare()
+
+        guard let collectionView = collectionView else { return }
+        
+        let availableWidth = collectionView.bounds.inset(by: collectionView.layoutMargins).width
+        let cellWidth = (availableWidth / 2).rounded(.down)
+        
+        self.itemSize = CGSize(width: cellWidth, height: cellWidth)
+        self.sectionInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
+
+    }
+    
+}

+ 67 - 0
LiveLikeiOSAssessmentV1/UIElements/ImageViews/LLAnimatableImageView.swift

@@ -0,0 +1,67 @@
+//
+//  LLAnimatableImageView.swift
+
+import UIKit
+
+class LLAnimatableImageView: UIImageView {
+    
+    var cache = LLCache.shared
+    
+    private var url: URL?
+    
+    private var worker: DispatchWorkItem?
+    
+    private var activityIndicator: UIActivityIndicatorView = {
+        let ai = UIActivityIndicatorView()
+        ai.translatesAutoresizingMaskIntoConstraints = false
+        ai.hidesWhenStopped = true
+        return ai
+    }()
+    
+    init() {
+        super.init(frame: .zero)
+        
+        self.addSubview(activityIndicator)
+        
+        NSLayoutConstraint.activate([
+            activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor),
+            activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor)
+        ])
+        
+        
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    
+    func loadGifFromUrl(urlString: String) {
+        
+        guard let url = URL(string: urlString) else { return }
+        self.url = url
+        
+        self.cancelAndClear()
+        
+        self.cache.retreiveDataFromUrl(url: url) {[weak self] gifImage in
+            
+            gifImage?.prepareForDisplay(completionHandler: { preparedImage in
+                DispatchQueue.main.async {
+                    self?.activityIndicator.stopAnimating()
+                    self?.image = preparedImage
+                }
+            })
+        }
+
+    }
+
+    /// Sets the current image to nil
+    func cancelAndClear() {
+        
+        self.activityIndicator.startAnimating()
+        self.image = nil
+        cache.cancelTaskFor(url: url!)
+
+    }
+    
+}

+ 89 - 0
LiveLikeiOSAssessmentV1/Utilities/ImageChache/LLCache.swift

@@ -0,0 +1,89 @@
+//
+//  LLCache.swift
+
+import Foundation
+import UIKit
+
+class LLCache: NSCache <NSString, UIImage> {
+        
+    static let shared: LLCache = LLCache()
+    
+    weak var activeTask: URLSessionDataTask?
+        
+    override init() {
+        super.init()
+        self.name = "com.livelike.gif_cache"
+    }
+    
+    
+    func retreiveDataFromUrl(url: URL, completion: @escaping (UIImage?) -> Void) {
+        
+        let path = url.path
+        
+        if let stored = self.object(forKey: path as NSString) {
+            completion(stored)
+            return
+        }
+        
+
+        activeTask = GiphyApiManager.shared.getData(url: url) { [weak self] data in
+
+            var preparedImage: UIImage?
+
+            if let data = data {
+                preparedImage = self?.prepareData(data: data)
+
+                if preparedImage != nil {
+                    self?.setObject(preparedImage!, forKey: path as NSString)
+                }
+
+            }
+
+            completion(preparedImage)
+
+        }
+                
+    }
+
+    func cancelTaskFor(url: URL) {
+        GiphyApiManager.shared.cancelDataTaskForUrl(url: url)
+    }
+    
+    /*!
+        Prepare the gif before saving it.
+        The loading will be faster.
+     */
+    private func prepareData(data: Data) -> UIImage? {
+        
+        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
+
+        var images = [UIImage]()
+        let imageCount = CGImageSourceGetCount(source)
+        
+        for i in 0 ..< imageCount {
+            
+            if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
+                let img = self.drawInContext(image: UIImage(cgImage: image))
+                images.append(img)
+                
+            }
+        }
+
+        let animatedImage = UIImage.animatedImage(with: images, duration: TimeInterval(images.count * (1/15)))
+
+        return animatedImage
+        
+    }
+    
+    private func drawInContext(image: UIImage) -> UIImage {
+        
+        UIGraphicsBeginImageContextWithOptions(image.size, true, 1)
+        image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
+        let newImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return newImage!
+    }
+
+    
+}

+ 153 - 0
LiveLikeiOSAssessmentV1/ViewControllers/Presenters/GifListPresenter.swift

@@ -0,0 +1,153 @@
+//
+//  GifListPresenter.swift
+
+import Foundation
+
+
+/*!
+    GifListPresenters delegate
+ */
+protocol GifListPresenterDelegate: AnyObject {
+    
+    /// Called before making a call on an empty data
+    func presenterWillStartLoadingForEmptyData()
+    
+    /// Called when inital data is loaded
+    func presenterDidReceiveInitialData()
+    
+    /// Called when next page is loaded
+    func presenterDidReceiveAdditionalData(at indexPaths: [IndexPath])
+
+    
+}
+
+/*!
+    Presenter that loads the trending page,
+    if a search keyword is present, it uses that keyword to
+    search the Giphy library
+ */
+class GifListPresenter {
+    
+    weak var delegate: GifListPresenterDelegate?
+
+    /*!
+        Last searched term
+        On search term change, reset the pagination
+     */
+    private var lastSearchedTerm: String = "" {
+        willSet {
+            if newValue != lastSearchedTerm {
+                pagination = GifListPagination()
+            }
+        }
+    }
+    
+    /*!
+        Last pagination object
+     */
+    private var pagination: GifListPagination = GifListPagination()
+    
+    /*!
+        Array of objects to be shown
+     */
+    var gifsList: [GifObject] = []
+    
+    /*!
+        Only one pagination request should be active
+     */
+    var isLoadingNewPage = false
+    
+    /*!
+        If a search query is present, search data is fetched.
+        If there is no search query, trending data is fetched.
+     */
+    func getData(query: String) {
+        
+        self.lastSearchedTerm = query
+        
+        if lastSearchedTerm.isEmpty {
+            self.fetchTrendingData()
+        } else {
+            self.fetchSearchData(query: query)
+        }
+        
+    }
+    
+    /// Trending data call
+    private func fetchTrendingData() {
+        
+        self.prepareForFetch()
+        
+        GiphyApiManager.shared.getTrendingGifs(offset: pagination.nextOffset) { [weak self] result in
+            
+            switch result {
+                
+            case .success(let listResponse):
+                self?.didReceiveSuccessfulListResponse(listResponse: listResponse)
+                
+            case .failure(let error):
+                print(error)
+            }
+            
+            self?.isLoadingNewPage = false
+
+        }
+        
+    }
+    
+    /// Search data called
+    private func fetchSearchData(query: String) {
+        
+        self.prepareForFetch()
+
+        GiphyApiManager.shared.getSearchGifs(offset: pagination.nextOffset, query: query) { [weak self] result in
+            
+            switch result {
+                
+            case .success(let listResponse):
+                self?.didReceiveSuccessfulListResponse(listResponse: listResponse)
+
+            case .failure(let error):
+                print(error)
+            }
+
+            self?.isLoadingNewPage = false
+        }
+        
+    }
+    
+    /// Prepares self before making a call
+    private func prepareForFetch() {
+        
+
+        if self.gifsList.count == 0 {
+            self.delegate?.presenterWillStartLoadingForEmptyData()
+        }
+
+        GiphyApiManager.shared.cancelLastSearchableTask()
+        
+        if pagination.count + pagination.offset != 0 {
+            isLoadingNewPage = true
+        }
+
+    }
+
+    private func didReceiveSuccessfulListResponse(listResponse: GiphyListResponse) {
+        
+        let offset = listResponse.pagination.offset
+        
+        if (offset == 0) {
+            self.gifsList = listResponse.data
+            self.delegate?.presenterDidReceiveInitialData()
+        } else {
+            let indexPaths = (offset..<(offset + listResponse.data.count)).map({IndexPath(row: $0, section: 0)})
+            self.gifsList += listResponse.data
+            self.delegate?.presenterDidReceiveAdditionalData(at: indexPaths)
+        }
+        
+        self.pagination = listResponse.pagination
+        
+    }
+
+    
+}

+ 177 - 0
LiveLikeiOSAssessmentV1/ViewControllers/Views/GifListViewController.swift

@@ -0,0 +1,177 @@
+//
+//  GifListViewController.swift
+
+import UIKit
+
+class GifListViewController: UIViewController {
+
+    private var presenter: GifListPresenter = GifListPresenter()
+    
+    private lazy var collectionView: UICollectionView = {
+        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: GifListCollectionViewFlowLayout())
+        collectionView.register(GifCollectionViewCell.self, forCellWithReuseIdentifier: GifCollectionViewCell.reuseIdentifier)
+        collectionView.keyboardDismissMode = .onDrag
+        return collectionView
+    }()
+    
+    private lazy var searchBar: UISearchBar = {
+        let sb = UISearchBar()
+        sb.delegate = self
+        sb.placeholder = "search library..."
+        return sb
+    }()
+    
+    /// Indicator view that we expect to show when no data is present
+    private lazy var activityIndicator: UIActivityIndicatorView = {
+        let aIndicator = UIActivityIndicatorView()
+        aIndicator.hidesWhenStopped = true
+        return aIndicator
+    }()
+    
+    /// Delay the search by couple of ms so the web devs will like us
+    private var delayedWorkItem: DispatchWorkItem?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.setupUI()
+        self.setupSelf()
+        self.presenter.getData(query: "")
+    }
+    
+    private func setupUI() {
+        
+        self.view.addSubview(self.collectionView)
+        self.view.addSubview(self.activityIndicator)
+        
+        self.initialConstraintSetup()
+        
+    }
+    
+    private func setupSelf() {
+        
+        self.navigationItem.titleView = self.searchBar
+                
+        presenter.delegate = self
+        
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.prefetchDataSource = self
+        
+    }
+
+}
+
+// MARK: - Constraints
+
+extension GifListViewController {
+    
+    /*!
+        Initial constraints setup
+     */
+    private func initialConstraintSetup() {
+        
+        self.collectionView.translatesAutoresizingMaskIntoConstraints = false
+        self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false
+
+        NSLayoutConstraint.activate([
+            
+            self.collectionView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
+            self.collectionView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
+            self.collectionView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
+            self.collectionView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
+            
+            self.activityIndicator.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
+            self.activityIndicator.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
+            
+        ])
+
+    }
+}
+
+// MARK: - Presenter delegate
+
+extension GifListViewController: GifListPresenterDelegate {
+    
+    func presenterDidReceiveInitialData() {
+        
+        DispatchQueue.main.async {
+            
+            self.collectionView.setContentOffset(CGPoint(x: 0, y: self.collectionView.safeAreaInsets.top), animated: false)
+            self.collectionView.reloadData()
+            
+            if self.activityIndicator.isAnimating == true {
+                self.activityIndicator.stopAnimating()
+            }
+            
+        }
+
+    }
+    
+    
+    func presenterDidReceiveAdditionalData(at indexPaths: [IndexPath]) {
+        DispatchQueue.main.async {
+            self.collectionView.insertItems(at: indexPaths)
+        }
+    }
+    
+    
+    func presenterWillStartLoadingForEmptyData() {
+        DispatchQueue.main.async {
+            self.activityIndicator.startAnimating()
+        }
+    }
+    
+}
+
+
+// MARK: - SearchBar delegate
+
+extension GifListViewController: UISearchBarDelegate {
+    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
+        
+        self.delayedWorkItem?.cancel()
+        
+        delayedWorkItem = DispatchWorkItem { [weak self] in
+            self?.presenter.getData(query: searchText)
+        }
+        
+        DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.2, execute: delayedWorkItem!)
+    }
+}
+
+
+// MARK: - CollectionView delegate and data source
+
+extension GifListViewController: UICollectionViewDataSource, UICollectionViewDelegate {
+    
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        return presenter.gifsList.count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GifCollectionViewCell.reuseIdentifier, for: indexPath) as! GifCollectionViewCell
+        cell.gif = presenter.gifsList[indexPath.row]
+        return cell
+    }
+        
+}
+
+extension GifListViewController: UICollectionViewDataSourcePrefetching {
+    
+    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
+        
+        guard presenter.isLoadingNewPage == false else { return }
+        
+        for indexPath in indexPaths {
+            if indexPath.row > presenter.gifsList.count - Constants.GiphyPaginationLimit/2 {
+                delayedWorkItem?.cancel()
+                presenter.getData(query: self.searchBar.text ?? "")
+                break
+            }
+        }
+        
+    }
+    
+    
+}

+ 29 - 0
LiveLikeiOSAssessmentV1Tests/LiveLikeGiphyChallengeTests.swift

@@ -0,0 +1,29 @@
+//
+//
+
+import XCTest
+@testable import LiveLikeiOSAssessmentV1
+
+class LiveLikeiOSAssessmentV1Tests: XCTestCase {
+
+    override func setUpWithError() throws {
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+    }
+
+    override func tearDownWithError() throws {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+    }
+
+    func testExample() throws {
+        // This is an example of a functional test case.
+        // Use XCTAssert and related functions to verify your tests produce the correct results.
+    }
+
+    func testPerformanceExample() throws {
+        // This is an example of a performance test case.
+        self.measure {
+            // Put the code you want to measure the time of here.
+        }
+    }
+
+}

+ 31 - 0
README.md

@@ -0,0 +1,31 @@
+# LiveLike iOS Code Assessment
+Welcome to the LiveLike iOS Code Assessment. The goal of this exercise is to fix and complete an app that allows you to search for animated GIFs and display trending ones using the [GiphyAPI](https://developers.giphy.com/docs/api#quick-start-guide)
+> Please follow instructions in the **Submission** section to correctly submit your project.
+
+### Task #1 - Create a network request
+- Open file `GiphyApiManager`
+- Observe the functionality inside `getCodable()` func is left empty
+- Fill in the appropriate functionality using native iOS code that would create a network request, receive data, decodes it or return a `GiphyApiError.decodingFailed` error
+
+### Task #2 - Add an Image to the UICollectionViewCell
+- Open file `GifCollectionViewCell`
+- Use `self.imageView` variable to add an image to the cell
+- Please make the image height and width conform to the cell's bounds
+- Using Auto Layout Constraints to build out the UI is a hard requirement 
+
+### Hard Requirements
+-   **Use of third party frameworks is not permitted**
+-  You must handle all functionality using native iOS frameworks
+-  Please manually write all UI code using Auto Layout Constraints
+-  The code needs to compile and run in the simulator
+-  The code should not crash under normal usage (searching, scrolling, rotation)
+-  All code must be written in Swift
+-  The project should run out of the box in the latest (non-beta) version of Xcode
+
+### Extra Credit
+-   Unit Tests
+
+## Submission
+-   Create an account with **code.livelike.com**
+-   Fork the repository into your local environment
+-   To submit your project **you must submit it as a Pull Request**