diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31ab0ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +# Created by https://www.toptal.com/developers/gitignore/api/swift +# Edit at https://www.toptal.com/developers/gitignore?templates=swift + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# End of https://www.toptal.com/developers/gitignore/api/swift diff --git a/Hearthstone/Hearthstone.xcodeproj/project.pbxproj b/Hearthstone/Hearthstone.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a804975 --- /dev/null +++ b/Hearthstone/Hearthstone.xcodeproj/project.pbxproj @@ -0,0 +1,896 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 3006060828939EFC00EF0BE4 /* CardsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3006060728939EFC00EF0BE4 /* CardsCollectionViewController.swift */; }; + 30070E4328916E450062F0E6 /* TestCardsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30070E4228916E450062F0E6 /* TestCardsFile.swift */; }; + 302FF91728B5FE7A00C9D114 /* TestFavoritesCRUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302FF91628B5FE7A00C9D114 /* TestFavoritesCRUD.swift */; }; + 302FF91A28B6023C00C9D114 /* TestCoreDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302FF91928B6023C00C9D114 /* TestCoreDataService.swift */; }; + 303E314228B4B5EA00A44FC8 /* TestDetailCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303E314128B4B5EA00A44FC8 /* TestDetailCardViewModel.swift */; }; + 30436AEE28A6DDA800BC0BFC /* FavoritesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30436AED28A6DDA800BC0BFC /* FavoritesService.swift */; }; + 30436AFA28A6E61400BC0BFC /* Favorite+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30436AF828A6E61400BC0BFC /* Favorite+CoreDataClass.swift */; }; + 30436AFB28A6E61400BC0BFC /* Favorite+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30436AF928A6E61400BC0BFC /* Favorite+CoreDataProperties.swift */; }; + 3058D74B28BC970900771FF6 /* LoadingImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3058D74A28BC970900771FF6 /* LoadingImage.swift */; }; + 305C6FEE288EED28001D4559 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305C6FED288EED28001D4559 /* AppDelegate.swift */; }; + 305C6FF0288EED28001D4559 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305C6FEF288EED28001D4559 /* SceneDelegate.swift */; }; + 305C6FF8288EED28001D4559 /* Hearthstone.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 305C6FF6288EED28001D4559 /* Hearthstone.xcdatamodeld */; }; + 305C6FFA288EED2B001D4559 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 305C6FF9288EED2B001D4559 /* Assets.xcassets */; }; + 305C6FFD288EED2B001D4559 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 305C6FFB288EED2B001D4559 /* LaunchScreen.storyboard */; }; + 3063DB78289273C100715E98 /* CardsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3063DB77289273C100715E98 /* CardsHelper.swift */; }; + 30818457288FCCCC00838205 /* cards.json in Resources */ = {isa = PBXBuildFile; fileRef = 30818456288FCCCB00838205 /* cards.json */; }; + 309EFDD328A43C1400D3F753 /* WatermarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 309EFDD228A43C1400D3F753 /* WatermarkView.swift */; }; + 30A0F276289C5AEA00617094 /* CardsDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A0F275289C5AEA00617094 /* CardsDataService.swift */; }; + 30A128B028B3774500B66C80 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A128AF28B3774500B66C80 /* CardView.swift */; }; + 30A8D56528929D1B00849D1F /* CardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A8D56428929D1B00849D1F /* CardViewModel.swift */; }; + 30A8D5692892A67600849D1F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A8D5682892A67600849D1F /* Extensions.swift */; }; + 30A8D56B2892AA2000849D1F /* TestCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A8D56A2892AA2000849D1F /* TestCardViewModel.swift */; }; + 30A8DD3A28A142B800AF6A01 /* TestAllCardService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A8DD3928A142B800AF6A01 /* TestAllCardService.swift */; }; + 30AE80A728B35A18006E0C53 /* CardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AE80A628B35A18006E0C53 /* CardViewController.swift */; }; + 30B596182896DF3F00253379 /* CardGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B596172896DF3F00253379 /* CardGridViewCell.swift */; }; + 30B71181288FE1BA00935D29 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B71180288FE1BA00935D29 /* Card.swift */; }; + 30B88F81289319C5005D326E /* HomeTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B88F80289319C5005D326E /* HomeTabViewController.swift */; }; + 30D7659E28B9558600D11818 /* TestInitFavoritesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D7659D28B9558600D11818 /* TestInitFavoritesProtocol.swift */; }; + 30D765A028B9562D00D11818 /* MockUpdateFavoritesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D7659F28B9562D00D11818 /* MockUpdateFavoritesProtocol.swift */; }; + 30D765A328B95A0E00D11818 /* TestUpdateFavoritesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D765A228B95A0E00D11818 /* TestUpdateFavoritesProtocol.swift */; }; + 30D765A528B965A200D11818 /* CardViewModelHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D765A428B965A200D11818 /* CardViewModelHelper.swift */; }; + 30ECBEB628BCA6F300A2805C /* TestNavigationToCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ECBEB528BCA6F300A2805C /* TestNavigationToCard.swift */; }; + 30ECBEB928BCAA2900A2805C /* NavigationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ECBEB828BCAA2900A2805C /* NavigationExtensions.swift */; }; + 30ECBEBB28BCD4AC00A2805C /* TestTabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ECBEBA28BCD4AC00A2805C /* TestTabNavigation.swift */; }; + 30ECBEBE28BCD89200A2805C /* TestWatermark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ECBEBD28BCD89200A2805C /* TestWatermark.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 30070E4E289180A70062F0E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 305C6FE2288EED28001D4559 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 305C6FE9288EED28001D4559; + remoteInfo = Hearthstone; + }; + 305C7004288EED2C001D4559 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 305C6FE2288EED28001D4559 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 305C6FE9288EED28001D4559; + remoteInfo = Hearthstone; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 3006060728939EFC00EF0BE4 /* CardsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsCollectionViewController.swift; sourceTree = ""; }; + 30070E4228916E450062F0E6 /* TestCardsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCardsFile.swift; sourceTree = ""; }; + 30070E48289180A70062F0E6 /* HearthstoneUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HearthstoneUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 30243DD128BCE5C300D1EC57 /* README-TSIKINAS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "README-TSIKINAS.md"; sourceTree = ""; }; + 302FF91628B5FE7A00C9D114 /* TestFavoritesCRUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFavoritesCRUD.swift; sourceTree = ""; }; + 302FF91928B6023C00C9D114 /* TestCoreDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCoreDataService.swift; sourceTree = ""; }; + 303E314128B4B5EA00A44FC8 /* TestDetailCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDetailCardViewModel.swift; sourceTree = ""; }; + 30436AED28A6DDA800BC0BFC /* FavoritesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesService.swift; sourceTree = ""; }; + 30436AF828A6E61400BC0BFC /* Favorite+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite+CoreDataClass.swift"; sourceTree = ""; }; + 30436AF928A6E61400BC0BFC /* Favorite+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite+CoreDataProperties.swift"; sourceTree = ""; }; + 3058D74A28BC970900771FF6 /* LoadingImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingImage.swift; sourceTree = ""; }; + 305C6FEA288EED28001D4559 /* Hearthstone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hearthstone.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 305C6FED288EED28001D4559 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 305C6FEF288EED28001D4559 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 305C6FF7288EED28001D4559 /* Hearthstone.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Hearthstone.xcdatamodel; sourceTree = ""; }; + 305C6FF9288EED2B001D4559 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 305C6FFC288EED2B001D4559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 305C6FFE288EED2B001D4559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 305C7003288EED2C001D4559 /* HearthstoneTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HearthstoneTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3063DB77289273C100715E98 /* CardsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsHelper.swift; sourceTree = ""; }; + 30818456288FCCCB00838205 /* cards.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = cards.json; path = Hearthstone/Assets/cards.json; sourceTree = SOURCE_ROOT; }; + 309EFDD228A43C1400D3F753 /* WatermarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatermarkView.swift; sourceTree = ""; }; + 30A0F275289C5AEA00617094 /* CardsDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsDataService.swift; sourceTree = ""; }; + 30A128AF28B3774500B66C80 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; + 30A8D56428929D1B00849D1F /* CardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewModel.swift; sourceTree = ""; }; + 30A8D5682892A67600849D1F /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 30A8D56A2892AA2000849D1F /* TestCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCardViewModel.swift; sourceTree = ""; }; + 30A8DD3928A142B800AF6A01 /* TestAllCardService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAllCardService.swift; sourceTree = ""; }; + 30AE80A628B35A18006E0C53 /* CardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewController.swift; sourceTree = ""; }; + 30B596172896DF3F00253379 /* CardGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardGridViewCell.swift; sourceTree = ""; }; + 30B71180288FE1BA00935D29 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; + 30B88F80289319C5005D326E /* HomeTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabViewController.swift; sourceTree = ""; }; + 30D7659D28B9558600D11818 /* TestInitFavoritesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInitFavoritesProtocol.swift; sourceTree = ""; }; + 30D7659F28B9562D00D11818 /* MockUpdateFavoritesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUpdateFavoritesProtocol.swift; sourceTree = ""; }; + 30D765A228B95A0E00D11818 /* TestUpdateFavoritesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUpdateFavoritesProtocol.swift; sourceTree = ""; }; + 30D765A428B965A200D11818 /* CardViewModelHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewModelHelper.swift; sourceTree = ""; }; + 30ECBEB528BCA6F300A2805C /* TestNavigationToCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNavigationToCard.swift; sourceTree = ""; }; + 30ECBEB828BCAA2900A2805C /* NavigationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationExtensions.swift; sourceTree = ""; }; + 30ECBEBA28BCD4AC00A2805C /* TestTabNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTabNavigation.swift; sourceTree = ""; }; + 30ECBEBD28BCD89200A2805C /* TestWatermark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWatermark.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 30070E45289180A70062F0E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 305C6FE7288EED28001D4559 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 305C7000288EED2C001D4559 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 30070E4028916DF50062F0E6 /* CardsTests */ = { + isa = PBXGroup; + children = ( + 30070E4228916E450062F0E6 /* TestCardsFile.swift */, + 30070E4128916E010062F0E6 /* Helpers */, + 30A8D56A2892AA2000849D1F /* TestCardViewModel.swift */, + 30A8DD3928A142B800AF6A01 /* TestAllCardService.swift */, + 303E314128B4B5EA00A44FC8 /* TestDetailCardViewModel.swift */, + ); + path = CardsTests; + sourceTree = ""; + }; + 30070E4128916E010062F0E6 /* Helpers */ = { + isa = PBXGroup; + children = ( + 3063DB77289273C100715E98 /* CardsHelper.swift */, + 30D765A428B965A200D11818 /* CardViewModelHelper.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 30070E49289180A70062F0E6 /* HearthstoneUITests */ = { + isa = PBXGroup; + children = ( + 30ECBEBC28BCD87000A2805C /* ViewsTests */, + 30ECBEB428BCA6C900A2805C /* NavigationTests */, + ); + path = HearthstoneUITests; + sourceTree = ""; + }; + 302FF91528B5FE2F00C9D114 /* FavoritesTests */ = { + isa = PBXGroup; + children = ( + 30D7659C28B9555600D11818 /* CardViewModel+Favorites */, + 302FF91828B6020400C9D114 /* Helpers */, + 302FF91628B5FE7A00C9D114 /* TestFavoritesCRUD.swift */, + ); + path = FavoritesTests; + sourceTree = ""; + }; + 302FF91828B6020400C9D114 /* Helpers */ = { + isa = PBXGroup; + children = ( + 302FF91928B6023C00C9D114 /* TestCoreDataService.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 30436AF328A6E35600BC0BFC /* FavoriteDB */ = { + isa = PBXGroup; + children = ( + 30436AF828A6E61400BC0BFC /* Favorite+CoreDataClass.swift */, + 30436AF928A6E61400BC0BFC /* Favorite+CoreDataProperties.swift */, + ); + path = FavoriteDB; + sourceTree = ""; + }; + 305C6FE1288EED28001D4559 = { + isa = PBXGroup; + children = ( + 30243DD128BCE5C300D1EC57 /* README-TSIKINAS.md */, + 305C6FEC288EED28001D4559 /* Hearthstone */, + 305C7006288EED2C001D4559 /* HearthstoneTests */, + 30070E49289180A70062F0E6 /* HearthstoneUITests */, + 305C6FEB288EED28001D4559 /* Products */, + ); + sourceTree = ""; + }; + 305C6FEB288EED28001D4559 /* Products */ = { + isa = PBXGroup; + children = ( + 305C6FEA288EED28001D4559 /* Hearthstone.app */, + 305C7003288EED2C001D4559 /* HearthstoneTests.xctest */, + 30070E48289180A70062F0E6 /* HearthstoneUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 305C6FEC288EED28001D4559 /* Hearthstone */ = { + isa = PBXGroup; + children = ( + 30436AF328A6E35600BC0BFC /* FavoriteDB */, + 30A0F274289C5A0700617094 /* Services */, + 305C6FED288EED28001D4559 /* AppDelegate.swift */, + 30818455288FCCBF00838205 /* Assets */, + 305C6FF6288EED28001D4559 /* Hearthstone.xcdatamodeld */, + 3081845B288FCE2300838205 /* Helpers */, + 305C6FFE288EED2B001D4559 /* Info.plist */, + 305C6FFB288EED2B001D4559 /* LaunchScreen.storyboard */, + 3081845A288FCE1C00838205 /* Models */, + 305C6FEF288EED28001D4559 /* SceneDelegate.swift */, + 3081845C288FCE2E00838205 /* ViewModels */, + 30818459288FCE1600838205 /* Views */, + ); + path = Hearthstone; + sourceTree = ""; + }; + 305C7006288EED2C001D4559 /* HearthstoneTests */ = { + isa = PBXGroup; + children = ( + 302FF91528B5FE2F00C9D114 /* FavoritesTests */, + 30070E4028916DF50062F0E6 /* CardsTests */, + ); + path = HearthstoneTests; + sourceTree = ""; + }; + 30818455288FCCBF00838205 /* Assets */ = { + isa = PBXGroup; + children = ( + 30818456288FCCCB00838205 /* cards.json */, + 305C6FF9288EED2B001D4559 /* Assets.xcassets */, + ); + path = Assets; + sourceTree = ""; + }; + 30818459288FCE1600838205 /* Views */ = { + isa = PBXGroup; + children = ( + 309EFDD128A43BF900D3F753 /* Misc */, + 30B596162896DF1C00253379 /* Cells */, + 30B88F8428931DC4005D326E /* ViewControllers */, + ); + path = Views; + sourceTree = ""; + }; + 3081845A288FCE1C00838205 /* Models */ = { + isa = PBXGroup; + children = ( + 30B71180288FE1BA00935D29 /* Card.swift */, + ); + path = Models; + sourceTree = ""; + }; + 3081845B288FCE2300838205 /* Helpers */ = { + isa = PBXGroup; + children = ( + 30A8D5682892A67600849D1F /* Extensions.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 3081845C288FCE2E00838205 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 30A8D56428929D1B00849D1F /* CardViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 309EFDD128A43BF900D3F753 /* Misc */ = { + isa = PBXGroup; + children = ( + 309EFDD228A43C1400D3F753 /* WatermarkView.swift */, + 30A128AF28B3774500B66C80 /* CardView.swift */, + 3058D74A28BC970900771FF6 /* LoadingImage.swift */, + ); + path = Misc; + sourceTree = ""; + }; + 30A0F274289C5A0700617094 /* Services */ = { + isa = PBXGroup; + children = ( + 30A0F275289C5AEA00617094 /* CardsDataService.swift */, + 30436AED28A6DDA800BC0BFC /* FavoritesService.swift */, + ); + path = Services; + sourceTree = ""; + }; + 30B596162896DF1C00253379 /* Cells */ = { + isa = PBXGroup; + children = ( + 30B596172896DF3F00253379 /* CardGridViewCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 30B88F8428931DC4005D326E /* ViewControllers */ = { + isa = PBXGroup; + children = ( + 30B88F80289319C5005D326E /* HomeTabViewController.swift */, + 3006060728939EFC00EF0BE4 /* CardsCollectionViewController.swift */, + 30AE80A628B35A18006E0C53 /* CardViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; + 30D7659C28B9555600D11818 /* CardViewModel+Favorites */ = { + isa = PBXGroup; + children = ( + 30D765A128B9563A00D11818 /* Helpers */, + 30D7659D28B9558600D11818 /* TestInitFavoritesProtocol.swift */, + 30D765A228B95A0E00D11818 /* TestUpdateFavoritesProtocol.swift */, + ); + path = "CardViewModel+Favorites"; + sourceTree = ""; + }; + 30D765A128B9563A00D11818 /* Helpers */ = { + isa = PBXGroup; + children = ( + 30D7659F28B9562D00D11818 /* MockUpdateFavoritesProtocol.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 30ECBEB428BCA6C900A2805C /* NavigationTests */ = { + isa = PBXGroup; + children = ( + 30ECBEB728BCAA1800A2805C /* Helpers */, + 30ECBEB528BCA6F300A2805C /* TestNavigationToCard.swift */, + 30ECBEBA28BCD4AC00A2805C /* TestTabNavigation.swift */, + ); + path = NavigationTests; + sourceTree = ""; + }; + 30ECBEB728BCAA1800A2805C /* Helpers */ = { + isa = PBXGroup; + children = ( + 30ECBEB828BCAA2900A2805C /* NavigationExtensions.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 30ECBEBC28BCD87000A2805C /* ViewsTests */ = { + isa = PBXGroup; + children = ( + 30ECBEBD28BCD89200A2805C /* TestWatermark.swift */, + ); + path = ViewsTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 30070E47289180A70062F0E6 /* HearthstoneUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 30070E50289180A70062F0E6 /* Build configuration list for PBXNativeTarget "HearthstoneUITests" */; + buildPhases = ( + 30070E44289180A70062F0E6 /* Sources */, + 30070E45289180A70062F0E6 /* Frameworks */, + 30070E46289180A70062F0E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 30070E4F289180A70062F0E6 /* PBXTargetDependency */, + ); + name = HearthstoneUITests; + productName = HearthstoneUITests; + productReference = 30070E48289180A70062F0E6 /* HearthstoneUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 305C6FE9288EED28001D4559 /* Hearthstone */ = { + isa = PBXNativeTarget; + buildConfigurationList = 305C7017288EED2C001D4559 /* Build configuration list for PBXNativeTarget "Hearthstone" */; + buildPhases = ( + 305C6FE6288EED28001D4559 /* Sources */, + 305C6FE7288EED28001D4559 /* Frameworks */, + 305C6FE8288EED28001D4559 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Hearthstone; + productName = Hearthstone; + productReference = 305C6FEA288EED28001D4559 /* Hearthstone.app */; + productType = "com.apple.product-type.application"; + }; + 305C7002288EED2C001D4559 /* HearthstoneTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 305C701A288EED2C001D4559 /* Build configuration list for PBXNativeTarget "HearthstoneTests" */; + buildPhases = ( + 305C6FFF288EED2C001D4559 /* Sources */, + 305C7000288EED2C001D4559 /* Frameworks */, + 305C7001288EED2C001D4559 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 305C7005288EED2C001D4559 /* PBXTargetDependency */, + ); + name = HearthstoneTests; + productName = HearthstoneTests; + productReference = 305C7003288EED2C001D4559 /* HearthstoneTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 305C6FE2288EED28001D4559 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1340; + LastUpgradeCheck = 1340; + TargetAttributes = { + 30070E47289180A70062F0E6 = { + CreatedOnToolsVersion = 13.4.1; + LastSwiftMigration = 1340; + TestTargetID = 305C6FE9288EED28001D4559; + }; + 305C6FE9288EED28001D4559 = { + CreatedOnToolsVersion = 13.4.1; + }; + 305C7002288EED2C001D4559 = { + CreatedOnToolsVersion = 13.4.1; + LastSwiftMigration = 1340; + TestTargetID = 305C6FE9288EED28001D4559; + }; + }; + }; + buildConfigurationList = 305C6FE5288EED28001D4559 /* Build configuration list for PBXProject "Hearthstone" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 305C6FE1288EED28001D4559; + productRefGroup = 305C6FEB288EED28001D4559 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 305C6FE9288EED28001D4559 /* Hearthstone */, + 305C7002288EED2C001D4559 /* HearthstoneTests */, + 30070E47289180A70062F0E6 /* HearthstoneUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 30070E46289180A70062F0E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 305C6FE8288EED28001D4559 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 305C6FFD288EED2B001D4559 /* LaunchScreen.storyboard in Resources */, + 305C6FFA288EED2B001D4559 /* Assets.xcassets in Resources */, + 30818457288FCCCC00838205 /* cards.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 305C7001288EED2C001D4559 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 30070E44289180A70062F0E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 30ECBEBE28BCD89200A2805C /* TestWatermark.swift in Sources */, + 30ECBEB628BCA6F300A2805C /* TestNavigationToCard.swift in Sources */, + 30ECBEBB28BCD4AC00A2805C /* TestTabNavigation.swift in Sources */, + 30ECBEB928BCAA2900A2805C /* NavigationExtensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 305C6FE6288EED28001D4559 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 305C6FF8288EED28001D4559 /* Hearthstone.xcdatamodeld in Sources */, + 30436AFB28A6E61400BC0BFC /* Favorite+CoreDataProperties.swift in Sources */, + 30A0F276289C5AEA00617094 /* CardsDataService.swift in Sources */, + 30436AFA28A6E61400BC0BFC /* Favorite+CoreDataClass.swift in Sources */, + 305C6FEE288EED28001D4559 /* AppDelegate.swift in Sources */, + 30A8D56528929D1B00849D1F /* CardViewModel.swift in Sources */, + 30B88F81289319C5005D326E /* HomeTabViewController.swift in Sources */, + 3058D74B28BC970900771FF6 /* LoadingImage.swift in Sources */, + 305C6FF0288EED28001D4559 /* SceneDelegate.swift in Sources */, + 309EFDD328A43C1400D3F753 /* WatermarkView.swift in Sources */, + 30A128B028B3774500B66C80 /* CardView.swift in Sources */, + 30B596182896DF3F00253379 /* CardGridViewCell.swift in Sources */, + 30AE80A728B35A18006E0C53 /* CardViewController.swift in Sources */, + 30B71181288FE1BA00935D29 /* Card.swift in Sources */, + 30436AEE28A6DDA800BC0BFC /* FavoritesService.swift in Sources */, + 3006060828939EFC00EF0BE4 /* CardsCollectionViewController.swift in Sources */, + 30A8D5692892A67600849D1F /* Extensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 305C6FFF288EED2C001D4559 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 30D765A028B9562D00D11818 /* MockUpdateFavoritesProtocol.swift in Sources */, + 30070E4328916E450062F0E6 /* TestCardsFile.swift in Sources */, + 30D765A328B95A0E00D11818 /* TestUpdateFavoritesProtocol.swift in Sources */, + 30D765A528B965A200D11818 /* CardViewModelHelper.swift in Sources */, + 302FF91A28B6023C00C9D114 /* TestCoreDataService.swift in Sources */, + 3063DB78289273C100715E98 /* CardsHelper.swift in Sources */, + 303E314228B4B5EA00A44FC8 /* TestDetailCardViewModel.swift in Sources */, + 30D7659E28B9558600D11818 /* TestInitFavoritesProtocol.swift in Sources */, + 302FF91728B5FE7A00C9D114 /* TestFavoritesCRUD.swift in Sources */, + 30A8D56B2892AA2000849D1F /* TestCardViewModel.swift in Sources */, + 30A8DD3A28A142B800AF6A01 /* TestAllCardService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 30070E4F289180A70062F0E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 305C6FE9288EED28001D4559 /* Hearthstone */; + targetProxy = 30070E4E289180A70062F0E6 /* PBXContainerItemProxy */; + }; + 305C7005288EED2C001D4559 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 305C6FE9288EED28001D4559 /* Hearthstone */; + targetProxy = 305C7004288EED2C001D4559 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 305C6FFB288EED2B001D4559 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 305C6FFC288EED2B001D4559 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 30070E51289180A70062F0E6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7BS7VD55S5; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tsikinas.HearthstoneUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Hearthstone; + }; + name = Debug; + }; + 30070E52289180A70062F0E6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7BS7VD55S5; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tsikinas.HearthstoneUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Hearthstone; + }; + name = Release; + }; + 305C7015288EED2C001D4559 /* 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_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.5; + 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; + }; + 305C7016288EED2C001D4559 /* 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_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.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 305C7018288EED2C001D4559 /* 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 = 7BS7VD55S5; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Hearthstone/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; + 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.tsikinas.Hearthstone; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 305C7019288EED2C001D4559 /* 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 = 7BS7VD55S5; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Hearthstone/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = ""; + 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.tsikinas.Hearthstone; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 305C701B288EED2C001D4559 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7BS7VD55S5; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tsikinas.HearthstoneTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hearthstone.app/Hearthstone"; + }; + name = Debug; + }; + 305C701C288EED2C001D4559 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7BS7VD55S5; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tsikinas.HearthstoneTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hearthstone.app/Hearthstone"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 30070E50289180A70062F0E6 /* Build configuration list for PBXNativeTarget "HearthstoneUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 30070E51289180A70062F0E6 /* Debug */, + 30070E52289180A70062F0E6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 305C6FE5288EED28001D4559 /* Build configuration list for PBXProject "Hearthstone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 305C7015288EED2C001D4559 /* Debug */, + 305C7016288EED2C001D4559 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 305C7017288EED2C001D4559 /* Build configuration list for PBXNativeTarget "Hearthstone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 305C7018288EED2C001D4559 /* Debug */, + 305C7019288EED2C001D4559 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 305C701A288EED2C001D4559 /* Build configuration list for PBXNativeTarget "HearthstoneTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 305C701B288EED2C001D4559 /* Debug */, + 305C701C288EED2C001D4559 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 305C6FF6288EED28001D4559 /* Hearthstone.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 305C6FF7288EED28001D4559 /* Hearthstone.xcdatamodel */, + ); + currentVersion = 305C6FF7288EED28001D4559 /* Hearthstone.xcdatamodel */; + path = Hearthstone.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 305C6FE2288EED28001D4559 /* Project object */; +} diff --git a/Hearthstone/Hearthstone/AppDelegate.swift b/Hearthstone/Hearthstone/AppDelegate.swift new file mode 100644 index 0000000..f9b7fcb --- /dev/null +++ b/Hearthstone/Hearthstone/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 25/7/22. +// + +import UIKit +import CoreData + +@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: - Core Data Stuff + + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "Hearthstone") + container.loadPersistentStores { description, error in + + if let error = error { + debugPrint("Unresolved error: \(error)") + } + + } + container.viewContext.automaticallyMergesChangesFromParent = true + return container + + }() + +} + diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/AccentColor.colorset/Contents.json b/Hearthstone/Hearthstone/Assets/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Hearthstone/Hearthstone/Assets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json b/Hearthstone/Hearthstone/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Hearthstone/Hearthstone/Assets/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 + } +} diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/Contents.json b/Hearthstone/Hearthstone/Assets/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Hearthstone/Hearthstone/Assets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/SeparatorColor.colorset/Contents.json b/Hearthstone/Hearthstone/Assets/Assets.xcassets/SeparatorColor.colorset/Contents.json new file mode 100644 index 0000000..b60e27f --- /dev/null +++ b/Hearthstone/Hearthstone/Assets/Assets.xcassets/SeparatorColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.161", + "green" : "0.113", + "red" : "0.131" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.251", + "green" : "0.251", + "red" : "0.251" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/SplashScreen.imageset/Contents.json b/Hearthstone/Hearthstone/Assets/Assets.xcassets/SplashScreen.imageset/Contents.json new file mode 100644 index 0000000..bc6d81b --- /dev/null +++ b/Hearthstone/Hearthstone/Assets/Assets.xcassets/SplashScreen.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Hearthstone-PNG-High-Quality-Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/SplashScreen.imageset/Hearthstone-PNG-High-Quality-Image.png b/Hearthstone/Hearthstone/Assets/Assets.xcassets/SplashScreen.imageset/Hearthstone-PNG-High-Quality-Image.png new file mode 100644 index 0000000..7ac414f Binary files /dev/null and b/Hearthstone/Hearthstone/Assets/Assets.xcassets/SplashScreen.imageset/Hearthstone-PNG-High-Quality-Image.png differ diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/Contents.json b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/Contents.json new file mode 100644 index 0000000..026b188 --- /dev/null +++ b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "PVPDR_SW_Passive_08.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "PVPDR_SW_Passive_08@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "PVPDR_SW_Passive_08@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08.png b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08.png new file mode 100644 index 0000000..36f1e2d Binary files /dev/null and b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08.png differ diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08@2x.png b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08@2x.png new file mode 100644 index 0000000..d2f3b57 Binary files /dev/null and b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08@2x.png differ diff --git a/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08@3x.png b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08@3x.png new file mode 100644 index 0000000..bf960b4 Binary files /dev/null and b/Hearthstone/Hearthstone/Assets/Assets.xcassets/oops.imageset/PVPDR_SW_Passive_08@3x.png differ diff --git a/cards.json b/Hearthstone/Hearthstone/Assets/cards.json similarity index 100% rename from cards.json rename to Hearthstone/Hearthstone/Assets/cards.json diff --git a/Hearthstone/Hearthstone/Base.lproj/LaunchScreen.storyboard b/Hearthstone/Hearthstone/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..880bb34 --- /dev/null +++ b/Hearthstone/Hearthstone/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hearthstone/Hearthstone/FavoriteDB/Favorite+CoreDataClass.swift b/Hearthstone/Hearthstone/FavoriteDB/Favorite+CoreDataClass.swift new file mode 100644 index 0000000..b0ce848 --- /dev/null +++ b/Hearthstone/Hearthstone/FavoriteDB/Favorite+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// Favorite+CoreDataClass.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 12/8/22. +// +// + +import Foundation +import CoreData + + +public class Favorite: NSManagedObject { + +} diff --git a/Hearthstone/Hearthstone/FavoriteDB/Favorite+CoreDataProperties.swift b/Hearthstone/Hearthstone/FavoriteDB/Favorite+CoreDataProperties.swift new file mode 100644 index 0000000..7d490cf --- /dev/null +++ b/Hearthstone/Hearthstone/FavoriteDB/Favorite+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// Favorite+CoreDataProperties.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 12/8/22. +// +// + +import Foundation +import CoreData + + +extension Favorite { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Favorite") + } + + @NSManaged public var cardID: String? + +} + +extension Favorite : Identifiable { + +} diff --git a/Hearthstone/Hearthstone/Hearthstone.xcdatamodeld/.xccurrentversion b/Hearthstone/Hearthstone/Hearthstone.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..782ed92 --- /dev/null +++ b/Hearthstone/Hearthstone/Hearthstone.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Hearthstone.xcdatamodel + + diff --git a/Hearthstone/Hearthstone/Hearthstone.xcdatamodeld/Hearthstone.xcdatamodel/contents b/Hearthstone/Hearthstone/Hearthstone.xcdatamodeld/Hearthstone.xcdatamodel/contents new file mode 100644 index 0000000..bff48c5 --- /dev/null +++ b/Hearthstone/Hearthstone/Hearthstone.xcdatamodeld/Hearthstone.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Hearthstone/Hearthstone/Helpers/Extensions.swift b/Hearthstone/Hearthstone/Helpers/Extensions.swift new file mode 100644 index 0000000..02aa1d1 --- /dev/null +++ b/Hearthstone/Hearthstone/Helpers/Extensions.swift @@ -0,0 +1,450 @@ +// +// Extensions.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 28/7/22. +// + +import Foundation +import UIKit + +// - MARK: - Constants +let imageCache = NSCache() + + +// - MARK: - Hepers + +// - MARK: UIKit Extensions + +extension UICollectionView { + + func hidePlaceholderView() { + backgroundView = nil + } + + func showWatermark(_ image: UIImage? = nil, with text: String = "") { + if let watermarkImage = UIImage(named: "oops") { + backgroundView = WatermarkView(frame: bounds, with: "Oops no cards found!", image: watermarkImage) + } + } +} + +extension UICollectionView { + private var loading: UIActivityIndicatorView { + get { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.color = .primaryColor + return indicator + } + } + + func addSpinner() { + backgroundView = loading + loading.startAnimating() + } + + func hideSpinner() { + if loading.isAnimating { + loading.stopAnimating() + backgroundView = nil + } + } + +} + +extension UIViewController { + + func showInfoAlert(with message: String) { + let alert = UIAlertController(title: "Info", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) + } + + func addButtons(right: [UIBarButtonItem]? = nil, left: [UIBarButtonItem]? = nil) { + if let right = right { + navigationItem.setRightBarButtonItems(right, animated: true) + } + if let left = left { + navigationItem.setLeftBarButtonItems(left, animated: true) + } + } +} + +extension UIBarButtonItem { + + public static func set(for expression: Bool, toggledName: String, nonToggledName: String) -> UIImage? { + UIImage(systemName: expression ? toggledName : nonToggledName) + } + +} + +extension UIButton { + public static func set(for expression: Bool, toggledName: String, nonToggledName: String) -> UIImage? { + UIImage(systemName: expression ? toggledName : nonToggledName) + } +} + +extension UIButton.Configuration { + public static func typed(with text: String) -> UIButton.Configuration { + var config = UIButton.Configuration.filled() + config.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) + config.title = text + var background = UIButton.Configuration.plain().background + background.cornerRadius = 15 + background.backgroundColor = .typeColor + config.background = background + + return config + } +} + +extension UIImageView { + func addPlaceholderImage(from url: URL, with mode: UIView.ContentMode) { + + if let imgData = try? Data(contentsOf: url) { + if let img = UIImage(data: imgData) { + DispatchQueue.main.async { [weak self] in + self?.image = img + self?.contentMode = mode + } + } + } + } +} + +extension UIColor { + + static var primaryColor: UIColor { + UIColor(red: 0.29, green: 0.79, blue: 0.79, alpha: 1.0) + } + + static var accentColor: UIColor { + UIColor(red: 0.28, green: 0.21, blue: 0.11, alpha: 1.0) + } + + static var typeColor: UIColor { + UIColor(red: 0.48, green: 0.39, blue: 0.29, alpha: 1.0) + } + +} + +extension UIView { + + func addSeparator() -> UIView { + let separatorView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) + separatorView.backgroundColor = UIColor(named: "SeparatorColor") + addSubview(separatorView) + + return separatorView + } + + /// Make view rounded (dashboard item) + func cardView(of radius: CGFloat, with shadow: UIColor? = nil) { + layer.cornerRadius = radius + if let shadow = shadow { + layer.shadowColor = shadow.cgColor + layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + layer.shadowRadius = 12.0 + layer.shadowOpacity = 0.7 + } + } + + /// Add anchors to the view programmatically + func addAnchors(wAnchor: NSLayoutDimension? = nil, _ wMulti: CGFloat? = nil, + hAnchor: NSLayoutDimension? = nil,_ hMulti: CGFloat? = nil, + cXAnchor: NSLayoutXAxisAnchor? = nil, + cYAnchor: NSLayoutYAxisAnchor? = nil, + lAnchor: NSLayoutXAxisAnchor? = nil, leftConstant: CGFloat? = nil, + tAnchor: NSLayoutYAxisAnchor? = nil, topConstant: CGFloat? = nil, + rAnchor: NSLayoutXAxisAnchor? = nil, rightConstant: CGFloat? = nil, + bAnchor: NSLayoutYAxisAnchor? = nil, bottomConstant: CGFloat? = nil, + widthConstant: CGFloat? = nil, + heightConstant: CGFloat? = nil) { + + translatesAutoresizingMaskIntoConstraints = false + + + if let wAnchor = wAnchor { + widthAnchor.constraint(equalTo: wAnchor, multiplier: wMulti == nil ? 1.0 : wMulti!).isActive = true + } + + if let hAnchor = hAnchor { + heightAnchor.constraint(equalTo: hAnchor, multiplier: hMulti == nil ? 1.0 : hMulti!).isActive = true + } + + if let cXAnchor = cXAnchor { + centerXAnchor.constraint(equalTo: cXAnchor, constant: 0).isActive = true + } + + if let cYAnchor = cYAnchor { + centerYAnchor.constraint(equalTo: cYAnchor, constant: 0).isActive = true + } + + if let lAnchor = lAnchor { + leftAnchor.constraint(equalTo: lAnchor, constant: leftConstant == nil ? 0 : leftConstant!).isActive = true + + } + + if let tAnchor = tAnchor { + topAnchor.constraint(equalTo: tAnchor, constant: topConstant == nil ? 0 : topConstant!).isActive = true + + } + + if let rAnchor = rAnchor { + rightAnchor.constraint(equalTo: rAnchor, constant: rightConstant == nil ? 0 : rightConstant!).isActive = true + } + + if let bAnchor = bAnchor { + bottomAnchor.constraint(equalTo: bAnchor, constant: bottomConstant == nil ? 0 : bottomConstant!).isActive = true + } + + if let widthConstant = widthConstant { + widthAnchor.constraint(equalToConstant: widthConstant).isActive = true + } + + if let heightConstant = heightConstant { + heightAnchor.constraint(equalToConstant: heightConstant).isActive = true + } + } + +} + + +// - MARK: Foundation extensions + +extension String { + + func convertToURL() -> URL? { + URL(string: self) + } +} + +// - MARK: Custom extensions + +// - MARK: - Models Extensions + +extension CardsResponse { + // Ensure that decoding is successful for keys with whitespaces/special characters + enum CodingKeys: String, CodingKey { + case Basic = "Basic" + case Classic = "Classic" + case Promo = "Promo" + case HallofFame = "Hall of Fame" + case Naxxramas = "Naxxramas" + case GoblinsvsGnomes = "Goblins vs Gnomes" + case BlackrockMountain = "Blackrock Mountain" + case TheGrandTournament = "The Grand Tournament" + case TheLeagueofExplorers = "The League of Explorers" + case WhispersoftheOldGods = "Whispers of the Old Gods" + case OneNightinKarazhan = "One Night in Karazhan" + case MeanStreetsofGadgetzan = "Mean Streets of Gadgetzan" + case JourneytoUnGoro = "Journey to Un'Goro" + case TavernBrawl = "Tavern Brawl" + case HeroSkins = "Hero Skins" + case Missions = "Missions" + case Credits = "Credits" + case System = "System" + case Debug = "Debug" + } + + func getAllCards() -> [Card] { + Basic + Classic + Promo + HallofFame + Naxxramas + GoblinsvsGnomes + BlackrockMountain + TheGrandTournament + TheLeagueofExplorers + WhispersoftheOldGods + OneNightinKarazhan + MeanStreetsofGadgetzan + JourneytoUnGoro + TavernBrawl + HeroSkins + Missions + Credits + System + Debug + } +} + +extension CardViewModel { + + // In case new entries have "corrupt" data + static var placeholderTitle: String { + "No Name" + } +} + +extension CardViewModel { + + func getUrl() -> URL { + guard let url = self.image.convertToURL() else { + return URL(string: "https://via.placeholder.com/500x500.png?text=No+Image+Found")! + } + return url + } + +} + +extension CardViewModel { + + func isFeatured(_ card: Card) -> Bool { + + return card.rarity == "Legendary" && ((card.mechanics?.contains(["name": "Deathrattle"])) != nil) + } +} + +extension CardViewModel { + + convenience init(card: Card) { + self.init(card: card, select: {}) + self.description = (card.flavor?.isEmpty ?? true) ? "No Information to show" : card.flavor + self.type = card.type + } +} + +extension CardViewModel { + func initFavorite(completion: @escaping() -> Void) { + delegate?.initFavorite(for: self) { [weak self] exists in + self?.isFavorite = exists + completion() + } + } +} + +extension CardViewModel { + func updateFavorite() { + isFavorite = !isFavorite + delegate?.updateFavorite(for: self) + } +} + +extension CardsDataService { + enum ServiceType { + case AllCards + case Favorites + } +} + +// - MARK: - Views Extensions + + +// MARK: - HomeTabViewController +extension HomeTabViewController { + + func initView() { + view.backgroundColor = .systemBackground + tabBar.tintColor = .primaryColor + } + +} + +extension HomeTabViewController { + + func createTabNavigation(for root: UIViewController, with title: String? = "Title", and icon: String? = nil) -> UIViewController { + let navigation = UINavigationController(rootViewController: root) + if let title = title { + navigation.tabBarItem.title = title + } + if let icon = icon { + navigation.tabBarItem.image = UIImage( + systemName: icon, + withConfiguration:UIImage.SymbolConfiguration(scale: .large) + ) + } + navigation.navigationBar.prefersLargeTitles = true + root.navigationItem.title = title + root.navigationItem.largeTitleDisplayMode = .always + + return navigation + } + +} + +extension HomeTabViewController { + + func attachFavoritesService() -> FavoritesService { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + fatalError("Unable to retrieve app delegate") + } + + return FavoritesService(with: appDelegate.persistentContainer) + } + +} + +// MARK: - CardsCollectionViewController +extension CardsCollectionViewController { + + func select(_ card: Card) { + let destVC = CardViewController() + destVC.card = card + destVC.favoriteService = dataService?.favoritesService + show(destVC, sender: self) + } + +} + +// MARK: - CardViewController +extension CardViewController { + + func initDetailViewModel(_ card: Card) { + + let cardView = CardView(frame: view.bounds, for: card) + + let cardViewModel = CardViewModel(card: card) + + cardView.cardViewModel = cardViewModel + view.addSubview(cardView) + } + + func update(for favorite: Bool) { + if !favorite { + favoriteService?.save(card?.cardId ?? "") { [weak self] success in + DispatchQueue.main.async { + self?.showInfoAlert(with: success ? "Card added to Favorites" : "Unable to add card to Favorites") + self?.updateFavorite(from: success) + } + } + } else { + favoriteService?.delete(cardID: card?.cardId ?? "") { [weak self] success in + DispatchQueue.main.async { + self?.showInfoAlert(with: success ? "Card deleted from Favorites" : "Unable to delete card to Favorites") + self?.updateFavorite(from: !success) + } + + } + } + } + + private func updateFavorite(from success: Bool) { + isFavorite = success + favoriteButton.image = UIBarButtonItem.set(for: isFavorite, toggledName: "heart.fill", nonToggledName: "heart") + } + +} + +extension CardGridViewCell { + + func configure() { + cardName.text = cardViewModel.title + cardImage.load(from: cardViewModel.getUrl(), with: .scaleAspectFit) + cardViewModel.initFavorite { [weak self] in + DispatchQueue.main.async { + self?.configureFavoriteButton() + } + } + favoriteButton.addTarget(self, action: #selector(updateFavorite), for: .touchUpInside) + } + +} + +extension CardGridViewCell { + + func configureFavoriteButton() { + favoriteButton.setImage(UIButton.set(for: cardViewModel.isFavorite, toggledName: "heart.fill", nonToggledName: "heart"), for: .normal) + } +} + +extension CardGridViewCell { + @objc func updateFavorite() { + cardViewModel.updateFavorite() + configureFavoriteButton() + } +} + +extension CardView { + + func configure() { + cardImage.load(from: cardViewModel.getUrl(), with: .scaleAspectFit) + cardName.text = cardViewModel.title + cardText.text = cardViewModel.description + cardType.configuration = .typed(with: cardViewModel.type ?? "") + } + +} diff --git a/Hearthstone/Hearthstone/Info.plist b/Hearthstone/Hearthstone/Info.plist new file mode 100644 index 0000000..bc240fd --- /dev/null +++ b/Hearthstone/Hearthstone/Info.plist @@ -0,0 +1,28 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/Hearthstone/Hearthstone/Models/Card.swift b/Hearthstone/Hearthstone/Models/Card.swift new file mode 100644 index 0000000..a5d243c --- /dev/null +++ b/Hearthstone/Hearthstone/Models/Card.swift @@ -0,0 +1,56 @@ +// +// Card.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 26/7/22. +// + +import Foundation + +// For the purpose of the DEMO we make the assumption that we know the Card Categories beforehand (if API requests, the setup would be different) +struct CardsResponse: Codable { + var Basic: [Card] = Array() + var Classic: [Card] = Array() + var Promo: [Card] = Array() + var HallofFame: [Card] = Array() + var Naxxramas: [Card] = Array() + var GoblinsvsGnomes: [Card] = Array() + var BlackrockMountain: [Card] = Array() + var TheGrandTournament: [Card] = Array() + var TheLeagueofExplorers: [Card] = Array() + var WhispersoftheOldGods: [Card] = Array() + var OneNightinKarazhan: [Card] = Array() + var MeanStreetsofGadgetzan: [Card] = Array() + var JourneytoUnGoro: [Card] = Array() + var TavernBrawl: [Card] = Array() + var HeroSkins: [Card] = Array() + var Missions: [Card] = Array() + var Credits: [Card] = Array() + var System: [Card] = Array() + var Debug: [Card] = Array() +} + +struct Card: Codable { + + var cardId: String? + var name: String? + var cardSet: String? + var type: String? + var rarity: String? + var cost: Int? + var attack: Int? + var health: Int? + var text: String? + var flavor: String? + var artist: String? + var collectible: Bool? + var elite: Bool? + var playerClass: String? + var multiClassGroup: String? + var classes: [String]? = Array() + var img: String? + var imgGold: String? + var locale: String? + var mechanics: [[String: String]]? = Array() + +} diff --git a/Hearthstone/Hearthstone/SceneDelegate.swift b/Hearthstone/Hearthstone/SceneDelegate.swift new file mode 100644 index 0000000..4ece2e7 --- /dev/null +++ b/Hearthstone/Hearthstone/SceneDelegate.swift @@ -0,0 +1,30 @@ +// +// SceneDelegate.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 25/7/22. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + // Init scene programmatically to "get rid" of Storyboards + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + guard let windowScene = (scene as? UIWindowScene) else { return } + + window = UIWindow(frame: UIScreen.main.bounds) + + let homeView = HomeTabViewController() + window?.rootViewController = homeView + window?.makeKeyAndVisible() + window?.windowScene = windowScene + + } + +} + diff --git a/Hearthstone/Hearthstone/Services/CardsDataService.swift b/Hearthstone/Hearthstone/Services/CardsDataService.swift new file mode 100644 index 0000000..ee77c0c --- /dev/null +++ b/Hearthstone/Hearthstone/Services/CardsDataService.swift @@ -0,0 +1,103 @@ +// +// CardsDataService.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 4/8/22. +// + +import Foundation + + +// MARK: - Service Protocols +protocol LocalDownloadProtocol { + func getUrl(from name: String?, typed: String?) -> URL? + func getJSONString(from url: URL) throws -> String? +} + +protocol JSONConverterProtocol { + func convert(from json: String, completion: @escaping([Card]) -> Void) +} + +protocol ResultHadlerProtocol { + func handleParsed(_ cards: [Card], from view: CardsCollectionViewController?, completion: @escaping ([CardViewModel]) -> Void) + func featuresFilter(for cards: [CardViewModel]) -> [CardViewModel] +} + + +struct CardsDataService: LocalDownloadProtocol, JSONConverterProtocol, ResultHadlerProtocol { + + + // MARK: - Variables + + // Public + public let type: ServiceType + public var favoritesService: FavoritesService? + + // Private + private let extensionType = "json" + + func convert(from json: String, completion: @escaping ([Card]) -> Void) { + do { + if let fileUrl = getUrl(from: json, typed: extensionType), + let jsonString = try getJSONString(from: fileUrl), + let jsonData = jsonString.data(using: .utf8) { + let decoder = JSONDecoder() + let cardsData = try decoder.decode(CardsResponse.self, from: jsonData) + + completion(cardsData.getAllCards()) + } + } catch { + print(error) + completion([]) + } + } + + func handleParsed(_ cards: [Card], from view: CardsCollectionViewController? = nil, completion: @escaping ([CardViewModel]) -> Void) { + var viewModels = [CardViewModel]() + switch type { + case .AllCards: + for card in cards { + let cardVM = CardViewModel(card: card, select: { + view?.select(card) + }) + cardVM.delegate = view + viewModels.append(cardVM) + } + completion(viewModels) + break + case .Favorites: + favoritesService?.getFavorites { favIDs in + for favID in favIDs { + if let card = cards.first(where: { $0.cardId == favID }) { + let cardVM = CardViewModel(card: card, select: { + view?.select(card) + }) + cardVM.delegate = view + viewModels.append(cardVM) + } + } + completion(viewModels) + } + } + } + + func featuresFilter(for cards: [CardViewModel]) -> [CardViewModel] { + return cards.filter(\.isHsiaoFav) + } + + + internal func getUrl(from name: String?, typed: String?) -> URL? { + Bundle.main.url(forResource: name, withExtension: typed) + } + + internal func getJSONString(from url: URL) throws -> String? { + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + return nil + } + } + + + +} diff --git a/Hearthstone/Hearthstone/Services/FavoritesService.swift b/Hearthstone/Hearthstone/Services/FavoritesService.swift new file mode 100644 index 0000000..7b8e61e --- /dev/null +++ b/Hearthstone/Hearthstone/Services/FavoritesService.swift @@ -0,0 +1,120 @@ +// +// FavoritesService.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 12/8/22. +// + +import Foundation +import CoreData + + +protocol StoreCardProtocol { + func save(_ cardID: String, completion: @escaping(Bool) -> Void) + func delete(cardID: String, completion: @escaping(Bool) -> Void) +} + +protocol ReadCardProtocol { + func exists(with id: String, completion: @escaping(Bool) -> Void) + func getFavorites(completion: @escaping([String]) -> Void) +} + +class FavoritesService: StoreCardProtocol, ReadCardProtocol { + + private let container: NSPersistentContainer + let context: NSManagedObjectContext + + + init(with persistentContainer: NSPersistentContainer) { + container = persistentContainer + // Create context that performs on the background, in order to avoid UI lags + context = container.newBackgroundContext() + } + + func save(_ cardID: String, completion: @escaping(Bool) -> Void) { + context.perform { [weak self] in + self?.exists(with: cardID) { inDB in + if !inDB { + let newFav = Favorite(context: self!.context) + newFav.cardID = cardID + do { + try self?.context.save() + completion(true) + } catch { + debugPrint("Unable to store card with ID: \(cardID)") + completion(false) + } + } else { + debugPrint("\(cardID) already exists in DB") + completion(false) + } + } + } + } + + func delete(cardID: String, completion: @escaping(Bool) -> Void) { + context.perform { [weak self] in + // Use different approach from save, since we are not have an existing NSManagedObject (Favorite) + let fetchRequest = NSFetchRequest(entityName: "Favorite") + fetchRequest.predicate = NSPredicate(format: "cardID = %@", cardID) + fetchRequest.fetchLimit = 1 + + do { + if let favorites = try self?.context.fetch(fetchRequest) { + for favorite in favorites { + self?.context.delete(favorite as? NSManagedObject ?? NSManagedObject()) + } + try self?.context.save() + completion(true) + } else { + completion(false) + } + + } catch { + debugPrint("Unable to delete card with ID: \(cardID)") + completion(false) + } + } + } + + func exists(with id: String, completion: @escaping(Bool) -> Void) { + context.perform { [weak self] in + let request = Favorite.fetchRequest() + request.predicate = NSPredicate(format: "cardID = %@", id) + do { + if let count = try self?.context.count(for: request) { + completion(count > 0) + } else { + completion(true) + } + } catch { + debugPrint("Unable to read if exists card: \(id)") + completion(false) + } + } + } + + func getFavorites(completion: @escaping([String]) -> Void) { + context.perform { [weak self] in + let request = Favorite.fetchRequest() + do { + if let favorites = try self?.context.fetch(request) { + var cards = [String]() + for favorite in favorites { + cards.append(favorite.cardID ?? "") + } + completion(cards) + } else { + completion([]) + } + } catch { + debugPrint("Unable to fetch Favorites") + completion([]) + } + + } + } + + + +} diff --git a/Hearthstone/Hearthstone/ViewModels/CardViewModel.swift b/Hearthstone/Hearthstone/ViewModels/CardViewModel.swift new file mode 100644 index 0000000..8da2e79 --- /dev/null +++ b/Hearthstone/Hearthstone/ViewModels/CardViewModel.swift @@ -0,0 +1,39 @@ +// +// CardViewModel.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 28/7/22. +// + +import Foundation +import UIKit + +// Write Unit/UI tests for protocol +protocol UpdateFavoritesProtocol { + func initFavorite(for card: CardViewModel, completion: @escaping(Bool) -> Void) + func updateFavorite(for card: CardViewModel) +} + +class CardViewModel { + + var delegate: UpdateFavoritesProtocol? + + let cardID: String + let title: String + let image: String + var isFavorite: Bool = false + var isHsiaoFav: Bool = false + // MARK: - Collection View variables + let select: () -> Void + // MARK: - Detail View variables + var description: String? + var type: String? + + init(card: Card, select: @escaping () -> Void) { + self.cardID = card.cardId ?? "" + self.title = card.name ?? CardViewModel.placeholderTitle + self.image = card.img ?? "https://via.placeholder.com/500x500.png?text=No+Image+Found" + self.select = select + isHsiaoFav = isFeatured(card) + } +} diff --git a/Hearthstone/Hearthstone/Views/Cells/CardGridViewCell.swift b/Hearthstone/Hearthstone/Views/Cells/CardGridViewCell.swift new file mode 100644 index 0000000..42d4778 --- /dev/null +++ b/Hearthstone/Hearthstone/Views/Cells/CardGridViewCell.swift @@ -0,0 +1,60 @@ +// +// CardGridViewCell.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 31/7/22. +// + +import UIKit + +class CardGridViewCell: UICollectionViewCell { + + // MARK: - View Initialization + + lazy var cardName: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .heavy) + label.textAlignment = .center + label.textColor = .systemBlue + + return label + }() + + lazy var cardImage: LoadingImage = { + let imageView = LoadingImage() + return imageView + }() + + lazy var favoriteButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "heart"), for: .normal) + button.tintColor = .red + return button + }() + + var cardViewModel: CardViewModel! { + didSet { + configure() + } + } + + // MARK: - Initializers + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(cardName) + cardName.addAnchors(wAnchor: widthAnchor, 0.85, cXAnchor: centerXAnchor, tAnchor: topAnchor, topConstant: 8) + addSubview(cardImage) + cardImage.addAnchors(lAnchor: leftAnchor, leftConstant: 4, tAnchor: cardName.bottomAnchor, rAnchor: rightAnchor, rightConstant: -4, bAnchor: bottomAnchor, bottomConstant: -8) + addSubview(favoriteButton) + favoriteButton.addAnchors(rAnchor: rightAnchor, rightConstant: -4, bAnchor: bottomAnchor, bottomConstant: -4) + + layer.borderWidth = 0.75 + layer.borderColor = UIColor.accentColor.cgColor + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Hearthstone/Hearthstone/Views/Misc/CardView.swift b/Hearthstone/Hearthstone/Views/Misc/CardView.swift new file mode 100644 index 0000000..91b8cfd --- /dev/null +++ b/Hearthstone/Hearthstone/Views/Misc/CardView.swift @@ -0,0 +1,65 @@ +// +// CardView.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 22/8/22. +// + +import UIKit + +class CardView: UIView { + + // MARK: - Subivew Initializers + lazy var cardImage: LoadingImage = { + let imageView = LoadingImage() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + lazy var cardName: UILabel = { + let label = UILabel() + label.font = UIFont(name: "MarkerFelt-Wide", size: 24.0) + label.textAlignment = .left + return label + }() + + lazy var cardText: UILabel = { + let label = UILabel() + label.font = UIFont(name: "MarkerFelt-Thin", size: 16.0) + label.numberOfLines = 0 + return label + }() + + lazy var cardType: UIButton = { + let button = UIButton() + return button + }() + + var cardViewModel: CardViewModel! { + didSet { + configure() + } + } + + // MARK: - Initializers + init(frame: CGRect, for card: Card?) { + super.init(frame: frame) + + addSubview(cardImage) + cardImage.addAnchors(wAnchor: widthAnchor, 0.7, cXAnchor: centerXAnchor, tAnchor: topAnchor, topConstant: 32) + + let separatorView = addSeparator() + separatorView.addAnchors(wAnchor: widthAnchor, 0.95, cXAnchor: centerXAnchor, tAnchor: cardImage.bottomAnchor, topConstant: 8, heightConstant: 1.0) + addSubview(cardType) + cardType.addAnchors(lAnchor: leftAnchor, leftConstant: 12, tAnchor: separatorView.bottomAnchor, topConstant: 16) + addSubview(cardName) + cardName.addAnchors(lAnchor: leftAnchor, leftConstant: 12, tAnchor: cardType.bottomAnchor, topConstant: 4, rAnchor: rightAnchor, rightConstant: -12) + addSubview(cardText) + cardText.addAnchors(lAnchor: cardName.leftAnchor, leftConstant: 4, tAnchor: cardName.bottomAnchor, topConstant: 4, rAnchor: cardName.rightAnchor) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Hearthstone/Hearthstone/Views/Misc/LoadingImage.swift b/Hearthstone/Hearthstone/Views/Misc/LoadingImage.swift new file mode 100644 index 0000000..0ff4306 --- /dev/null +++ b/Hearthstone/Hearthstone/Views/Misc/LoadingImage.swift @@ -0,0 +1,47 @@ +// +// LoadingImage.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 29/8/22. +// + +import UIKit + +// Add UI Tests +class LoadingImage: UIImageView { + + let activityIndicator = UIActivityIndicatorView() + + func load(from url: URL, with mode: UIView.ContentMode) { + + activityIndicator.color = .primaryColor + addSubview(activityIndicator) + activityIndicator.addAnchors(cXAnchor: centerXAnchor, cYAnchor: centerYAnchor) + activityIndicator.startAnimating() + + if let cachedImage = imageCache.object(forKey: url as AnyObject) as? UIImage { + image = cachedImage + activityIndicator.stopAnimating() + return + } + + URLSession.shared.dataTask(with: url) { [weak self] data, response, error in + guard + let httpURLResponse = response as? HTTPURLResponse, + httpURLResponse.statusCode == 200, + let data = data, error == nil, + let image = UIImage(data: data) else { + self?.addPlaceholderImage(from: url, with: mode) + return + } + DispatchQueue.main.async() { + self?.image = image + self?.contentMode = mode + imageCache.setObject(image, forKey: url as AnyObject) + self?.activityIndicator.stopAnimating() + } + }.resume() + + } + +} diff --git a/Hearthstone/Hearthstone/Views/Misc/WatermarkView.swift b/Hearthstone/Hearthstone/Views/Misc/WatermarkView.swift new file mode 100644 index 0000000..20daa91 --- /dev/null +++ b/Hearthstone/Hearthstone/Views/Misc/WatermarkView.swift @@ -0,0 +1,52 @@ +// +// WatermarkView.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 10/8/22. +// + +import Foundation +import UIKit + + +class WatermarkView: UIView { + + // MARK: - Subivew Initializers + lazy var title: UILabel = { + let label = UILabel() + label.font = UIFont(name: "Helvetica", size: 16) + label.textColor = UIColor(named: "Placeholder Color") + label.textAlignment = .center + label.sizeToFit() + label.numberOfLines = 0 + + return label + }() + + lazy var imageview: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .center + imageView.accessibilityIdentifier = "oops" + return imageView + }() + + + // MARK: - Initializers + init(frame: CGRect, with text: String, image: UIImage) { + super.init(frame: frame) + + imageview.image = image + title.text = text + + addSubview(imageview) + imageview.addAnchors(cXAnchor: centerXAnchor, cYAnchor: centerYAnchor) + addSubview(title) + title.addAnchors(wAnchor: widthAnchor, 0.8, cXAnchor: centerXAnchor, tAnchor: imageview.bottomAnchor, topConstant: 8) + + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + +} diff --git a/Hearthstone/Hearthstone/Views/ViewControllers/CardViewController.swift b/Hearthstone/Hearthstone/Views/ViewControllers/CardViewController.swift new file mode 100644 index 0000000..b951baa --- /dev/null +++ b/Hearthstone/Hearthstone/Views/ViewControllers/CardViewController.swift @@ -0,0 +1,50 @@ +// +// CardViewController.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 22/8/22. +// + +import UIKit + +class CardViewController: UIViewController { + + // MARK: - Variables + + // Public + public var card: Card? + public var favoriteService: FavoritesService? + public var isFavorite: Bool = true + public var favoriteButton = UIBarButtonItem() + // Private + private var cardViewModel: CardViewModel! + + override func viewDidLoad() { + super.viewDidLoad() + + guard let card = card else { + return + } + + initDetailViewModel(card) + setupBar() + } + + // MARK: - Setup Functions + private func setupBar() { + favoriteService?.exists(with: card?.cardId ?? "") { [weak self] exists in + self?.isFavorite = exists + DispatchQueue.main.async { + self?.favoriteButton = UIBarButtonItem(image: UIBarButtonItem.set(for: self?.isFavorite ?? false, toggledName: "heart.fill", nonToggledName: "heart"), style: .plain, target: self, action: #selector(self?.toggleFavorite)) + self?.favoriteButton.tintColor = .red + self?.favoriteButton.accessibilityIdentifier = "addToFavorites" + self?.addButtons(right: [self?.favoriteButton ?? UIBarButtonItem()]) + } + } + } + + // MARK: - Obj-C Functions + @objc func toggleFavorite() { + update(for: isFavorite) + } +} diff --git a/Hearthstone/Hearthstone/Views/ViewControllers/CardsCollectionViewController.swift b/Hearthstone/Hearthstone/Views/ViewControllers/CardsCollectionViewController.swift new file mode 100644 index 0000000..85a0318 --- /dev/null +++ b/Hearthstone/Hearthstone/Views/ViewControllers/CardsCollectionViewController.swift @@ -0,0 +1,119 @@ +// +// CardsCollectionViewController.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 29/7/22. +// + +import UIKit + +class CardsCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UpdateFavoritesProtocol { + + // MARK: - Variables + + // Public + public var dataService: CardsDataService? + + // Private + private var filteredCards = [CardViewModel]() + private var isFeatured: Bool = false + private var hsiaoFavButton = UIBarButtonItem() + + override func viewDidLoad() { + super.viewDidLoad() + + collectionView.register(CardGridViewCell.self, forCellWithReuseIdentifier: "CardCell") + setupBar() + } + + override func viewDidAppear(_ animated: Bool) { + refreshData() + } + + // MARK: - CollectionView Functions + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + filteredCards.count + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 150, height: 250) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCell", for: indexPath) as? CardGridViewCell else { + return UICollectionViewCell() + } + if filteredCards.isEmpty { + return cell + } + cell.cardViewModel = filteredCards[indexPath.row] + + return cell + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let card = filteredCards[indexPath.row] + card.select() + } + + // MARK: - Setup Functions + private func setupBar() { + hsiaoFavButton = UIBarButtonItem(image: UIImage(systemName: "star.fill"), style: .plain, target: self, action: #selector(filterFeatured)) + addButtons(right: [hsiaoFavButton]) + } + + // MARK: - Update Functions + private func refreshData() { + collectionView.addSpinner() + DispatchQueue.global().async { [weak self] in + self?.dataService?.convert(from: "cards") { items in + self?.dataService?.handleParsed(items, from: self) { itemsVM in + self?.filteredCards = itemsVM + if let isFeatured = self?.isFeatured { + if isFeatured { + self?.filteredCards = self?.dataService?.featuresFilter(for: self?.filteredCards ?? []) ?? [] + } + } + DispatchQueue.main.async { + self?.collectionView.hideSpinner() + if self?.filteredCards.count == 0 { + self?.collectionView.showWatermark(UIImage(named: "oops"), with: "Oops no cards found!") + } else { + self?.collectionView.hidePlaceholderView() + } + self?.collectionView.reloadData() + } + } + } + } + } + + // MARK: - UpdateFavoritesProtocol functions + + func initFavorite(for card: CardViewModel, completion: @escaping(Bool) -> Void) { + dataService?.favoritesService?.exists(with: card.cardID) { isFavorite in + completion(isFavorite) + } + } + + func updateFavorite(for card: CardViewModel) { + if card.isFavorite { + dataService?.favoritesService?.save(card.cardID) { _ in } + } else { + dataService?.favoritesService?.delete(cardID: card.cardID) { _ in } + } + refreshData() + } + + // MARK: - Obj-C Functions + @objc func filterFeatured() { + isFeatured = !isFeatured + hsiaoFavButton.image = UIImage(systemName: isFeatured ? "star.slash.fill" : "star.fill") + refreshData() + } + +} diff --git a/Hearthstone/Hearthstone/Views/ViewControllers/HomeTabViewController.swift b/Hearthstone/Hearthstone/Views/ViewControllers/HomeTabViewController.swift new file mode 100644 index 0000000..177bf9a --- /dev/null +++ b/Hearthstone/Hearthstone/Views/ViewControllers/HomeTabViewController.swift @@ -0,0 +1,29 @@ +// +// HomeTabViewController.swift +// Hearthstone +// +// Created by Stavros Tsikinas on 26/7/22. +// + +import UIKit + +class HomeTabViewController: UITabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + + initView() + + let favoritesService = attachFavoritesService() + + let allVC = CardsCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout()) + allVC.dataService = CardsDataService(type: .AllCards, favoritesService: favoritesService) + let favVC = CardsCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout()) + favVC.dataService = CardsDataService(type: .Favorites, favoritesService: favoritesService) + + viewControllers = [ + createTabNavigation(for: allVC, with: "Cards", and: "lanyardcard.fill"), + createTabNavigation(for: favVC, with: "Favorites", and: "star.fill") + ] + } +} diff --git a/Hearthstone/HearthstoneTests/CardsTests/Helpers/CardViewModelHelper.swift b/Hearthstone/HearthstoneTests/CardsTests/Helpers/CardViewModelHelper.swift new file mode 100644 index 0000000..e4a0f71 --- /dev/null +++ b/Hearthstone/HearthstoneTests/CardsTests/Helpers/CardViewModelHelper.swift @@ -0,0 +1,18 @@ +// +// CardViewModelHelper.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 26/8/22. +// + +@testable import Hearthstone + +func testCardViewModel() -> CardViewModel { + CardViewModel(card: testCard(cardID: "TestCardID", name: "A test card")) +} + +func testCardViewModel(favorite: Bool) -> CardViewModel { + let cardVM = testCardViewModel() + cardVM.isFavorite = favorite + return cardVM +} diff --git a/Hearthstone/HearthstoneTests/CardsTests/Helpers/CardsHelper.swift b/Hearthstone/HearthstoneTests/CardsTests/Helpers/CardsHelper.swift new file mode 100644 index 0000000..a0f9908 --- /dev/null +++ b/Hearthstone/HearthstoneTests/CardsTests/Helpers/CardsHelper.swift @@ -0,0 +1,50 @@ +// +// CardsHelper.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 28/7/22. +// + +import Foundation +@testable import Hearthstone + +/// +/// Initializers for dummy cards. +/// +func testCard(cardID: String? = nil, name: String? = nil, img: String? = nil, rarity: String? = nil, mechanics: [[String: String]]? = nil) -> Card { + Card(cardId: nil, name: name, cardSet: nil, type: nil, rarity: rarity, cost: nil, attack: nil, health: nil, text: nil, flavor: nil, artist: nil, collectible: nil, elite: nil, playerClass: nil, multiClassGroup: nil, classes: nil, img: img, imgGold: nil, locale: nil, mechanics: mechanics) +} + +func testCardArray() -> [Card] { + var cards = [Card]() + for i in 0...5 { + + cards.append(testCard(name: "Card #\(i)", + img: "a://whatever.image", + rarity: i % 2 == 0 ? "Legendary" : "Basic", + mechanics: i % 2 == 0 ? [["name": "Deathrattle"]] : nil + )) + } + + return cards +} + +/// +/// Test helper method of retrieving the json url. +/// +func getUrlFile(from name: String?, typed: String?) -> URL? { + Bundle.main.url(forResource: name, withExtension: typed) +} + +/// +/// Test helper method of converting url to json string. +/// +func getJSONString(from url: URL) throws -> String? { + + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + return nil + } + +} diff --git a/Hearthstone/HearthstoneTests/CardsTests/TestAllCardService.swift b/Hearthstone/HearthstoneTests/CardsTests/TestAllCardService.swift new file mode 100644 index 0000000..f511419 --- /dev/null +++ b/Hearthstone/HearthstoneTests/CardsTests/TestAllCardService.swift @@ -0,0 +1,71 @@ +// +// TestAllCardService.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 8/8/22. +// + +import XCTest +@testable import Hearthstone + +class TestAllCardService: XCTestCase { + + var sut: CardsDataService! + var elements: [Card]? + + override func setUpWithError() throws { + try super.setUpWithError() + sut = CardsDataService(type: .AllCards) + elements = testCardArray() + } + + override func tearDownWithError() throws { + sut = nil + elements = nil + try super.tearDownWithError() + } + + func testServiceIsCorrectType() { + + XCTAssertTrue(sut.type == CardsDataService.ServiceType.AllCards, "Service should be of type: \(CardsDataService.ServiceType.AllCards)") + + } + + func testServiceReturnsAllCards() { + + let expectation = expectation(description: "testServiceReturnsAllCards") + + sut.handleParsed(elements ?? []) { [weak self] cardVMs in + XCTAssertNotEqual(cardVMs.count, 0, "Service should return value > 0") + XCTAssertEqual(cardVMs.count, self?.elements?.count ?? 0, "Service should return as many VMs as the input") + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0) { error in + XCTAssertNil(error, "testServiceReturnsAllCards unsuccessful") + } + + } + + func testServiceReturnsFeaturedOnly() { + + let expectation = expectation(description: "testServiceReturnsFeaturedOnly") + + sut.handleParsed(elements ?? []) { [weak self] cardVMs in + + if let featured = self?.sut.featuresFilter(for: cardVMs) { + XCTAssertNotEqual(featured.count, 0, "Dummy data have provided 3 cards with Hsiao Favorite elements") + XCTAssertEqual(featured.count, 3, "Dummy data have provided 3 cards with Hsiao Favorite elements") + } else { + XCTFail() + } + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0) { error in + XCTAssertNil(error, "testServiceReturnsFeaturedOnly unsuccessful") + } + + } + +} diff --git a/Hearthstone/HearthstoneTests/CardsTests/TestCardViewModel.swift b/Hearthstone/HearthstoneTests/CardsTests/TestCardViewModel.swift new file mode 100644 index 0000000..5cc6e1a --- /dev/null +++ b/Hearthstone/HearthstoneTests/CardsTests/TestCardViewModel.swift @@ -0,0 +1,41 @@ +// +// TestCardViewModel.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 28/7/22. +// + +import XCTest +@testable import Hearthstone + +class TestCardViewModel: XCTestCase { + + override func setUpWithError() throws { + super.setUp() + + } + + func testCardViewModelInitialization() { + let cardVM = CardViewModel(card: testCard(name: "a Card", img: "https://www.cardlink.card/card.png"), select: {}) + + XCTAssertNotNil(cardVM.title, "No title found on cardVM") + XCTAssertNotNil(cardVM.image, "No image path found on cardVM") + } + + func testCardViewModelTitlePlaceholder() { + + let cardVM = CardViewModel(card: testCard(), select: {}) + + XCTAssertEqual(cardVM.title, CardViewModel.placeholderTitle, "Placeholder title should've been provided on an empty Card") + + } + + func testCardViewModelImageUrlPlaceholder() { + let cardVM = CardViewModel(card: testCard(name: "a Card"), select: {}) + + XCTAssertEqual(cardVM.getUrl(), + URL(string: "https://via.placeholder.com/500x500.png?text=No+Image+Found")!, + "Placeholder Image should have a valid URL") + } + +} diff --git a/Hearthstone/HearthstoneTests/CardsTests/TestCardsFile.swift b/Hearthstone/HearthstoneTests/CardsTests/TestCardsFile.swift new file mode 100644 index 0000000..8a1f786 --- /dev/null +++ b/Hearthstone/HearthstoneTests/CardsTests/TestCardsFile.swift @@ -0,0 +1,50 @@ +// +// TestCardsFileExists.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 27/7/22. +// + +import XCTest + +class TestCardsFile: XCTestCase { + + override func setUpWithError() throws { + super.setUp() + } + + func testJSONFileExists() { + guard let _ = getUrlFile(from: "cards", typed: "json") else { + XCTFail("File is Missing") + return + } + } + + func testJSONFileIsValid() { + guard let cardsUrl = getUrlFile(from: "cards", typed: "json") else { + XCTFail("File is missing") + return + } + + do { + if let cardsJSON = try getJSONString(from: cardsUrl) { + guard let cardsData = cardsJSON.data(using: .utf8) else { + XCTFail("Cards data can't be created from JSON") + return + } + + guard let cardsDictionary = try JSONSerialization.jsonObject(with: cardsData) as? [String: Any] else { + XCTFail("Cards Dictionary can't be created from Data object") + return + } + + XCTAssertTrue(JSONSerialization.isValidJSONObject(cardsDictionary)) + } else { + XCTFail("Can't parse cards.json into JSON string") + } + } catch { + XCTFail("Cards data can't be created") + } + } + +} diff --git a/Hearthstone/HearthstoneTests/CardsTests/TestDetailCardViewModel.swift b/Hearthstone/HearthstoneTests/CardsTests/TestDetailCardViewModel.swift new file mode 100644 index 0000000..9e55f87 --- /dev/null +++ b/Hearthstone/HearthstoneTests/CardsTests/TestDetailCardViewModel.swift @@ -0,0 +1,38 @@ +// +// TestDetailCardViewModel.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 23/8/22. +// + +import XCTest +@testable import Hearthstone + +class TestDetailCardViewModel: XCTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + } + + func testDetailCardViewModelType() { + var card = Card() + card.type = "Hero" + + let vm = CardViewModel(card: card) + + XCTAssertTrue(vm.type != nil, "Card type has been defined and is: 'Hero'") + + } + + func testDetailCardViewModelDescription() { + let card = Card() + let vm = CardViewModel(card: card) + + XCTAssert(vm.description == "No Information to show", "Description should've been: 'No Information to show'") + } + +} diff --git a/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/Helpers/MockUpdateFavoritesProtocol.swift b/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/Helpers/MockUpdateFavoritesProtocol.swift new file mode 100644 index 0000000..57a39e5 --- /dev/null +++ b/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/Helpers/MockUpdateFavoritesProtocol.swift @@ -0,0 +1,58 @@ +// +// MockUpdateFavoritesProtocol.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 26/8/22. +// + +import XCTest +@testable import Hearthstone + +class MockUpdateFavoritesProtocol: UpdateFavoritesProtocol { + + var isFavorite: Bool? + + private let dbService: TestCoreDataService! + private let testCase: XCTestCase + // For the updating tests + private var expectation: XCTestExpectation? + + init(for testCase: XCTestCase, with dbService: TestCoreDataService) { + self.testCase = testCase + self.dbService = dbService + } + + // MAR: - Helpers + func expectDidBecomeFavorite() { + expectation = testCase.expectation(description: "expectDidBecomeFavorite") + } + + // MARK: - UpdateFavoritesProtocol functions + func initFavorite(for card: CardViewModel, completion: @escaping (Bool) -> Void) { + dbService.exists(with: card.cardID) { isFavorite in + completion(isFavorite) + } + } + + func updateFavorite(for card: CardViewModel) { + if card.isFavorite { + dbService.save(card.cardID) { [weak self] saved in + if self?.expectation != nil { + self?.isFavorite = saved + self?.expectation?.fulfill() + self?.expectation = nil + } + } + } else { + dbService.delete(cardID: card.cardID) { [weak self] deleted in + if self?.expectation != nil { + self?.isFavorite = !deleted + self?.expectation?.fulfill() + self?.expectation = nil + } + } + } + } + + +} diff --git a/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/TestInitFavoritesProtocol.swift b/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/TestInitFavoritesProtocol.swift new file mode 100644 index 0000000..1b65466 --- /dev/null +++ b/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/TestInitFavoritesProtocol.swift @@ -0,0 +1,64 @@ +// +// TestInitFavoritesProtocol.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 26/8/22. +// + +import XCTest +@testable import Hearthstone + +class TestInitFavoritesProtocol: XCTestCase { + + var testCoreData: TestCoreDataService! + + override func setUpWithError() throws { + try super.setUpWithError() + testCoreData = TestCoreDataService() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + testCoreData = nil + } + + func testCardViewModelInitIsFavorite() { + + let mockUpdater = MockUpdateFavoritesProtocol(for: self, with: testCoreData) + + let cardVM = testCardViewModel() + cardVM.delegate = mockUpdater + + let expectation = expectation(description: "testCardViewModelInitIsFavorite") + + testCoreData.save(cardVM.cardID) { saved in + XCTAssertTrue(saved) + cardVM.initFavorite { + XCTAssertTrue(cardVM.isFavorite) + expectation.fulfill() + } + } + + waitForExpectations(timeout: 3.0) { error in + XCTAssertNil(error, "testCardViewModelInitIsFavorite unsuccessful") + } + } + + func testCardViewModelInitIsntFavorite() { + let mockUpdater = MockUpdateFavoritesProtocol(for: self, with: testCoreData) + + let cardVM = testCardViewModel() + cardVM.delegate = mockUpdater + + let expectation = expectation(description: "testCardViewModelInitIsntFavorite") + cardVM.initFavorite { + XCTAssertFalse(cardVM.isFavorite) + expectation.fulfill() + } + + waitForExpectations(timeout: 3.0) { error in + XCTAssertNil(error, "testCardViewModelInitIsntFavorite unsuccessful") + } + } + +} diff --git a/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/TestUpdateFavoritesProtocol.swift b/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/TestUpdateFavoritesProtocol.swift new file mode 100644 index 0000000..8f29bbb --- /dev/null +++ b/Hearthstone/HearthstoneTests/FavoritesTests/CardViewModel+Favorites/TestUpdateFavoritesProtocol.swift @@ -0,0 +1,61 @@ +// +// TestUpdateFavoritesProtocol.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 26/8/22. +// + +import XCTest +@testable import Hearthstone + +class TestUpdateFavoritesProtocol: XCTestCase { + + var testCoreData: TestCoreDataService! + + override func setUpWithError() throws { + try super.setUpWithError() + testCoreData = TestCoreDataService() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + testCoreData = nil + } + + func testViewModelBecameFavorite() throws { + let mockUpdater = MockUpdateFavoritesProtocol(for: self, with: testCoreData) + + let cardVM = testCardViewModel() + cardVM.delegate = mockUpdater + + mockUpdater.expectDidBecomeFavorite() + cardVM.updateFavorite() + + waitForExpectations(timeout: 3.0) { error in + XCTAssertNil(error, "testViewModelBecameFavorite unsuccessful") + } + + let didBecomeFavorite = try XCTUnwrap(mockUpdater.isFavorite, "Unable to unwrap isFavorite from MockUpdateFavoritesProtocol") + XCTAssertTrue(didBecomeFavorite, "CardViewModel's isFavorite param should've been true.") + + } + + func testViewModelDeletedFromFavorites() throws { + let mockUpdater = MockUpdateFavoritesProtocol(for: self, with: testCoreData) + + let cardVM = testCardViewModel(favorite: true) + cardVM.delegate = mockUpdater + + mockUpdater.expectDidBecomeFavorite() + cardVM.updateFavorite() + + waitForExpectations(timeout: 3.0) { error in + XCTAssertNil(error, "testViewModelDeletedFromFavorites unsuccessful") + } + + let isFavorite = try XCTUnwrap(mockUpdater.isFavorite, "Unable to unwrap isFavorite from MockUpdateFavoritesProtocol") + XCTAssertFalse(isFavorite, "CardViewModel's isFavorite param should've been false.") + + } + +} diff --git a/Hearthstone/HearthstoneTests/FavoritesTests/Helpers/TestCoreDataService.swift b/Hearthstone/HearthstoneTests/FavoritesTests/Helpers/TestCoreDataService.swift new file mode 100644 index 0000000..2be34e1 --- /dev/null +++ b/Hearthstone/HearthstoneTests/FavoritesTests/Helpers/TestCoreDataService.swift @@ -0,0 +1,56 @@ +// +// TestCoreDataService.swift +// HearthstoneTests +// +// Created by Stavros Tsikinas on 24/8/22. +// + + +import CoreData +@testable import Hearthstone + +class TestCoreDataService: FavoritesService { + + init() { + let inMemoryDescription = NSPersistentStoreDescription() + // Create inMemoryStoreType for testing purposes, to avoid "messing" with real DB + inMemoryDescription.type = NSInMemoryStoreType + + let container = NSPersistentContainer(name: "Hearthstone") + + container.persistentStoreDescriptions = [inMemoryDescription] + + container.loadPersistentStores { _, error in + if let error = error { + debugPrint("Couldn't load stores with error: \(error.localizedDescription)") + } + } + + super.init(with: container) + + } + + /// Helper function to add multiple cards to favorites, not necessary to project though + func addCards(of count: Int, completion: @escaping(Bool) -> Void) { + var cardsArray = [String]() + for i in 0.. 0 { + collectionView.cells.element(boundBy: indexPath.row).tap() + let title = NSPredicate(format: "label BEGINSWITH 'AFK'") + app.staticTexts.element(matching: title) + app.navigationBars.buttons["Cards"].tap() + } + } + + func testAddFavoriteButton() { + + let indexPath = IndexPath(row: 2, section: 0) + + XCTAssertTrue(app.staticTexts["Cards"].exists) + let collectionView = app.collectionViews.element.firstMatch + if collectionView.cells.count > 0 { + // Move to detail view + collectionView.cells.element(boundBy: indexPath.row).tap() + // Tap add to Favorites + app.navigationBars.buttons["addToFavorites"].tap() + + // Validate save successful + let alert = app.alerts.firstMatch + let alertExpectation = alert.waitForExistence(timeout: 10) + XCTAssertTrue(alertExpectation, "Added to Favorites alert should've been present") + let alertDescription = alert.staticTexts["Card added to Favorites"] + XCTAssertTrue(alertDescription.exists, "Added to Favorites should've been successful") + alert.buttons["OK"].tap() + } + } + + func testDeleteFavoriteButton() { + + let indexPath = IndexPath(row: 2, section: 0) + + XCTAssertTrue(app.staticTexts["Cards"].exists) + let collectionView = app.collectionViews.element.firstMatch + if collectionView.cells.count > 0 { + // Move to detail view + collectionView.cells.element(boundBy: indexPath.row).tap() + // Tap add to Favorites + app.navigationBars.buttons["addToFavorites"].tap() + + // Validate delete successful + let alert = app.alerts.firstMatch + let alertExpectation = alert.waitForExistence(timeout: 10) + XCTAssertTrue(alertExpectation, "Deleted from Favorites alert should've been present") + let alertDescription = alert.staticTexts["Card deleted from Favorites"] + XCTAssertTrue(alertDescription.exists, "Deleted from Favorites should've been successful") + alert.buttons["OK"].tap() + } + } + +} diff --git a/Hearthstone/HearthstoneUITests/NavigationTests/TestTabNavigation.swift b/Hearthstone/HearthstoneUITests/NavigationTests/TestTabNavigation.swift new file mode 100644 index 0000000..9fde331 --- /dev/null +++ b/Hearthstone/HearthstoneUITests/NavigationTests/TestTabNavigation.swift @@ -0,0 +1,38 @@ +// +// TestTabNavigation.swift +// HearthstoneUITests +// +// Created by Stavros Tsikinas on 29/8/22. +// + +import XCTest + +class TestTabNavigation: XCTestCase { + let app = XCUIApplication() + + override func setUpWithError() throws { + try super.setUpWithError() + + app.launch() + continueAfterFailure = false + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + } + + func testInitialTabSelected() { + let tabBar = app.tabBars.firstMatch + if tabBar.exists { + XCTAssertTrue(app.staticTexts["Cards"].exists, "Cards tab should've been selected") + } + } + + func testChangeTabSelected() { + let tabBar = app.tabBars.firstMatch + if tabBar.exists { + tabBar.buttons["Favorites"].tap() + XCTAssertTrue(app.staticTexts["Favorites"].exists, "Favorites tab should've been selected") + } + } +} diff --git a/Hearthstone/HearthstoneUITests/ViewsTests/TestWatermark.swift b/Hearthstone/HearthstoneUITests/ViewsTests/TestWatermark.swift new file mode 100644 index 0000000..ad5e583 --- /dev/null +++ b/Hearthstone/HearthstoneUITests/ViewsTests/TestWatermark.swift @@ -0,0 +1,35 @@ +// +// TestWatermark.swift +// HearthstoneUITests +// +// Created by Stavros Tsikinas on 29/8/22. +// + +import XCTest + +class TestWatermark: XCTestCase { + + let app = XCUIApplication() + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + app.launch() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + } + + func testWatermarkImageOnEmptyView() { + let tabBar = app.tabBars.firstMatch + if tabBar.exists { + tabBar.buttons["Favorites"].tap() + XCTAssertTrue(app.staticTexts["Favorites"].exists, "Favorites tab should've been selected") + let collectionView = app.collectionViews.element.firstMatch + if collectionView.cells.count == 0 { + XCTAssertTrue(collectionView.images["oops"].waitForExistence(timeout: 3.0)) + } + } + } +} diff --git a/Hearthstone/README-TSIKINAS.md b/Hearthstone/README-TSIKINAS.md new file mode 100644 index 0000000..d3c51a1 --- /dev/null +++ b/Hearthstone/README-TSIKINAS.md @@ -0,0 +1,29 @@ +# Stavros Tsikinas (iOS Developer) +Hi! The source code provided is an attempt I did to build an app based on **Hearthstone Cards**. The following *Markdown* file is meant to explain the app. + +## App UI/UX +The application is based on the KLM Houses app to an extent. There is a collection view with cells showing: + + - Card Image + - Card Title + - Favorite Button + +There are 2 collection screens on the app: + 1. All Cards + 2. Favourite Cards + +In both collections, the user is able to tap the **magic** Hsiao Featured button, that filters the cards and shows only the Hsiao's favourite ones. + +The **Detail View** presents the selected card with the description and also the *type*. The user is able to add/remove the card to favourites, via a UIBarButtonItem. + +## Data +The data (cards) are retrieved from the *"cards.json"* file that was provided in the master branch. An assumption that all card categories are known, therefore the *"CardsResponse"* struct is constructed the way it is. + +## Persist Data +The cards data is persisted via the file explained in the previous section. In order to persist favourite cards, **CoreData** is used. The choice was based on the following: + - is a native framework + - is easier expandable to large DBs + - could be expanded to store large amount of data and/or images, compared to **UserDefaults** (scalability) + +## Version Control +Trunk-based development was followed, since the project consisted of 1 developer and the app was starting up. diff --git a/README.md b/Hearthstone/README.md similarity index 100% rename from README.md rename to Hearthstone/README.md