├── .gitignore ├── App_Preview_Giraffe_Screens.png ├── Cartfile ├── Cartfile.resolved ├── Giraffe-iOS ├── Giraffe-iOS.xcodeproj │ └── project.pbxproj ├── Giraffe-iOS │ ├── AnimatedImageCell.swift │ ├── AnimatedImageCollectionViewController.swift │ ├── AnimatedImageViewModel.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 120x120_Giraffe_Icon-60@2x.png │ │ │ ├── 120x120_Giraffe_Icon-Small-40@3x.png │ │ │ ├── 152x152_Giraffe_Icon-76@2x.png │ │ │ ├── 167x167_Giraffe@2x.png │ │ │ ├── 180x180_Giraffe_Icon-60@3x.png │ │ │ ├── 29x29_Giraffe_Icon-Small.png │ │ │ ├── 40x40_Giraffe_Icon-Small-40.png │ │ │ ├── 58x58_Giraffe_Icon-Small@2x-1.png │ │ │ ├── 58x58_Giraffe_Icon-Small@2x.png │ │ │ ├── 76x76_Giraffe_Icon-76.png │ │ │ ├── 80x80_Giraffe_Icon-Small-40@2x-1.png │ │ │ ├── 80x80_Giraffe_Icon-Small-40@2x.png │ │ │ ├── 87x87_Giraffe_Icon-Small@3x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── GiraffeIsDisappointed.imageset │ │ │ ├── Contents.json │ │ │ ├── notfound@2x.png │ │ │ └── notfound@3x.png │ │ ├── GiraffeIsThinking.imageset │ │ │ ├── Contents.json │ │ │ ├── loading@2x.png │ │ │ └── loading@3x.png │ │ ├── GiraffeLogo.imageset │ │ │ ├── Contents.json │ │ │ ├── Giraffe_Logo.png │ │ │ ├── Giraffe_Logo@2x.png │ │ │ └── Giraffe_Logo@3x.png │ │ ├── GiraffeNeckLong.imageset │ │ │ ├── Contents.json │ │ │ ├── GiraffeNeck.png │ │ │ ├── GiraffeNeck@2x.png │ │ │ └── GiraffeNeck@3x.png │ │ ├── cancelsearch-highlighted.imageset │ │ │ ├── Contents.json │ │ │ ├── cancelsearch-highlighted@2x.png │ │ │ └── cancelsearch-highlighted@3x.png │ │ ├── cancelsearch-normal.imageset │ │ │ ├── Contents.json │ │ │ ├── cancelsearch-normal@2x.png │ │ │ └── cancelsearch-normal@3x.png │ │ ├── family.imageset │ │ │ ├── Contents.json │ │ │ ├── family@2x.png │ │ │ └── family@3x.png │ │ ├── familyhl.imageset │ │ │ ├── Contents.json │ │ │ ├── familyhl@2x.png │ │ │ └── familyhl@3x.png │ │ ├── mainsearch.imageset │ │ │ ├── Contents.json │ │ │ ├── mainsearch@2x.png │ │ │ └── mainsearch@3x.png │ │ ├── playpause.imageset │ │ │ ├── Contents.json │ │ │ ├── playpause@2x.png │ │ │ └── playpause@3x.png │ │ ├── searchbar.imageset │ │ │ ├── Contents.json │ │ │ ├── searchbar@2x.png │ │ │ └── searchbar@3x.png │ │ └── trending.imageset │ │ │ ├── Contents.json │ │ │ ├── trending@2x.png │ │ │ └── trending@3x.png │ ├── Base.lproj │ │ ├── InfoPlist.strings │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── BaseViewController.swift │ ├── CommonUserInterface.swift │ ├── Image+RAC.swift │ ├── Info.plist │ ├── ModelType.swift │ ├── RACHelpers.swift │ ├── SearchResult.swift │ ├── SearchResultViewController.swift │ ├── SearchResultViewModel.swift │ ├── Trending.swift │ ├── TrendingViewController.swift │ ├── TrendingViewModel.swift │ ├── UIColor+Giraffe.swift │ ├── UIColor+HEX.swift │ ├── UIFont+Giraffe.swift │ ├── UIKitExtensions.swift │ ├── UIViewController+ViewType.swift │ ├── ViewModelType.swift │ ├── ViewType.swift │ └── en.lproj │ │ └── InfoPlist.strings ├── Giraffe-iOSTests │ ├── Giraffe_iOSTests.swift │ └── Info.plist └── Giraffe-iOSUITests │ ├── Giraffe_iOSUITests.swift │ └── Info.plist ├── Giraffe.xcworkspace └── contents.xcworkspacedata ├── GiraffeKit ├── GiraffeKit.xcodeproj │ └── project.pbxproj ├── GiraffeKit │ ├── Decodable.swift │ ├── Error.swift │ ├── Framable.swift │ ├── GiraffeKit.h │ ├── Info.plist │ ├── Item.swift │ ├── NSDateFormatter+GiraffeKit.swift │ ├── Parameters.swift │ ├── Response+Decodable.swift │ ├── Response.swift │ ├── SearchService.swift │ ├── Service.swift │ └── TrendingService.swift └── GiraffeKitTests │ ├── GiraffeKitTests.swift │ ├── Info.plist │ ├── NSDateFormatter+GiraffeKitTests.swift │ └── ServiceTests.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | Carthage/Checkouts 51 | Carthage/Build 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 59 | 60 | fastlane/report.xml 61 | fastlane/Preview.html 62 | fastlane/screenshots 63 | fastlane/test_output -------------------------------------------------------------------------------- /App_Preview_Giraffe_Screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/App_Preview_Giraffe_Screens.png -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "Flipboard/FLAnimatedImage" ~> 1.0.12 2 | github "ReactiveCocoa/ReactiveCocoa" == 4.2.2 3 | github "johnsundell/unbox" ~> 1.8 4 | github "evgeniyd/Nuke" "Swift_2_3" 5 | github "evgeniyd/Nuke-AnimatedImage-Plugin" "Swift_2_3" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Flipboard/FLAnimatedImage" "1.0.12" 2 | github "evgeniyd/Nuke" "ad7c23eea641b6b7b03a277d2bf7fb8788626917" 3 | github "antitypical/Result" "2.1.3" 4 | github "johnsundell/unbox" "1.9" 5 | github "ReactiveCocoa/ReactiveCocoa" "v4.2.2" 6 | github "evgeniyd/Nuke-AnimatedImage-Plugin" "48e5b3ba1aee97047d89bbfd2207a262c50e2f1f" 7 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 30061B891D29209800ABEF7F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30061B881D29209800ABEF7F /* AppDelegate.swift */; }; 11 | 30061B8B1D29209800ABEF7F /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30061B8A1D29209800ABEF7F /* TrendingViewController.swift */; }; 12 | 30061B8E1D29209800ABEF7F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 30061B8C1D29209800ABEF7F /* Main.storyboard */; }; 13 | 30061B901D29209800ABEF7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 30061B8F1D29209800ABEF7F /* Assets.xcassets */; }; 14 | 30061B931D29209800ABEF7F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 30061B911D29209800ABEF7F /* LaunchScreen.storyboard */; }; 15 | 30061B9E1D29209800ABEF7F /* Giraffe_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30061B9D1D29209800ABEF7F /* Giraffe_iOSTests.swift */; }; 16 | 30061BA91D29209800ABEF7F /* Giraffe_iOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30061BA81D29209800ABEF7F /* Giraffe_iOSUITests.swift */; }; 17 | 30061C021D29D3A600ABEF7F /* FLAnimatedImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30061C011D29D3A600ABEF7F /* FLAnimatedImage.framework */; }; 18 | 301593091D326D21009813E5 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 301593071D326D21009813E5 /* ReactiveCocoa.framework */; }; 19 | 3015930A1D326D21009813E5 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 301593081D326D21009813E5 /* Result.framework */; }; 20 | 30305A3C1D3041B00059919F /* UIFont+Giraffe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30305A3B1D3041B00059919F /* UIFont+Giraffe.swift */; }; 21 | 30305A3F1D3052F60059919F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 30305A411D3052F60059919F /* InfoPlist.strings */; }; 22 | 30305A431D3053940059919F /* AnimatedImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30305A421D3053940059919F /* AnimatedImageCell.swift */; }; 23 | 306B61E41D4576100001EDB2 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306B61E31D4576100001EDB2 /* BaseViewController.swift */; }; 24 | 3089E8C91D2DA88700726903 /* GiraffeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3089E8C81D2DA88700726903 /* GiraffeKit.framework */; }; 25 | 3089E8CA1D2DA88700726903 /* GiraffeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3089E8C81D2DA88700726903 /* GiraffeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 26 | 30A0C2271D3C32B300627ECB /* AnimatedImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A0C2261D3C32B300627ECB /* AnimatedImageViewModel.swift */; }; 27 | 30A3B90B1D2F893B00ADEF41 /* AnimatedImageCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A3B90A1D2F893B00ADEF41 /* AnimatedImageCollectionViewController.swift */; }; 28 | 30A3B90D1D2F8A3A00ADEF41 /* CommonUserInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A3B90C1D2F8A3A00ADEF41 /* CommonUserInterface.swift */; }; 29 | 30B3B7651D3A9D960077BCAA /* Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B7641D3A9D960077BCAA /* Trending.swift */; }; 30 | 30B3B7671D3A9F140077BCAA /* TrendingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B7661D3A9F140077BCAA /* TrendingViewModel.swift */; }; 31 | 30B3B76B1D3A9F600077BCAA /* RACHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B76A1D3A9F600077BCAA /* RACHelpers.swift */; }; 32 | 30B3B76F1D3AA16C0077BCAA /* ViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B76E1D3AA16C0077BCAA /* ViewType.swift */; }; 33 | 30B3B7711D3AA17E0077BCAA /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B7701D3AA17E0077BCAA /* ViewModelType.swift */; }; 34 | 30B3B7731D3AA1C90077BCAA /* UIViewController+ViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B7721D3AA1C90077BCAA /* UIViewController+ViewType.swift */; }; 35 | 30B3B7751D3AC0F70077BCAA /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B3B7741D3AC0F70077BCAA /* UIKitExtensions.swift */; }; 36 | 30CA4C2D1D302A0100A36F69 /* UIColor+Giraffe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CA4C2C1D302A0100A36F69 /* UIColor+Giraffe.swift */; }; 37 | 30CA4C2F1D302AC000A36F69 /* UIColor+HEX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CA4C2E1D302AC000A36F69 /* UIColor+HEX.swift */; }; 38 | 30CA50D71D45068B0030E566 /* SearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CA50D61D45068B0030E566 /* SearchResultViewController.swift */; }; 39 | 30CA50D91D450C580030E566 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CA50D81D450C580030E566 /* SearchResult.swift */; }; 40 | 30CA50DB1D4510CA0030E566 /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CA50DA1D4510CA0030E566 /* SearchResultViewModel.swift */; }; 41 | 30D6EE861D411F3E00356747 /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30D6EE841D411F3E00356747 /* Nuke.framework */; }; 42 | 30D6EE871D411F3E00356747 /* NukeAnimatedImagePlugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30D6EE851D411F3E00356747 /* NukeAnimatedImagePlugin.framework */; }; 43 | 30F2AE981D4E6D6500E3496F /* ModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F2AE971D4E6D6500E3496F /* ModelType.swift */; }; 44 | 30FC78651D4257DC00D5DE5E /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30FC78641D4257DC00D5DE5E /* Unbox.framework */; }; 45 | 30FC78691D425E4F00D5DE5E /* Image+RAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FC78681D425E4F00D5DE5E /* Image+RAC.swift */; }; 46 | /* End PBXBuildFile section */ 47 | 48 | /* Begin PBXContainerItemProxy section */ 49 | 30061B9A1D29209800ABEF7F /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = 30061B7D1D29209800ABEF7F /* Project object */; 52 | proxyType = 1; 53 | remoteGlobalIDString = 30061B841D29209800ABEF7F; 54 | remoteInfo = "Giraffe-iOS"; 55 | }; 56 | 30061BA51D29209800ABEF7F /* PBXContainerItemProxy */ = { 57 | isa = PBXContainerItemProxy; 58 | containerPortal = 30061B7D1D29209800ABEF7F /* Project object */; 59 | proxyType = 1; 60 | remoteGlobalIDString = 30061B841D29209800ABEF7F; 61 | remoteInfo = "Giraffe-iOS"; 62 | }; 63 | /* End PBXContainerItemProxy section */ 64 | 65 | /* Begin PBXCopyFilesBuildPhase section */ 66 | 3089E8CB1D2DA88700726903 /* Embed Frameworks */ = { 67 | isa = PBXCopyFilesBuildPhase; 68 | buildActionMask = 2147483647; 69 | dstPath = ""; 70 | dstSubfolderSpec = 10; 71 | files = ( 72 | 3089E8CA1D2DA88700726903 /* GiraffeKit.framework in Embed Frameworks */, 73 | ); 74 | name = "Embed Frameworks"; 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXCopyFilesBuildPhase section */ 78 | 79 | /* Begin PBXFileReference section */ 80 | 30061B851D29209800ABEF7F /* Giraffe-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Giraffe-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 30061B881D29209800ABEF7F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 82 | 30061B8A1D29209800ABEF7F /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; 83 | 30061B8D1D29209800ABEF7F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 84 | 30061B8F1D29209800ABEF7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 85 | 30061B921D29209800ABEF7F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 86 | 30061B941D29209800ABEF7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 87 | 30061B991D29209800ABEF7F /* Giraffe-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Giraffe-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | 30061B9D1D29209800ABEF7F /* Giraffe_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Giraffe_iOSTests.swift; sourceTree = ""; }; 89 | 30061B9F1D29209800ABEF7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 90 | 30061BA41D29209800ABEF7F /* Giraffe-iOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Giraffe-iOSUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | 30061BA81D29209800ABEF7F /* Giraffe_iOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Giraffe_iOSUITests.swift; sourceTree = ""; }; 92 | 30061BAA1D29209800ABEF7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 93 | 30061C011D29D3A600ABEF7F /* FLAnimatedImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FLAnimatedImage.framework; path = ../Carthage/Build/iOS/FLAnimatedImage.framework; sourceTree = ""; }; 94 | 301593071D326D21009813E5 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveCocoa.framework; path = ../Carthage/Build/iOS/ReactiveCocoa.framework; sourceTree = ""; }; 95 | 301593081D326D21009813E5 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = ../Carthage/Build/iOS/Result.framework; sourceTree = ""; }; 96 | 30305A3B1D3041B00059919F /* UIFont+Giraffe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Giraffe.swift"; sourceTree = ""; }; 97 | 30305A401D3052F60059919F /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; 98 | 30305A421D3053940059919F /* AnimatedImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageCell.swift; sourceTree = ""; }; 99 | 306B61E31D4576100001EDB2 /* BaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; 100 | 3089E8C81D2DA88700726903 /* GiraffeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GiraffeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 101 | 30A0C2261D3C32B300627ECB /* AnimatedImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageViewModel.swift; sourceTree = ""; }; 102 | 30A3B90A1D2F893B00ADEF41 /* AnimatedImageCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageCollectionViewController.swift; sourceTree = ""; }; 103 | 30A3B90C1D2F8A3A00ADEF41 /* CommonUserInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommonUserInterface.swift; sourceTree = ""; }; 104 | 30B3B7641D3A9D960077BCAA /* Trending.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Trending.swift; sourceTree = ""; }; 105 | 30B3B7661D3A9F140077BCAA /* TrendingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingViewModel.swift; sourceTree = ""; }; 106 | 30B3B76A1D3A9F600077BCAA /* RACHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RACHelpers.swift; sourceTree = ""; }; 107 | 30B3B76E1D3AA16C0077BCAA /* ViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewType.swift; sourceTree = ""; }; 108 | 30B3B7701D3AA17E0077BCAA /* ViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; 109 | 30B3B7721D3AA1C90077BCAA /* UIViewController+ViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+ViewType.swift"; sourceTree = ""; }; 110 | 30B3B7741D3AC0F70077BCAA /* UIKitExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitExtensions.swift; sourceTree = ""; }; 111 | 30CA4C2C1D302A0100A36F69 /* UIColor+Giraffe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Giraffe.swift"; sourceTree = ""; }; 112 | 30CA4C2E1D302AC000A36F69 /* UIColor+HEX.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+HEX.swift"; sourceTree = ""; }; 113 | 30CA50D61D45068B0030E566 /* SearchResultViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultViewController.swift; sourceTree = ""; }; 114 | 30CA50D81D450C580030E566 /* SearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = ""; }; 115 | 30CA50DA1D4510CA0030E566 /* SearchResultViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultViewModel.swift; sourceTree = ""; }; 116 | 30D6EE841D411F3E00356747 /* Nuke.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nuke.framework; path = ../Carthage/Build/iOS/Nuke.framework; sourceTree = ""; }; 117 | 30D6EE851D411F3E00356747 /* NukeAnimatedImagePlugin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NukeAnimatedImagePlugin.framework; path = ../Carthage/Build/iOS/NukeAnimatedImagePlugin.framework; sourceTree = ""; }; 118 | 30F2AE971D4E6D6500E3496F /* ModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelType.swift; sourceTree = ""; }; 119 | 30FC78641D4257DC00D5DE5E /* Unbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Unbox.framework; path = ../Carthage/Build/iOS/Unbox.framework; sourceTree = ""; }; 120 | 30FC78661D425AD300D5DE5E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 121 | 30FC78681D425E4F00D5DE5E /* Image+RAC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+RAC.swift"; sourceTree = ""; }; 122 | /* End PBXFileReference section */ 123 | 124 | /* Begin PBXFrameworksBuildPhase section */ 125 | 30061B821D29209800ABEF7F /* Frameworks */ = { 126 | isa = PBXFrameworksBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | 30FC78651D4257DC00D5DE5E /* Unbox.framework in Frameworks */, 130 | 3089E8C91D2DA88700726903 /* GiraffeKit.framework in Frameworks */, 131 | 30061C021D29D3A600ABEF7F /* FLAnimatedImage.framework in Frameworks */, 132 | 30D6EE861D411F3E00356747 /* Nuke.framework in Frameworks */, 133 | 30D6EE871D411F3E00356747 /* NukeAnimatedImagePlugin.framework in Frameworks */, 134 | 301593091D326D21009813E5 /* ReactiveCocoa.framework in Frameworks */, 135 | 3015930A1D326D21009813E5 /* Result.framework in Frameworks */, 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | 30061B961D29209800ABEF7F /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | 30061BA11D29209800ABEF7F /* Frameworks */ = { 147 | isa = PBXFrameworksBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXFrameworksBuildPhase section */ 154 | 155 | /* Begin PBXGroup section */ 156 | 30061B7C1D29209800ABEF7F = { 157 | isa = PBXGroup; 158 | children = ( 159 | 30061B871D29209800ABEF7F /* Giraffe-iOS */, 160 | 30061B9C1D29209800ABEF7F /* Giraffe-iOSTests */, 161 | 30061BA71D29209800ABEF7F /* Giraffe-iOSUITests */, 162 | 30061BF21D29C71C00ABEF7F /* Frameworks */, 163 | 30061B861D29209800ABEF7F /* Products */, 164 | ); 165 | sourceTree = ""; 166 | }; 167 | 30061B861D29209800ABEF7F /* Products */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 30061B851D29209800ABEF7F /* Giraffe-iOS.app */, 171 | 30061B991D29209800ABEF7F /* Giraffe-iOSTests.xctest */, 172 | 30061BA41D29209800ABEF7F /* Giraffe-iOSUITests.xctest */, 173 | ); 174 | name = Products; 175 | sourceTree = ""; 176 | }; 177 | 30061B871D29209800ABEF7F /* Giraffe-iOS */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 30061B881D29209800ABEF7F /* AppDelegate.swift */, 181 | 30B3B76D1D3AA1350077BCAA /* MVVM */, 182 | 30D7D4A31D3A97C90063F684 /* Model */, 183 | 30D7D4A41D3A97D00063F684 /* ViewModel */, 184 | 30D7D4A21D3A97BF0063F684 /* View */, 185 | 30B3B7691D3A9F460077BCAA /* Utils */, 186 | 30CA4C2B1D30298600A36F69 /* Framework Extensions */, 187 | 3089E8C71D2DA69800726903 /* Supporting Files */, 188 | ); 189 | path = "Giraffe-iOS"; 190 | sourceTree = ""; 191 | }; 192 | 30061B9C1D29209800ABEF7F /* Giraffe-iOSTests */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 30061B9D1D29209800ABEF7F /* Giraffe_iOSTests.swift */, 196 | 30061B9F1D29209800ABEF7F /* Info.plist */, 197 | ); 198 | path = "Giraffe-iOSTests"; 199 | sourceTree = ""; 200 | }; 201 | 30061BA71D29209800ABEF7F /* Giraffe-iOSUITests */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 30061BA81D29209800ABEF7F /* Giraffe_iOSUITests.swift */, 205 | 30061BAA1D29209800ABEF7F /* Info.plist */, 206 | ); 207 | path = "Giraffe-iOSUITests"; 208 | sourceTree = ""; 209 | }; 210 | 30061BF21D29C71C00ABEF7F /* Frameworks */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 30FC78641D4257DC00D5DE5E /* Unbox.framework */, 214 | 30D6EE841D411F3E00356747 /* Nuke.framework */, 215 | 30D6EE851D411F3E00356747 /* NukeAnimatedImagePlugin.framework */, 216 | 3089E8C81D2DA88700726903 /* GiraffeKit.framework */, 217 | 30061C011D29D3A600ABEF7F /* FLAnimatedImage.framework */, 218 | 301593071D326D21009813E5 /* ReactiveCocoa.framework */, 219 | 301593081D326D21009813E5 /* Result.framework */, 220 | ); 221 | name = Frameworks; 222 | sourceTree = ""; 223 | }; 224 | 3089E8C71D2DA69800726903 /* Supporting Files */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 30061B8C1D29209800ABEF7F /* Main.storyboard */, 228 | 30061B8F1D29209800ABEF7F /* Assets.xcassets */, 229 | 30061B911D29209800ABEF7F /* LaunchScreen.storyboard */, 230 | 30061B941D29209800ABEF7F /* Info.plist */, 231 | 30305A411D3052F60059919F /* InfoPlist.strings */, 232 | ); 233 | name = "Supporting Files"; 234 | sourceTree = ""; 235 | }; 236 | 30B3B7691D3A9F460077BCAA /* Utils */ = { 237 | isa = PBXGroup; 238 | children = ( 239 | 30A3B90C1D2F8A3A00ADEF41 /* CommonUserInterface.swift */, 240 | 30B3B76A1D3A9F600077BCAA /* RACHelpers.swift */, 241 | 30B3B7741D3AC0F70077BCAA /* UIKitExtensions.swift */, 242 | 30FC78681D425E4F00D5DE5E /* Image+RAC.swift */, 243 | ); 244 | name = Utils; 245 | sourceTree = ""; 246 | }; 247 | 30B3B76D1D3AA1350077BCAA /* MVVM */ = { 248 | isa = PBXGroup; 249 | children = ( 250 | 30F2AE971D4E6D6500E3496F /* ModelType.swift */, 251 | 30B3B76E1D3AA16C0077BCAA /* ViewType.swift */, 252 | 30B3B7701D3AA17E0077BCAA /* ViewModelType.swift */, 253 | 30B3B7721D3AA1C90077BCAA /* UIViewController+ViewType.swift */, 254 | ); 255 | name = MVVM; 256 | sourceTree = ""; 257 | }; 258 | 30CA4C2B1D30298600A36F69 /* Framework Extensions */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | 30CA4C2E1D302AC000A36F69 /* UIColor+HEX.swift */, 262 | 30CA4C2C1D302A0100A36F69 /* UIColor+Giraffe.swift */, 263 | 30305A3B1D3041B00059919F /* UIFont+Giraffe.swift */, 264 | ); 265 | name = "Framework Extensions"; 266 | sourceTree = ""; 267 | }; 268 | 30D7D4A21D3A97BF0063F684 /* View */ = { 269 | isa = PBXGroup; 270 | children = ( 271 | 306B61E31D4576100001EDB2 /* BaseViewController.swift */, 272 | 30061B8A1D29209800ABEF7F /* TrendingViewController.swift */, 273 | 30305A421D3053940059919F /* AnimatedImageCell.swift */, 274 | 30A3B90A1D2F893B00ADEF41 /* AnimatedImageCollectionViewController.swift */, 275 | 30CA50D61D45068B0030E566 /* SearchResultViewController.swift */, 276 | ); 277 | name = View; 278 | sourceTree = ""; 279 | }; 280 | 30D7D4A31D3A97C90063F684 /* Model */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | 30B3B7641D3A9D960077BCAA /* Trending.swift */, 284 | 30CA50D81D450C580030E566 /* SearchResult.swift */, 285 | ); 286 | name = Model; 287 | sourceTree = ""; 288 | }; 289 | 30D7D4A41D3A97D00063F684 /* ViewModel */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | 30B3B7661D3A9F140077BCAA /* TrendingViewModel.swift */, 293 | 30CA50DA1D4510CA0030E566 /* SearchResultViewModel.swift */, 294 | 30A0C2261D3C32B300627ECB /* AnimatedImageViewModel.swift */, 295 | ); 296 | name = ViewModel; 297 | sourceTree = ""; 298 | }; 299 | /* End PBXGroup section */ 300 | 301 | /* Begin PBXNativeTarget section */ 302 | 30061B841D29209800ABEF7F /* Giraffe-iOS */ = { 303 | isa = PBXNativeTarget; 304 | buildConfigurationList = 30061BAD1D29209800ABEF7F /* Build configuration list for PBXNativeTarget "Giraffe-iOS" */; 305 | buildPhases = ( 306 | 30061B811D29209800ABEF7F /* Sources */, 307 | 30061B821D29209800ABEF7F /* Frameworks */, 308 | 30061B831D29209800ABEF7F /* Resources */, 309 | 30061BF51D29C73E00ABEF7F /* Carthage Copy Frameworks */, 310 | 3089E8CB1D2DA88700726903 /* Embed Frameworks */, 311 | ); 312 | buildRules = ( 313 | ); 314 | dependencies = ( 315 | ); 316 | name = "Giraffe-iOS"; 317 | productName = "Giraffe-iOS"; 318 | productReference = 30061B851D29209800ABEF7F /* Giraffe-iOS.app */; 319 | productType = "com.apple.product-type.application"; 320 | }; 321 | 30061B981D29209800ABEF7F /* Giraffe-iOSTests */ = { 322 | isa = PBXNativeTarget; 323 | buildConfigurationList = 30061BB01D29209800ABEF7F /* Build configuration list for PBXNativeTarget "Giraffe-iOSTests" */; 324 | buildPhases = ( 325 | 30061B951D29209800ABEF7F /* Sources */, 326 | 30061B961D29209800ABEF7F /* Frameworks */, 327 | 30061B971D29209800ABEF7F /* Resources */, 328 | ); 329 | buildRules = ( 330 | ); 331 | dependencies = ( 332 | 30061B9B1D29209800ABEF7F /* PBXTargetDependency */, 333 | ); 334 | name = "Giraffe-iOSTests"; 335 | productName = "Giraffe-iOSTests"; 336 | productReference = 30061B991D29209800ABEF7F /* Giraffe-iOSTests.xctest */; 337 | productType = "com.apple.product-type.bundle.unit-test"; 338 | }; 339 | 30061BA31D29209800ABEF7F /* Giraffe-iOSUITests */ = { 340 | isa = PBXNativeTarget; 341 | buildConfigurationList = 30061BB31D29209800ABEF7F /* Build configuration list for PBXNativeTarget "Giraffe-iOSUITests" */; 342 | buildPhases = ( 343 | 30061BA01D29209800ABEF7F /* Sources */, 344 | 30061BA11D29209800ABEF7F /* Frameworks */, 345 | 30061BA21D29209800ABEF7F /* Resources */, 346 | ); 347 | buildRules = ( 348 | ); 349 | dependencies = ( 350 | 30061BA61D29209800ABEF7F /* PBXTargetDependency */, 351 | ); 352 | name = "Giraffe-iOSUITests"; 353 | productName = "Giraffe-iOSUITests"; 354 | productReference = 30061BA41D29209800ABEF7F /* Giraffe-iOSUITests.xctest */; 355 | productType = "com.apple.product-type.bundle.ui-testing"; 356 | }; 357 | /* End PBXNativeTarget section */ 358 | 359 | /* Begin PBXProject section */ 360 | 30061B7D1D29209800ABEF7F /* Project object */ = { 361 | isa = PBXProject; 362 | attributes = { 363 | LastSwiftUpdateCheck = 0800; 364 | LastUpgradeCheck = 0800; 365 | ORGANIZATIONNAME = "Yevhen Dubinin"; 366 | TargetAttributes = { 367 | 30061B841D29209800ABEF7F = { 368 | CreatedOnToolsVersion = 8.0; 369 | ProvisioningStyle = Automatic; 370 | }; 371 | 30061B981D29209800ABEF7F = { 372 | CreatedOnToolsVersion = 8.0; 373 | ProvisioningStyle = Automatic; 374 | TestTargetID = 30061B841D29209800ABEF7F; 375 | }; 376 | 30061BA31D29209800ABEF7F = { 377 | CreatedOnToolsVersion = 8.0; 378 | ProvisioningStyle = Automatic; 379 | TestTargetID = 30061B841D29209800ABEF7F; 380 | }; 381 | }; 382 | }; 383 | buildConfigurationList = 30061B801D29209800ABEF7F /* Build configuration list for PBXProject "Giraffe-iOS" */; 384 | compatibilityVersion = "Xcode 3.2"; 385 | developmentRegion = English; 386 | hasScannedForEncodings = 0; 387 | knownRegions = ( 388 | en, 389 | Base, 390 | ); 391 | mainGroup = 30061B7C1D29209800ABEF7F; 392 | productRefGroup = 30061B861D29209800ABEF7F /* Products */; 393 | projectDirPath = ""; 394 | projectRoot = ""; 395 | targets = ( 396 | 30061B841D29209800ABEF7F /* Giraffe-iOS */, 397 | 30061B981D29209800ABEF7F /* Giraffe-iOSTests */, 398 | 30061BA31D29209800ABEF7F /* Giraffe-iOSUITests */, 399 | ); 400 | }; 401 | /* End PBXProject section */ 402 | 403 | /* Begin PBXResourcesBuildPhase section */ 404 | 30061B831D29209800ABEF7F /* Resources */ = { 405 | isa = PBXResourcesBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | 30305A3F1D3052F60059919F /* InfoPlist.strings in Resources */, 409 | 30061B931D29209800ABEF7F /* LaunchScreen.storyboard in Resources */, 410 | 30061B901D29209800ABEF7F /* Assets.xcassets in Resources */, 411 | 30061B8E1D29209800ABEF7F /* Main.storyboard in Resources */, 412 | ); 413 | runOnlyForDeploymentPostprocessing = 0; 414 | }; 415 | 30061B971D29209800ABEF7F /* Resources */ = { 416 | isa = PBXResourcesBuildPhase; 417 | buildActionMask = 2147483647; 418 | files = ( 419 | ); 420 | runOnlyForDeploymentPostprocessing = 0; 421 | }; 422 | 30061BA21D29209800ABEF7F /* Resources */ = { 423 | isa = PBXResourcesBuildPhase; 424 | buildActionMask = 2147483647; 425 | files = ( 426 | ); 427 | runOnlyForDeploymentPostprocessing = 0; 428 | }; 429 | /* End PBXResourcesBuildPhase section */ 430 | 431 | /* Begin PBXShellScriptBuildPhase section */ 432 | 30061BF51D29C73E00ABEF7F /* Carthage Copy Frameworks */ = { 433 | isa = PBXShellScriptBuildPhase; 434 | buildActionMask = 2147483647; 435 | files = ( 436 | ); 437 | inputPaths = ( 438 | "$(SRCROOT)/../Carthage/Build/iOS/FLAnimatedImage.framework", 439 | "$(SRCROOT)/../Carthage/Build/iOS/ReactiveCocoa.framework", 440 | "$(SRCROOT)/../Carthage/Build/iOS/Result.framework", 441 | "$(SRCROOT)/../Carthage/Build/iOS/NukeAnimatedImagePlugin.framework", 442 | "$(SRCROOT)/../Carthage/Build/iOS/Nuke.framework", 443 | "$(SRCROOT)/../Carthage/Build/iOS/Unbox.framework", 444 | ); 445 | name = "Carthage Copy Frameworks"; 446 | outputPaths = ( 447 | ); 448 | runOnlyForDeploymentPostprocessing = 0; 449 | shellPath = /bin/sh; 450 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 451 | }; 452 | /* End PBXShellScriptBuildPhase section */ 453 | 454 | /* Begin PBXSourcesBuildPhase section */ 455 | 30061B811D29209800ABEF7F /* Sources */ = { 456 | isa = PBXSourcesBuildPhase; 457 | buildActionMask = 2147483647; 458 | files = ( 459 | 30305A431D3053940059919F /* AnimatedImageCell.swift in Sources */, 460 | 30061B8B1D29209800ABEF7F /* TrendingViewController.swift in Sources */, 461 | 30B3B7711D3AA17E0077BCAA /* ViewModelType.swift in Sources */, 462 | 30CA50DB1D4510CA0030E566 /* SearchResultViewModel.swift in Sources */, 463 | 30F2AE981D4E6D6500E3496F /* ModelType.swift in Sources */, 464 | 30CA4C2F1D302AC000A36F69 /* UIColor+HEX.swift in Sources */, 465 | 306B61E41D4576100001EDB2 /* BaseViewController.swift in Sources */, 466 | 30B3B7651D3A9D960077BCAA /* Trending.swift in Sources */, 467 | 30FC78691D425E4F00D5DE5E /* Image+RAC.swift in Sources */, 468 | 30B3B76F1D3AA16C0077BCAA /* ViewType.swift in Sources */, 469 | 30B3B7731D3AA1C90077BCAA /* UIViewController+ViewType.swift in Sources */, 470 | 30305A3C1D3041B00059919F /* UIFont+Giraffe.swift in Sources */, 471 | 30CA50D91D450C580030E566 /* SearchResult.swift in Sources */, 472 | 30CA4C2D1D302A0100A36F69 /* UIColor+Giraffe.swift in Sources */, 473 | 30CA50D71D45068B0030E566 /* SearchResultViewController.swift in Sources */, 474 | 30A0C2271D3C32B300627ECB /* AnimatedImageViewModel.swift in Sources */, 475 | 30A3B90D1D2F8A3A00ADEF41 /* CommonUserInterface.swift in Sources */, 476 | 30B3B7671D3A9F140077BCAA /* TrendingViewModel.swift in Sources */, 477 | 30061B891D29209800ABEF7F /* AppDelegate.swift in Sources */, 478 | 30A3B90B1D2F893B00ADEF41 /* AnimatedImageCollectionViewController.swift in Sources */, 479 | 30B3B76B1D3A9F600077BCAA /* RACHelpers.swift in Sources */, 480 | 30B3B7751D3AC0F70077BCAA /* UIKitExtensions.swift in Sources */, 481 | ); 482 | runOnlyForDeploymentPostprocessing = 0; 483 | }; 484 | 30061B951D29209800ABEF7F /* Sources */ = { 485 | isa = PBXSourcesBuildPhase; 486 | buildActionMask = 2147483647; 487 | files = ( 488 | 30061B9E1D29209800ABEF7F /* Giraffe_iOSTests.swift in Sources */, 489 | ); 490 | runOnlyForDeploymentPostprocessing = 0; 491 | }; 492 | 30061BA01D29209800ABEF7F /* Sources */ = { 493 | isa = PBXSourcesBuildPhase; 494 | buildActionMask = 2147483647; 495 | files = ( 496 | 30061BA91D29209800ABEF7F /* Giraffe_iOSUITests.swift in Sources */, 497 | ); 498 | runOnlyForDeploymentPostprocessing = 0; 499 | }; 500 | /* End PBXSourcesBuildPhase section */ 501 | 502 | /* Begin PBXTargetDependency section */ 503 | 30061B9B1D29209800ABEF7F /* PBXTargetDependency */ = { 504 | isa = PBXTargetDependency; 505 | target = 30061B841D29209800ABEF7F /* Giraffe-iOS */; 506 | targetProxy = 30061B9A1D29209800ABEF7F /* PBXContainerItemProxy */; 507 | }; 508 | 30061BA61D29209800ABEF7F /* PBXTargetDependency */ = { 509 | isa = PBXTargetDependency; 510 | target = 30061B841D29209800ABEF7F /* Giraffe-iOS */; 511 | targetProxy = 30061BA51D29209800ABEF7F /* PBXContainerItemProxy */; 512 | }; 513 | /* End PBXTargetDependency section */ 514 | 515 | /* Begin PBXVariantGroup section */ 516 | 30061B8C1D29209800ABEF7F /* Main.storyboard */ = { 517 | isa = PBXVariantGroup; 518 | children = ( 519 | 30061B8D1D29209800ABEF7F /* Base */, 520 | ); 521 | name = Main.storyboard; 522 | sourceTree = ""; 523 | }; 524 | 30061B911D29209800ABEF7F /* LaunchScreen.storyboard */ = { 525 | isa = PBXVariantGroup; 526 | children = ( 527 | 30061B921D29209800ABEF7F /* Base */, 528 | ); 529 | name = LaunchScreen.storyboard; 530 | sourceTree = ""; 531 | }; 532 | 30305A411D3052F60059919F /* InfoPlist.strings */ = { 533 | isa = PBXVariantGroup; 534 | children = ( 535 | 30305A401D3052F60059919F /* Base */, 536 | 30FC78661D425AD300D5DE5E /* en */, 537 | ); 538 | name = InfoPlist.strings; 539 | sourceTree = ""; 540 | }; 541 | /* End PBXVariantGroup section */ 542 | 543 | /* Begin XCBuildConfiguration section */ 544 | 30061BAB1D29209800ABEF7F /* Debug */ = { 545 | isa = XCBuildConfiguration; 546 | buildSettings = { 547 | ALWAYS_SEARCH_USER_PATHS = NO; 548 | CLANG_ANALYZER_NONNULL = YES; 549 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 550 | CLANG_CXX_LIBRARY = "libc++"; 551 | CLANG_ENABLE_MODULES = YES; 552 | CLANG_ENABLE_OBJC_ARC = YES; 553 | CLANG_WARN_BOOL_CONVERSION = YES; 554 | CLANG_WARN_CONSTANT_CONVERSION = YES; 555 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 556 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 557 | CLANG_WARN_EMPTY_BODY = YES; 558 | CLANG_WARN_ENUM_CONVERSION = YES; 559 | CLANG_WARN_INT_CONVERSION = YES; 560 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 561 | CLANG_WARN_UNREACHABLE_CODE = YES; 562 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 563 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 564 | COPY_PHASE_STRIP = NO; 565 | DEBUG_INFORMATION_FORMAT = dwarf; 566 | ENABLE_STRICT_OBJC_MSGSEND = YES; 567 | ENABLE_TESTABILITY = YES; 568 | GCC_C_LANGUAGE_STANDARD = gnu99; 569 | GCC_DYNAMIC_NO_PIC = NO; 570 | GCC_NO_COMMON_BLOCKS = YES; 571 | GCC_OPTIMIZATION_LEVEL = 0; 572 | GCC_PREPROCESSOR_DEFINITIONS = ( 573 | "DEBUG=1", 574 | "$(inherited)", 575 | ); 576 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 577 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 578 | GCC_WARN_UNDECLARED_SELECTOR = YES; 579 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 580 | GCC_WARN_UNUSED_FUNCTION = YES; 581 | GCC_WARN_UNUSED_VARIABLE = YES; 582 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 583 | MTL_ENABLE_DEBUG_INFO = YES; 584 | ONLY_ACTIVE_ARCH = YES; 585 | SDKROOT = iphoneos; 586 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 587 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 588 | SWIFT_VERSION = 2.3; 589 | TARGETED_DEVICE_FAMILY = "1,2"; 590 | }; 591 | name = Debug; 592 | }; 593 | 30061BAC1D29209800ABEF7F /* Release */ = { 594 | isa = XCBuildConfiguration; 595 | buildSettings = { 596 | ALWAYS_SEARCH_USER_PATHS = NO; 597 | CLANG_ANALYZER_NONNULL = YES; 598 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 599 | CLANG_CXX_LIBRARY = "libc++"; 600 | CLANG_ENABLE_MODULES = YES; 601 | CLANG_ENABLE_OBJC_ARC = YES; 602 | CLANG_WARN_BOOL_CONVERSION = YES; 603 | CLANG_WARN_CONSTANT_CONVERSION = YES; 604 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 605 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 606 | CLANG_WARN_EMPTY_BODY = YES; 607 | CLANG_WARN_ENUM_CONVERSION = YES; 608 | CLANG_WARN_INT_CONVERSION = YES; 609 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 610 | CLANG_WARN_UNREACHABLE_CODE = YES; 611 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 612 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 613 | COPY_PHASE_STRIP = NO; 614 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 615 | ENABLE_NS_ASSERTIONS = NO; 616 | ENABLE_STRICT_OBJC_MSGSEND = YES; 617 | GCC_C_LANGUAGE_STANDARD = gnu99; 618 | GCC_NO_COMMON_BLOCKS = YES; 619 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 620 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 621 | GCC_WARN_UNDECLARED_SELECTOR = YES; 622 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 623 | GCC_WARN_UNUSED_FUNCTION = YES; 624 | GCC_WARN_UNUSED_VARIABLE = YES; 625 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 626 | MTL_ENABLE_DEBUG_INFO = NO; 627 | SDKROOT = iphoneos; 628 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 629 | SWIFT_VERSION = 2.3; 630 | TARGETED_DEVICE_FAMILY = "1,2"; 631 | VALIDATE_PRODUCT = YES; 632 | }; 633 | name = Release; 634 | }; 635 | 30061BAE1D29209800ABEF7F /* Debug */ = { 636 | isa = XCBuildConfiguration; 637 | buildSettings = { 638 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 639 | FRAMEWORK_SEARCH_PATHS = ( 640 | "$(BUILT_PRODUCTS_DIR)/**", 641 | "$(SRCROOT)/../Carthage/Build/iOS", 642 | ); 643 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../Carthage/Build/iOS/**"; 644 | INFOPLIST_FILE = "Giraffe-iOS/Info.plist"; 645 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 646 | PRODUCT_BUNDLE_IDENTIFIER = "com.devgeniy.Giraffe-iOS"; 647 | PRODUCT_NAME = "$(TARGET_NAME)"; 648 | SWIFT_VERSION = 2.3; 649 | }; 650 | name = Debug; 651 | }; 652 | 30061BAF1D29209800ABEF7F /* Release */ = { 653 | isa = XCBuildConfiguration; 654 | buildSettings = { 655 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 656 | FRAMEWORK_SEARCH_PATHS = ( 657 | "$(BUILT_PRODUCTS_DIR)/**", 658 | "$(SRCROOT)/../Carthage/Build/iOS", 659 | ); 660 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../Carthage/Build/iOS/**"; 661 | INFOPLIST_FILE = "Giraffe-iOS/Info.plist"; 662 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 663 | PRODUCT_BUNDLE_IDENTIFIER = "com.devgeniy.Giraffe-iOS"; 664 | PRODUCT_NAME = "$(TARGET_NAME)"; 665 | SWIFT_VERSION = 2.3; 666 | }; 667 | name = Release; 668 | }; 669 | 30061BB11D29209800ABEF7F /* Debug */ = { 670 | isa = XCBuildConfiguration; 671 | buildSettings = { 672 | BUNDLE_LOADER = "$(TEST_HOST)"; 673 | INFOPLIST_FILE = "Giraffe-iOSTests/Info.plist"; 674 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 675 | PRODUCT_BUNDLE_IDENTIFIER = "com.devgeniy.Giraffe-iOSTests"; 676 | PRODUCT_NAME = "$(TARGET_NAME)"; 677 | SWIFT_VERSION = 2.3; 678 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Giraffe-iOS.app/Giraffe-iOS"; 679 | }; 680 | name = Debug; 681 | }; 682 | 30061BB21D29209800ABEF7F /* Release */ = { 683 | isa = XCBuildConfiguration; 684 | buildSettings = { 685 | BUNDLE_LOADER = "$(TEST_HOST)"; 686 | INFOPLIST_FILE = "Giraffe-iOSTests/Info.plist"; 687 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 688 | PRODUCT_BUNDLE_IDENTIFIER = "com.devgeniy.Giraffe-iOSTests"; 689 | PRODUCT_NAME = "$(TARGET_NAME)"; 690 | SWIFT_VERSION = 2.3; 691 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Giraffe-iOS.app/Giraffe-iOS"; 692 | }; 693 | name = Release; 694 | }; 695 | 30061BB41D29209800ABEF7F /* Debug */ = { 696 | isa = XCBuildConfiguration; 697 | buildSettings = { 698 | INFOPLIST_FILE = "Giraffe-iOSUITests/Info.plist"; 699 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 700 | PRODUCT_BUNDLE_IDENTIFIER = "com.devgeniy.Giraffe-iOSUITests"; 701 | PRODUCT_NAME = "$(TARGET_NAME)"; 702 | SWIFT_VERSION = 2.3; 703 | TEST_TARGET_NAME = "Giraffe-iOS"; 704 | }; 705 | name = Debug; 706 | }; 707 | 30061BB51D29209800ABEF7F /* Release */ = { 708 | isa = XCBuildConfiguration; 709 | buildSettings = { 710 | INFOPLIST_FILE = "Giraffe-iOSUITests/Info.plist"; 711 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 712 | PRODUCT_BUNDLE_IDENTIFIER = "com.devgeniy.Giraffe-iOSUITests"; 713 | PRODUCT_NAME = "$(TARGET_NAME)"; 714 | SWIFT_VERSION = 2.3; 715 | TEST_TARGET_NAME = "Giraffe-iOS"; 716 | }; 717 | name = Release; 718 | }; 719 | /* End XCBuildConfiguration section */ 720 | 721 | /* Begin XCConfigurationList section */ 722 | 30061B801D29209800ABEF7F /* Build configuration list for PBXProject "Giraffe-iOS" */ = { 723 | isa = XCConfigurationList; 724 | buildConfigurations = ( 725 | 30061BAB1D29209800ABEF7F /* Debug */, 726 | 30061BAC1D29209800ABEF7F /* Release */, 727 | ); 728 | defaultConfigurationIsVisible = 0; 729 | defaultConfigurationName = Release; 730 | }; 731 | 30061BAD1D29209800ABEF7F /* Build configuration list for PBXNativeTarget "Giraffe-iOS" */ = { 732 | isa = XCConfigurationList; 733 | buildConfigurations = ( 734 | 30061BAE1D29209800ABEF7F /* Debug */, 735 | 30061BAF1D29209800ABEF7F /* Release */, 736 | ); 737 | defaultConfigurationIsVisible = 0; 738 | defaultConfigurationName = Release; 739 | }; 740 | 30061BB01D29209800ABEF7F /* Build configuration list for PBXNativeTarget "Giraffe-iOSTests" */ = { 741 | isa = XCConfigurationList; 742 | buildConfigurations = ( 743 | 30061BB11D29209800ABEF7F /* Debug */, 744 | 30061BB21D29209800ABEF7F /* Release */, 745 | ); 746 | defaultConfigurationIsVisible = 0; 747 | defaultConfigurationName = Release; 748 | }; 749 | 30061BB31D29209800ABEF7F /* Build configuration list for PBXNativeTarget "Giraffe-iOSUITests" */ = { 750 | isa = XCConfigurationList; 751 | buildConfigurations = ( 752 | 30061BB41D29209800ABEF7F /* Debug */, 753 | 30061BB51D29209800ABEF7F /* Release */, 754 | ); 755 | defaultConfigurationIsVisible = 0; 756 | defaultConfigurationName = Release; 757 | }; 758 | /* End XCConfigurationList section */ 759 | }; 760 | rootObject = 30061B7D1D29209800ABEF7F /* Project object */; 761 | } 762 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/AnimatedImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedImageCell.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/9/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FLAnimatedImage 11 | import ReactiveCocoa 12 | import Result 13 | 14 | class AnimatedImageCell: UICollectionViewCell { 15 | 16 | // MARK: Outlets - 17 | 18 | @IBOutlet weak var animatedImageView: FLAnimatedImageView! 19 | @IBOutlet weak var trendingIndicator: UIImageView! 20 | // MARK: prepareForReuse Signal - 21 | 22 | let racsignal_prepareForReuse: Signal 23 | let racobserver_prepareForReuse: Observer 24 | 25 | // MARK: Initialization - 26 | 27 | override init(frame: CGRect) { 28 | fatalError("init(frame:) is not implemented") 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | (racsignal_prepareForReuse, racobserver_prepareForReuse) = Signal.pipe() 33 | super.init(coder: aDecoder) 34 | contentView.backgroundColor = .giraffeLightGray() 35 | } 36 | 37 | // MARK: ViewType Protocol - 38 | 39 | private(set) var viewModel: AnimatedImageViewModel? 40 | 41 | // MARK: UICollectionReusableView overrides - 42 | 43 | override func prepareForReuse() { 44 | super.prepareForReuse() 45 | self.racobserver_prepareForReuse.sendNext() 46 | } 47 | 48 | // MARK: Configuration - 49 | 50 | func bind(viewModel vm: AnimatedImageViewModel) { 51 | self.viewModel = vm 52 | 53 | self.animatedImageView.rac_animatedImage <~ self.viewModel!.image.producer.observeOn(UIScheduler()).takeUntil(self.racsignal_prepareForReuse) 54 | self.trendingIndicator.rac_hidden <~ self.viewModel!.shouldHideTrendingIndicator.producer.observeOn(UIScheduler()).takeUntil(self.racsignal_prepareForReuse) 55 | 56 | // ACTIVATE upon binding completes 57 | // DEACTIVATE loading upon prepare for reuse 58 | let (activeSignal, activeObserver) = Signal.pipe() 59 | let inactiveSignal = self.racsignal_prepareForReuse.map { _ in false } 60 | self.viewModel!.isActive <~ Signal.merge([inactiveSignal,activeSignal]) 61 | 62 | activeObserver.sendNext(true) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/AnimatedImageCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedImageCollectionViewController.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/8/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ReactiveCocoa 11 | import Result 12 | import GiraffeKit 13 | 14 | final class AnimatedImageCollectionViewController: UICollectionViewController { 15 | private let animatedImageCellIdentifier = "AnimatedImageCellIdentifier" 16 | private let racobserver_didScrollToBottom: Observer 17 | private let distanceToReportDidScrollToBottom: CGFloat = 10.0 18 | 19 | let rac_itemViewModels = MutableProperty<[AnimatedImageViewModel]>([]) 20 | let racsignal_didScrollToBottom: Signal 21 | 22 | // MARK: Initializer - 23 | 24 | override init(collectionViewLayout layout: UICollectionViewLayout) { 25 | (racsignal_didScrollToBottom, racobserver_didScrollToBottom) = Signal.pipe() 26 | super.init(collectionViewLayout: layout) 27 | } 28 | 29 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { 30 | (racsignal_didScrollToBottom, racobserver_didScrollToBottom) = Signal.pipe() 31 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | (racsignal_didScrollToBottom, racobserver_didScrollToBottom) = Signal.pipe() 36 | super.init(coder: aDecoder) 37 | } 38 | 39 | // MARK: View Life Cycle - 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | self.setupBindings() 44 | } 45 | 46 | // MARK: Collection View Data Source - 47 | 48 | override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 49 | return self.rac_itemViewModels.value.count 50 | } 51 | 52 | override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 53 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(animatedImageCellIdentifier, forIndexPath: indexPath) 54 | let animatedImageCell = cell as! AnimatedImageCell // check agains conforming to a protocol, not a concrete type 55 | animatedImageCell.bind(viewModel: self.rac_itemViewModels.value[indexPath.row]) 56 | return animatedImageCell 57 | } 58 | 59 | // MARK: UICollectionViewDelegateFlowLayout - 60 | 61 | func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{ 62 | let minSpaceForCells: CGFloat = 1.0 63 | let width = collectionView.bounds.width - minSpaceForCells 64 | return CGSizeMake(width/2.0, width/2.0) 65 | } 66 | 67 | // MARK: RAC binding - 68 | 69 | private func setupBindings() { 70 | // ???: do we need some timeout, so reloadData is not called in the same run loop? 71 | self.rac_itemViewModels.producer 72 | .observeOn(UIScheduler()) 73 | .on (next: { _ in 74 | self.collectionView!.reloadData() 75 | }) 76 | .start() 77 | } 78 | 79 | // MARK: UIScrollViewDelegate - 80 | 81 | override func scrollViewDidScroll(scrollView: UIScrollView) { 82 | let offset = scrollView.contentOffset 83 | let bounds = scrollView.bounds 84 | let size = scrollView.contentSize 85 | let inset = scrollView.contentInset 86 | let y: CGFloat = offset.y + bounds.size.height - inset.bottom 87 | let h: CGFloat = size.height 88 | 89 | if (offset.y > 0.0 && y > h + distanceToReportDidScrollToBottom) { 90 | racobserver_didScrollToBottom.sendNext() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/AnimatedImageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedImageViewModel.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/18/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import GiraffeKit 12 | import Nuke 13 | 14 | struct AnimatedImageViewModel { 15 | private let model: Item 16 | private let shouldDenoteTrending = MutableProperty(false) 17 | 18 | let isActive = MutableProperty(false) 19 | let image = MutableProperty(nil) 20 | let shouldHideTrendingIndicator = MutableProperty(false) 21 | 22 | init(model: Item, denoteTrending shouldDenoteTrending: Bool = false) { 23 | self.shouldDenoteTrending.value = shouldDenoteTrending 24 | self.model = model 25 | self.setupBindings() 26 | } 27 | 28 | private func setupBindings() { 29 | self.isActive.producer 30 | .filter { $0 } // TODO: cancel work upon 'false' 31 | .mapError { _ in 32 | return GiraffeError.UnknownError 33 | } 34 | .flatMap(.Latest) { _ in 35 | return self.model.multiFrame().get() 36 | } 37 | .startWithResult { result in 38 | print(result) 39 | self.image.value = result.value 40 | 41 | } 42 | 43 | let isEverTrended = MutableProperty(self.model.trendingDate).producer.map { $0 != nil } 44 | self.shouldHideTrendingIndicator <~ combineLatest(isEverTrended, shouldDenoteTrending.producer).producer 45 | .map { (first, second) in 46 | return !(first && second) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/3/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | application.applyGlobalAppearance() 19 | 20 | // Override point for customization after application launch. 21 | return true 22 | } 23 | 24 | func applicationWillResignActive(application: UIApplication) { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 27 | } 28 | 29 | func applicationDidEnterBackground(application: UIApplication) { 30 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 31 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 32 | } 33 | 34 | func applicationWillEnterForeground(application: UIApplication) { 35 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 36 | } 37 | 38 | func applicationDidBecomeActive(application: UIApplication) { 39 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 40 | } 41 | 42 | func applicationWillTerminate(application: UIApplication) { 43 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 44 | } 45 | 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/120x120_Giraffe_Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/120x120_Giraffe_Icon-60@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/120x120_Giraffe_Icon-Small-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/120x120_Giraffe_Icon-Small-40@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/152x152_Giraffe_Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/152x152_Giraffe_Icon-76@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/167x167_Giraffe@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/167x167_Giraffe@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/180x180_Giraffe_Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/180x180_Giraffe_Icon-60@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/29x29_Giraffe_Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/29x29_Giraffe_Icon-Small.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/40x40_Giraffe_Icon-Small-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/40x40_Giraffe_Icon-Small-40.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/58x58_Giraffe_Icon-Small@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/58x58_Giraffe_Icon-Small@2x-1.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/58x58_Giraffe_Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/58x58_Giraffe_Icon-Small@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/76x76_Giraffe_Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/76x76_Giraffe_Icon-76.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/80x80_Giraffe_Icon-Small-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/80x80_Giraffe_Icon-Small-40@2x-1.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/80x80_Giraffe_Icon-Small-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/80x80_Giraffe_Icon-Small-40@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/87x87_Giraffe_Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/87x87_Giraffe_Icon-Small@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "58x58_Giraffe_Icon-Small@2x-1.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "87x87_Giraffe_Icon-Small@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "40x40", 17 | "idiom" : "iphone", 18 | "filename" : "80x80_Giraffe_Icon-Small-40@2x-1.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "120x120_Giraffe_Icon-Small-40@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "60x60", 29 | "idiom" : "iphone", 30 | "filename" : "120x120_Giraffe_Icon-60@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "180x180_Giraffe_Icon-60@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "29x29", 41 | "idiom" : "ipad", 42 | "filename" : "29x29_Giraffe_Icon-Small.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "29x29", 47 | "idiom" : "ipad", 48 | "filename" : "58x58_Giraffe_Icon-Small@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "40x40", 53 | "idiom" : "ipad", 54 | "filename" : "40x40_Giraffe_Icon-Small-40.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "40x40", 59 | "idiom" : "ipad", 60 | "filename" : "80x80_Giraffe_Icon-Small-40@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "76x76", 65 | "idiom" : "ipad", 66 | "filename" : "76x76_Giraffe_Icon-76.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "76x76", 71 | "idiom" : "ipad", 72 | "filename" : "152x152_Giraffe_Icon-76@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "83.5x83.5", 77 | "idiom" : "ipad", 78 | "filename" : "167x167_Giraffe@2x.png", 79 | "scale" : "2x" 80 | } 81 | ], 82 | "info" : { 83 | "version" : 1, 84 | "author" : "xcode" 85 | } 86 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsDisappointed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "notfound@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "notfound@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsDisappointed.imageset/notfound@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsDisappointed.imageset/notfound@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsDisappointed.imageset/notfound@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsDisappointed.imageset/notfound@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsThinking.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "loading@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "loading@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsThinking.imageset/loading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsThinking.imageset/loading@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsThinking.imageset/loading@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeIsThinking.imageset/loading@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Giraffe_Logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Giraffe_Logo@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Giraffe_Logo@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Giraffe_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Giraffe_Logo.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Giraffe_Logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Giraffe_Logo@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Giraffe_Logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeLogo.imageset/Giraffe_Logo@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "GiraffeNeck.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "GiraffeNeck@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "GiraffeNeck@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/GiraffeNeck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/GiraffeNeck.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/GiraffeNeck@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/GiraffeNeck@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/GiraffeNeck@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/GiraffeNeckLong.imageset/GiraffeNeck@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-highlighted.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "cancelsearch-highlighted@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "cancelsearch-highlighted@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-highlighted.imageset/cancelsearch-highlighted@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-highlighted.imageset/cancelsearch-highlighted@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-highlighted.imageset/cancelsearch-highlighted@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-highlighted.imageset/cancelsearch-highlighted@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-normal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "cancelsearch-normal@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "cancelsearch-normal@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-normal.imageset/cancelsearch-normal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-normal.imageset/cancelsearch-normal@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-normal.imageset/cancelsearch-normal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/cancelsearch-normal.imageset/cancelsearch-normal@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/family.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "family@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "family@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/family.imageset/family@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/family.imageset/family@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/family.imageset/family@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/family.imageset/family@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/familyhl.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "familyhl@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "familyhl@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/familyhl.imageset/familyhl@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/familyhl.imageset/familyhl@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/familyhl.imageset/familyhl@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/familyhl.imageset/familyhl@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/mainsearch.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "mainsearch@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "mainsearch@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/mainsearch.imageset/mainsearch@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/mainsearch.imageset/mainsearch@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/mainsearch.imageset/mainsearch@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/mainsearch.imageset/mainsearch@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/playpause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "playpause@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "playpause@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/playpause.imageset/playpause@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/playpause.imageset/playpause@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/playpause.imageset/playpause@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/playpause.imageset/playpause@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/searchbar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "searchbar@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "searchbar@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/searchbar.imageset/searchbar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/searchbar.imageset/searchbar@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/searchbar.imageset/searchbar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/searchbar.imageset/searchbar@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/trending.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "trending@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "trending@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/trending.imageset/trending@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/trending.imageset/trending@2x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Assets.xcassets/trending.imageset/trending@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evgeniyd/Giraffe/d5b53b7f2e00deacac17a6ee6e83c33812745b24/Giraffe-iOS/Giraffe-iOS/Assets.xcassets/trending.imageset/trending@3x.png -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Base.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Giraffe-iOS 4 | 5 | Created by Evgen Dubinin on 7/9/16. 6 | Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | */ 8 | 9 | "CFBundleDisplayName" = "GIraFfe"; 10 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/25/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseViewController: UIViewController, ViewControllerAppearanceCustomizable { 12 | private let embededCollectionViewSegueIdentifier = "embedCollectionVC" 13 | private(set) var collectionViewController: AnimatedImageCollectionViewController! 14 | 15 | // MARK: View Life Cycle - 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | setBlankBackButtonTitle() 20 | } 21 | 22 | // MARK: Storyboard - 23 | 24 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 25 | if (segue.identifier == embededCollectionViewSegueIdentifier) { 26 | self.collectionViewController = segue.destinationViewController as! AnimatedImageCollectionViewController 27 | } 28 | } 29 | 30 | // MARK: Status Bar - 31 | 32 | override func preferredStatusBarStyle() -> UIStatusBarStyle { return .LightContent } 33 | override func prefersStatusBarHidden() -> Bool { return false } 34 | 35 | // MARK: Memmory Management - 36 | 37 | override func didReceiveMemoryWarning() { 38 | super.didReceiveMemoryWarning() 39 | // Dispose of any resources that can be recreated. 40 | } 41 | 42 | // MARK: Styling - 43 | 44 | // hides "Back" button title (leaving only a shevron) for all VCs pushed by this VC 45 | private func setBlankBackButtonTitle() { 46 | if (isViewLoaded()) { 47 | let backItem = UIBarButtonItem(title: " ", style: .Plain, target: nil, action: nil) 48 | navigationItem.backBarButtonItem = backItem 49 | } 50 | } 51 | 52 | // MARK: Layout - 53 | 54 | override func viewDidLayoutSubviews() { 55 | super.viewDidLayoutSubviews() 56 | 57 | applyCustomAppearance(DefaultViewControllerAppearance()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/CommonUserInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonUserInterface.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/8/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | // MARK: Common View Controller Appearance - 13 | 14 | // Note: This is a direct mapping between plain parameters one might 15 | // want to customize and their direct application to the "real" properties 16 | 17 | protocol ViewControllerAppearance { 18 | var navigationBarBackgroundColor: UIColor { get } 19 | var navigationBarTitleColor: UIColor { get } 20 | var navigationBarButtonsColor: UIColor { get } 21 | var navigationBarTitleFont: UIFont { get } 22 | } 23 | 24 | struct DefaultViewControllerAppearance: ViewControllerAppearance { 25 | var navigationBarBackgroundColor = UIColor.giraffeYellow() 26 | var navigationBarTitleColor = UIColor.giraffeWhite() 27 | var navigationBarTitleFont = UIFont.giraffeScreenTitleFont() 28 | var navigationBarButtonsColor = UIColor.giraffeOrange() 29 | } 30 | 31 | protocol ViewControllerAppearanceCustomizable { 32 | // In order to make everything work, this should be called in 33 | // -viewDidLayoutSubviews() 34 | func applyCustomAppearance(appearance: ViewControllerAppearance) 35 | } 36 | 37 | extension ViewControllerAppearanceCustomizable where Self: UIViewController { 38 | func applyCustomAppearance(appearance: ViewControllerAppearance) { 39 | self.navigationController?.navigationBar.tintColor = appearance.navigationBarButtonsColor 40 | self.navigationController?.navigationBar.barTintColor = appearance.navigationBarBackgroundColor 41 | self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : appearance.navigationBarTitleColor, 42 | NSFontAttributeName : appearance.navigationBarTitleFont] 43 | self.navigationController?.navigationBar.translucent = false 44 | } 45 | } 46 | 47 | // MARK: Status Bar for Navigation Ctrl - 48 | 49 | extension UINavigationController { 50 | public override func childViewControllerForStatusBarHidden() -> UIViewController? { 51 | return self.topViewController 52 | } 53 | 54 | public override func childViewControllerForStatusBarStyle() -> UIViewController? { 55 | return self.topViewController 56 | } 57 | } 58 | 59 | // MARK: Search Bar - 60 | 61 | extension UISearchBar { 62 | class func giraffeSearchBar() -> UISearchBar { 63 | let searchBar = UISearchBar() 64 | searchBar.showsCancelButton = true; 65 | searchBar.setImage(UIImage(named: "cancelsearch-normal"), forSearchBarIcon: UISearchBarIcon.Clear, state: UIControlState.Normal) 66 | searchBar.setImage(UIImage(named: "cancelsearch-highlighted"), forSearchBarIcon: UISearchBarIcon.Clear, state: UIControlState.Highlighted) 67 | searchBar.setImage(UIImage(named: "searchbar"), forSearchBarIcon: UISearchBarIcon.Search, state: UIControlState.Normal) 68 | searchBar.setImage(UIImage(named: "searchbar"), forSearchBarIcon: UISearchBarIcon.Search, state: UIControlState.Highlighted) 69 | searchBar.tintColor = .giraffeOrange() 70 | searchBar.barTintColor = .giraffeOrange() 71 | return searchBar 72 | } 73 | } 74 | 75 | // MARK: Global Appearance - 76 | 77 | // Should be called somewhere at application launch sequence (AppDelegate) 78 | extension UIApplication { 79 | func applyGlobalAppearance() { 80 | UITextField.appearanceWhenContainedInInstancesOfClasses([UISearchBar.self]).tintColor = .giraffeOrange() 81 | UITextField.appearanceWhenContainedInInstancesOfClasses([UISearchBar.self]).backgroundColor = .giraffeWhite() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Image+RAC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+RAC.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/22/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GiraffeKit 11 | import ReactiveCocoa 12 | import Nuke 13 | import NukeAnimatedImagePlugin 14 | 15 | typealias GiraffeImage = GiraffeKit.Image 16 | typealias NukeImage = Nuke.Image 17 | 18 | // MARK: RAC gettable image - 19 | 20 | protocol ImageGettable { 21 | func get() -> SignalProducer 22 | } 23 | 24 | // MARK: Default ImageGettable implementation for GiraffeImage - 25 | 26 | extension GiraffeImage: ImageGettable { 27 | func get() -> SignalProducer { 28 | dispatch_once(&Static.dispatchOnceToken) { 29 | ImageManager.shared = ImageManager.configuredForAnimatedImages() 30 | } 31 | 32 | let imageURL = self.url 33 | let request = ImageRequest(URL: imageURL) 34 | 35 | return SignalProducer { observer, disposable in 36 | let task: ImageTask = Nuke.taskWith(request) { response in 37 | switch response { 38 | case .Success(let image, _): 39 | observer.sendNext(image) 40 | observer.sendCompleted() 41 | case .Failure(let error ): 42 | let nsError: NSError = error as NSError 43 | if nsError.domain == Nuke.ImageManagerErrorDomain 44 | && nsError.code == Nuke.ImageManagerErrorCode.Cancelled.rawValue { 45 | observer.sendInterrupted() 46 | } else { 47 | observer.sendFailed(GiraffeError.LoadImageError(nsError.localizedDescription)) 48 | } 49 | 50 | } 51 | }.resume() 52 | 53 | disposable.addDisposable(task.rac_cancel) 54 | } 55 | } 56 | 57 | private struct Static { 58 | static var dispatchOnceToken: dispatch_once_t = 0 59 | } 60 | } 61 | 62 | // MARK: Nuke Extensions - 63 | 64 | extension ImageManager { 65 | public static func configuredForAnimatedImages() -> ImageManager { 66 | let decoder = ImageDecoderComposition(decoders: [AnimatedImageDecoder(), ImageDecoder()]) 67 | let cache = AnimatedImageMemoryCache() 68 | // TODO: tune app ImageDataLoader() 69 | let animatedImageLoaderConfiguration = ImageLoaderConfiguration(dataLoader: ImageDataLoader(), decoder: decoder) 70 | let loader = ImageLoader(configuration: animatedImageLoaderConfiguration, delegate: AnimatedImageLoaderDelegate()) 71 | return ImageManager(configuration: ImageManagerConfiguration(loader: loader, cache: cache)) 72 | } 73 | } 74 | 75 | /// Nuke's ImageTask extension to match the Disposable's action format 76 | extension ImageTask { 77 | public func rac_cancel() { 78 | _ = self.cancel() 79 | } 80 | } 81 | 82 | // MARK: NukeAnimatedImagePlugin & FLAnimatedImage Extensions - 83 | 84 | import FLAnimatedImage 85 | 86 | extension FLAnimatedImageView { 87 | public var rac_animatedImage: MutableProperty { 88 | return lazyMutableProperty(self, key: &AssociationKey.image, setter: { self.nk_displayImage($0) }, getter: { self.image }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarHidden 34 | 35 | UIStatusBarStyle 36 | UIStatusBarStyleLightContent 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/ModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelType.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/31/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import GiraffeKit 12 | 13 | // MARK: Model Constant - 14 | 15 | struct ServiceConstanst { 16 | static let itemsPerPage = 26 17 | static let retryAttempts = 3 18 | } 19 | 20 | // MARK: Model Page Protocol - 21 | 22 | protocol Pageable { 23 | var page: Page? { get set } 24 | func hasNextPage() -> Bool 25 | func nextPage() -> SignalProducer 26 | } 27 | 28 | extension Pageable { 29 | func hasNextPage() -> Bool { 30 | if let _ = self.page { 31 | return true 32 | } 33 | return self.page!.hasNextPage() 34 | } 35 | } 36 | 37 | // MARK: Parameters Extension - 38 | 39 | extension Pageable { 40 | func getNextPageParameters() -> Parameters { 41 | guard let previousPageInfo = self.page else { 42 | return Parameters(limit: ServiceConstanst.itemsPerPage) 43 | } 44 | 45 | let limit = ServiceConstanst.itemsPerPage 46 | // TODO: throws when previousPageInfo.hasNextPage() == false 47 | let offset = previousPageInfo.indexOfFirstElementOnNextPage() 48 | 49 | return Parameters(limit: limit, offset: offset) 50 | } 51 | } 52 | 53 | // MARK: Invokable Protocol - 54 | 55 | protocol Invokable: class { 56 | func invoke(service s: ServiceRequestable) -> SignalProducer 57 | } 58 | 59 | extension Invokable where Self: Pageable { 60 | func invoke(service s: ServiceRequestable) -> SignalProducer { 61 | let session = NSURLSession.sharedSession() 62 | let request = s.request() 63 | 64 | return session.rac_dataWithRequest(request) 65 | .retry(ServiceConstanst.retryAttempts) 66 | .mapError{ _ in 67 | return GiraffeError.NetworkError 68 | } 69 | .flatMapError { networkError in 70 | print(networkError) 71 | return SignalProducer(error: networkError) 72 | } 73 | .map { [weak self] data, URLResponse in 74 | // TODO: #1 make this reactive, so 75 | switch Response.decodedFrom(data: data, response: URLResponse) { 76 | case .Failure(let decodeError): 77 | print("Parsing error occurred. Error was:\n\(decodeError)") 78 | // TODO: #2 we can do the following: 79 | // return SignalProducer(error: GiraffeError.ParserError) 80 | return nil 81 | case .Success(let result): 82 | self?.page = result.pagination 83 | return result 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/RACHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RACHelpers.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | import Result 11 | 12 | // SignalProducer -> SignalProducer 13 | extension SignalProducer where Value: OptionalType { 14 | public func ignoreError() -> SignalProducer { 15 | return flatMapError { _ in 16 | SignalProducer.empty 17 | } 18 | } 19 | } 20 | 21 | // Merge a collection of SignalProducer's 22 | public func merge(signals: [SignalProducer]) -> SignalProducer { 23 | return SignalProducer, E>(values: signals).flatten(.Merge) 24 | } 25 | 26 | 27 | extension Signal { 28 | // Merget two Signal's 29 | public static func merge>(signals: S) -> Signal { 30 | let producer = SignalProducer, Error>(values: signals) 31 | var result: Signal! 32 | 33 | producer.startWithSignal { (signal, _) in 34 | result = signal.flatten(.Merge) 35 | } 36 | 37 | return result 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/SearchResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResult.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/24/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import GiraffeKit 12 | 13 | // MARK: Model Implementation - 14 | 15 | final class SearchResult: Pageable { 16 | private let query: String 17 | var page: Page? 18 | 19 | init(query q: String) { 20 | self.query = q 21 | } 22 | 23 | // MARK: Pageable - 24 | 25 | func nextPage() -> SignalProducer { 26 | let params = getNextPageParameters() 27 | let service = SearchService(query: self.query, parameters: params) 28 | 29 | return invoke(service: service) 30 | } 31 | } 32 | 33 | extension SearchResult: Invokable { } 34 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/SearchResultViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultViewController.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/24/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ReactiveCocoa 11 | import Result 12 | import GiraffeKit 13 | 14 | final class SearchResultViewController: BaseViewController { 15 | 16 | private var viewModel: SearchResultViewModel? = nil 17 | 18 | @IBOutlet weak var containerView: UIView! 19 | @IBOutlet weak var messageLabel: UILabel! 20 | @IBOutlet weak var filterButton: UIBarButtonItem! 21 | @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! 22 | @IBOutlet weak var statusImageView: UIImageView! 23 | 24 | // MARK: View Life Cycle - 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | // Setup RAC bindings. 30 | setupBindings() 31 | } 32 | 33 | // MARK: RAC - 34 | 35 | func bindWith(viewModel vm: SearchResultViewModel) { 36 | self.viewModel = vm 37 | } 38 | 39 | private func setupBindings() { 40 | // Setup custom bindings. 41 | navigationItem.rac_title <~ viewModel!.headline.producer.observeOn(UIScheduler()) 42 | collectionViewController.rac_itemViewModels <~ viewModel!.itemViewModels.producer.observeOn(UIScheduler()) 43 | messageLabel.rac_text <~ viewModel!.message.producer.observeOn(UIScheduler()) 44 | containerView.rac_hidden <~ viewModel!.shouldHideItemsView.producer.observeOn(UIScheduler()) 45 | loadingIndicator.rac_animated <~ viewModel!.isLoading.producer.map{ $0 }.observeOn(UIScheduler()) 46 | statusImageView.rac_image <~ viewModel!.statusImage.producer.observeOn(UIScheduler()) 47 | 48 | filterButton.rac_selected <~ viewModel!.shouldSelectFamilyFilterButton.producer.observeOn(UIScheduler()) 49 | filterButton.rac_enabled <~ viewModel!.toggleFamilyFilter!.enabled.producer.observeOn(UIScheduler()) 50 | 51 | let viewWillAppear = self.rac_signalForSelector(#selector(UIViewController.viewWillAppear(_:))).toSignalProducer().ignoreError().ignoreNil().map { _ in true } 52 | let viewWillDisappear = self.rac_signalForSelector(#selector(UIViewController.viewWillDisappear(_:))).toSignalProducer().ignoreError().ignoreNil().map { _ in false } 53 | self.viewModel!.isActive <~ merge([viewWillAppear, viewWillDisappear]) 54 | } 55 | 56 | // MARK: Actions - 57 | 58 | @IBAction func onFilterButtonTap(sender: AnyObject) { 59 | _ = viewModel!.toggleFamilyFilter!.apply().start() 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/SearchResultViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultViewModel.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/24/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import Result 12 | import GiraffeKit 13 | 14 | final class SearchResultViewModel: ViewModelType { 15 | 16 | private struct Constants { 17 | static let RetryAttempts: Int = Int.max 18 | } 19 | 20 | private let model : SearchResult 21 | private let response = MutableProperty(nil) 22 | private let items = MutableProperty<[Item]>([]) 23 | private let isFamilyFilterActive = MutableProperty(false) 24 | private let isFamilyFilterEnable = MutableProperty(true) 25 | private let filteredItems = MutableProperty<[Item]>([]) 26 | 27 | // messages 28 | private let fetchErrorMsg = "Something went wrong while getting trending" 29 | private let blankMsg = "" 30 | private let emptyResponseMsg = "No items were found" 31 | // images 32 | private let loadingImage = UIImage(named: "GiraffeIsThinking") 33 | private let notFoundImage = UIImage(named: "GiraffeIsDisappointed") 34 | 35 | // MARK: ViewModelType - 36 | 37 | let isActive = MutableProperty(false) 38 | let message = MutableProperty("") 39 | let shouldHideItemsView = MutableProperty(true) 40 | let itemViewModels = MutableProperty<[AnimatedImageViewModel]>([]) 41 | let shouldDenoteTrending = ConstantProperty(true) 42 | let isLoading = MutableProperty(false) 43 | let statusImage = MutableProperty(nil) 44 | 45 | // MARK: ViewModel Public Properties - 46 | 47 | let headline = MutableProperty(nil) 48 | // TODO: make it const and Not Optional 49 | private(set) var toggleFamilyFilter : Action? 50 | let shouldSelectFamilyFilterButton = MutableProperty(false) 51 | 52 | // MARK: Initialization - 53 | 54 | init(model: SearchResult) { 55 | self.model = model 56 | 57 | shouldSelectFamilyFilterButton <~ isFamilyFilterActive 58 | isFamilyFilterEnable <~ isLoading.producer.map { !$0 } 59 | toggleFamilyFilter = Action(enabledIf: isFamilyFilterEnable, { _ -> SignalProducer in 60 | return SignalProducer { [unowned self] observer, _ in 61 | self.isFamilyFilterActive.value = !self.isFamilyFilterActive.value 62 | observer.sendCompleted() 63 | } 64 | }) 65 | 66 | self.isActive.producer 67 | .filter { $0 } 68 | .mapError { _ in 69 | return GiraffeError.UnknownError 70 | }.on(next: { [unowned self] _ in 71 | self.isLoading.value = true 72 | self.statusImage.value = self.loadingImage 73 | }) 74 | .flatMap(.Latest) { [unowned self] _ in 75 | return self.model.nextPage() 76 | } 77 | .on(failed: { [unowned self] _ in 78 | self.isLoading.value = false 79 | self.message.value = self.fetchErrorMsg 80 | self.statusImage.value = nil 81 | }, 82 | next: { [unowned self] _ in 83 | self.isLoading.value = false 84 | self.statusImage.value = nil 85 | } 86 | ) 87 | // HACK: We'd like to receive an error and update the UI, but it well known 88 | // that SignalProducer stops upon failure, thus we have to retry it. 89 | // For now, retry as much as we can (Int.max times). There has to be more correct way to do this 90 | .retry(Constants.RetryAttempts) 91 | .startWithResult { [unowned self] result in 92 | self.response.value = result.value! 93 | self.message.value = self.response.value!.zeroItems ? self.emptyResponseMsg : self.blankMsg 94 | } 95 | 96 | self.items <~ self.response.producer 97 | .ignoreNil() 98 | .map { response in 99 | response.data 100 | } 101 | 102 | self.filteredItems <~ self.items.producer 103 | .map() { items in 104 | let filtered = items.filter{ $0.isFamilyFriendly() } 105 | return filtered 106 | } 107 | 108 | let updates = combineLatest(self.items.producer, self.filteredItems.producer, self.isFamilyFilterActive.producer) 109 | self.itemViewModels <~ updates.producer.map { items, filteredItems, filter in 110 | if filter { 111 | return filteredItems.map { AnimatedImageViewModel(model: $0, denoteTrending: self.shouldDenoteTrending.value) } 112 | } 113 | else { 114 | return items.map { AnimatedImageViewModel(model: $0, denoteTrending: self.shouldDenoteTrending.value) } 115 | } 116 | } 117 | 118 | let noItemsSignal = self.items.producer.map { $0.count == 0 } 119 | self.shouldHideItemsView <~ noItemsSignal 120 | self.statusImage <~ noItemsSignal.map { $0 ? self.notFoundImage : nil } 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/Trending.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Trending.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import GiraffeKit 12 | 13 | // MARK: Model Implementation - 14 | 15 | final class Trending: Pageable { 16 | var page: Page? 17 | 18 | init() { 19 | // do nothing 20 | } 21 | 22 | // MARK: Pageable - 23 | 24 | func nextPage() -> SignalProducer { 25 | let params = getNextPageParameters() 26 | let service = TrendingService(parameters: params) 27 | return invoke(service: service) 28 | } 29 | } 30 | 31 | extension Trending: Invokable { } 32 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/TrendingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/3/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ReactiveCocoa 11 | import Result 12 | import GiraffeKit 13 | 14 | final class TrendingViewController: BaseViewController, ViewType { 15 | private let searchResultSegueIdentiier = "searchResultVC" 16 | 17 | let viewModel: TrendingViewModel? = TrendingViewModel(model: Trending()) 18 | 19 | let searchBar = UISearchBar.giraffeSearchBar() 20 | var searchBarButtonItem: UIBarButtonItem? = nil 21 | var animationPlaybackControl: UIBarButtonItem? = nil 22 | 23 | @IBOutlet weak var searchButton: UIBarButtonItem! 24 | @IBOutlet weak var containerView: UIView! 25 | @IBOutlet weak var messageLabel: UILabel! 26 | @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! 27 | @IBOutlet weak var statusImageView: UIImageView! 28 | 29 | // MARK: View Life Cycle - 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | // Setup Search button 35 | searchBarButtonItem = navigationItem.rightBarButtonItem 36 | searchBarButtonItem?.target = self 37 | searchBarButtonItem?.action = #selector(didPressSearchButton) 38 | 39 | // Setup Search button 40 | // TODO: uncomment when is ready to implement 41 | // animationPlaybackControl = navigationItem.leftBarButtonItem 42 | // animationPlaybackControl?.target = self 43 | // animationPlaybackControl?.action = #selector(didChangeAnimationPlayback) 44 | // animationPlaybackControl?.enabled = false // disabled for now 45 | 46 | setupBindings() 47 | } 48 | 49 | // MARK: RAC Bindings - 50 | 51 | func setupBindings() { 52 | // Setup view helper bindings. 53 | self.setupViewBindings() 54 | 55 | // Setup custom bindings. 56 | navigationItem.rac_title <~ viewModel!.headline.producer.observeOn(UIScheduler()) 57 | messageLabel.rac_text <~ viewModel!.message.producer.observeOn(UIScheduler()) 58 | containerView.rac_hidden <~ viewModel!.shouldHideItemsView.producer.observeOn(UIScheduler()) 59 | collectionViewController.rac_itemViewModels <~ viewModel!.itemViewModels.producer.observeOn(UIScheduler()) 60 | searchButton.rac_enabled <~ viewModel!.shouldEnableSearchButton.producer.observeOn(UIScheduler()) 61 | loadingIndicator.rac_animated <~ viewModel!.isLoading.producer.map{ $0 }.observeOn(UIScheduler()) 62 | statusImageView.rac_image <~ viewModel!.statusImage.producer.observeOn(UIScheduler()) 63 | 64 | viewModel!.didScrollToBottom <~ collectionViewController.racsignal_didScrollToBottom 65 | 66 | // search bar 67 | viewModel!.searchText <~ self.searchBar.rac_text 68 | 69 | self.searchBar.rac_searchBarSearchButtonClicked { [unowned self] searchBar in 70 | searchBar.resignFirstResponder() 71 | self.hideSearchBar() 72 | self.performSegueWithIdentifier(self.searchResultSegueIdentiier, sender: self) 73 | } 74 | 75 | self.searchBar.rac_searchBarCancelButtonClicked { [unowned self] searchBar in 76 | self.hideSearchBar() 77 | } 78 | } 79 | 80 | @objc func didChangeAnimationPlayback() { 81 | // TODO: start/stop animation 82 | } 83 | 84 | // MARK: Search Bar Presentation - 85 | 86 | @objc func didPressSearchButton() { 87 | showSearchBar() 88 | } 89 | 90 | private func showSearchBar() { 91 | searchBar.alpha = 0 92 | navigationItem.titleView = searchBar 93 | navigationItem.setRightBarButtonItem(nil, animated: false) 94 | navigationItem.setLeftBarButtonItem(nil, animated: false) 95 | UIView.animateWithDuration(0.5, animations: { 96 | self.searchBar.alpha = 1 97 | }, completion: { finished in 98 | self.searchBar.becomeFirstResponder() 99 | }) 100 | } 101 | 102 | private func hideSearchBar() { 103 | navigationItem.setRightBarButtonItem(searchBarButtonItem, animated: true) 104 | navigationItem.setLeftBarButtonItem(animationPlaybackControl, animated: true) 105 | UIView.animateWithDuration(0.3, animations: { 106 | self.navigationItem.title = self.viewModel!.headline.value 107 | self.navigationItem.titleView = nil 108 | }, completion: { finished in 109 | 110 | }) 111 | } 112 | 113 | // MARK: Storyboard - 114 | 115 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 116 | if (segue.identifier == searchResultSegueIdentiier) { 117 | guard let searchResultVC = segue.destinationViewController as? SearchResultViewController else { 118 | return 119 | } 120 | searchResultVC.bindWith(viewModel: viewModel!.searchResultViewModel.value!) 121 | } 122 | else { 123 | super.prepareForSegue(segue, sender: sender) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/TrendingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrendingViewModel.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import GiraffeKit 12 | 13 | // MARK: ViewModel Protocol - 14 | 15 | struct TrendingViewModel: ViewModelType { 16 | 17 | private struct Constants { 18 | static let RetryAttempts: Int = Int.max 19 | } 20 | 21 | private let model: Pageable 22 | private let response = MutableProperty(nil) 23 | private let items = MutableProperty<[Item]>([]) 24 | // messages 25 | private let fetchErrorMsg = "Something went wrong while getting trending" 26 | private let blankMsg = "" 27 | private let emptyResponseMsg = "No items were found" 28 | // images 29 | private let loadingImage = UIImage(named: "GiraffeIsThinking") 30 | private let notFoundImage = UIImage(named: "GiraffeIsDisappointed") 31 | 32 | // MARK: ViewModelType - 33 | 34 | let isActive = MutableProperty(false) 35 | let message = MutableProperty("") 36 | let shouldHideItemsView = MutableProperty(true) 37 | let itemViewModels = MutableProperty<[AnimatedImageViewModel]>([]) 38 | let shouldDenoteTrending = ConstantProperty(false) 39 | let isLoading = MutableProperty(false) 40 | let statusImage = MutableProperty(nil) 41 | 42 | // MARK: ViewModel Public Properties - 43 | 44 | let headline = ConstantProperty("Trending") 45 | let searchText = MutableProperty("") 46 | let searchResultViewModel = MutableProperty(nil) 47 | let shouldEnableSearchButton = MutableProperty(false) 48 | 49 | let didScrollToBottom = MutableProperty() 50 | 51 | // MARK: Initialization - 52 | 53 | init(model: Pageable) { 54 | self.model = model 55 | 56 | // Setup RAC bindings. 57 | self.setupBindings() 58 | } 59 | 60 | func setupBindings() { 61 | 62 | self.isActive.producer 63 | .filter { $0 } 64 | .mapError { _ in 65 | return GiraffeError.UnknownError 66 | }.on(next: { _ in 67 | self.isLoading.value = true 68 | self.statusImage.value = self.loadingImage 69 | }) 70 | .flatMap(.Latest) { _ in 71 | return self.model.nextPage() 72 | } 73 | .on(failed: { _ in 74 | self.isLoading.value = false 75 | self.message.value = self.fetchErrorMsg 76 | self.statusImage.value = nil 77 | }, 78 | next: { _ in 79 | self.isLoading.value = false 80 | self.statusImage.value = nil 81 | } 82 | ) 83 | // HACK: We'd like to receive an error and update the UI, but it well known 84 | // that SignalProducer stops upon failure, thus we have to retry it. 85 | // For now, retry as much as we can (Int.max times). There has to be more correct way to do this 86 | .retry(Constants.RetryAttempts) 87 | .startWithResult { result in 88 | self.response.value = result.value! 89 | self.message.value = self.response.value!.zeroItems ? self.emptyResponseMsg : self.blankMsg 90 | } 91 | 92 | self.items <~ self.response.producer 93 | .ignoreNil() 94 | .map { response in 95 | var result = self.items.value 96 | result.appendContentsOf(response.data) 97 | return result 98 | } 99 | 100 | self.itemViewModels <~ self.items.producer.map { items in 101 | items.map { AnimatedImageViewModel(model: $0, denoteTrending: self.shouldDenoteTrending.value) } 102 | } 103 | 104 | let noItemsSignal = self.items.producer.map { items in 105 | items.count == 0 106 | } 107 | 108 | self.shouldHideItemsView <~ noItemsSignal 109 | self.statusImage <~ noItemsSignal.map { $0 ? self.notFoundImage : nil } 110 | 111 | self.searchText.producer 112 | .startWithResult { result in 113 | if let query: String = result.value { 114 | let searchModel = SearchResult(query: query) 115 | let searchResultViewModel = SearchResultViewModel(model: searchModel) 116 | searchResultViewModel.headline.value = query 117 | self.searchResultViewModel.value = searchResultViewModel 118 | } 119 | } 120 | 121 | self.shouldEnableSearchButton <~ self.isLoading.producer.map { !$0 } 122 | 123 | let loadNextPage = self.didScrollToBottom.producer 124 | loadNextPage 125 | // prevent from calling this at first place, before the first set is loaded 126 | // this is weird, but it works 127 | .filter {_ in self.items.value.count > 0 } 128 | .flatMap(.Latest) { _ in 129 | return self.model.nextPage() 130 | } 131 | // HACK: We'd like to receive an error and update the UI, but it well known 132 | // that SignalProducer stops upon failure, thus we have to retry it. 133 | // For now, retry as much as we can (Int.max times). There has to be more correct way to do this 134 | .retry(Constants.RetryAttempts) 135 | .startWithResult { result in 136 | self.response.value = result.value! 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/UIColor+Giraffe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Giraffe.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/8/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | public class func giraffeYellow() -> UIColor { 13 | return UIColor(hexString: "#F1C40F")! 14 | } 15 | 16 | public class func giraffeOrange() -> UIColor { 17 | return UIColor(hexString: "#E67E22")! 18 | } 19 | 20 | public class func giraffeWhite() -> UIColor { 21 | return UIColor.whiteColor() // just in case, the black would become a new white :) 22 | } 23 | 24 | public class func giraffeLightGray() -> UIColor { 25 | return UIColor(hexString: "#ededed")! 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/UIColor+HEX.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+HEX.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 12/15/14. 6 | // Copyright (c) 2014 FAS. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | /** 13 | Create non-autoreleased color with in the given hex string 14 | Alpha will be set as 1 by default 15 | 16 | - parameter hexString: 17 | - returns: color with the given hex string 18 | */ 19 | convenience init?(hexString: String) { 20 | self.init(hexString: hexString, alpha: 1.0) 21 | } 22 | 23 | /** 24 | Create non-autoreleased color with in the given hex string and alpha 25 | 26 | - parameter hexString: 27 | - parameter alpha: 28 | - returns: color with the given hex string and alpha 29 | */ 30 | convenience init?(hexString: String, alpha: Float) { 31 | var hex = hexString 32 | 33 | // Check for hash and remove the hash 34 | if hex.hasPrefix("#") { 35 | hex = hex.substringFromIndex(hex.startIndex.advancedBy(1)) 36 | } 37 | 38 | if let _ = hex.rangeOfString("(^[0-9A-Fa-f]{6}$)|(^[0-9A-Fa-f]{3}$)", options: .RegularExpressionSearch) { 39 | // the hex passed is correct 40 | 41 | // Deal with 3 character Hex strings 42 | if hex.characters.count == 3 { 43 | let redHex = hex.substringToIndex(hex.startIndex.advancedBy(1)) 44 | 45 | let greenHex = hex.substringWithRange(hex.startIndex.advancedBy(1) ..< hex.startIndex.advancedBy(2)) 46 | let blueHex = hex.substringFromIndex(hex.startIndex.advancedBy(2)) 47 | 48 | hex = redHex + redHex + greenHex + greenHex + blueHex + blueHex 49 | } 50 | 51 | let redHex = hex.substringToIndex(hex.startIndex.advancedBy(2)) 52 | let greenHex = hex.substringWithRange(hex.startIndex.advancedBy(2) ..< hex.startIndex.advancedBy(4)) 53 | let blueHex = hex.substringWithRange(hex.startIndex.advancedBy(4) ..< hex.startIndex.advancedBy(6)) 54 | 55 | var redInt: CUnsignedInt = 0 56 | var greenInt: CUnsignedInt = 0 57 | var blueInt: CUnsignedInt = 0 58 | 59 | NSScanner(string: redHex).scanHexInt(&redInt) 60 | NSScanner(string: greenHex).scanHexInt(&greenInt) 61 | NSScanner(string: blueHex).scanHexInt(&blueInt) 62 | 63 | self.init(red: CGFloat(redInt) / 255.0, green: CGFloat(greenInt) / 255.0, blue: CGFloat(blueInt) / 255.0, alpha: CGFloat(alpha)) 64 | } else { 65 | // Note: 66 | // The swift 1.1 compiler is currently unable to destroy partially initialized classes in all cases, 67 | // so it disallows formation of a situation where it would have to. We consider this a bug to be fixed 68 | // in future releases, not a feature. -- Apple Forum 69 | self.init() 70 | return nil 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/UIFont+Giraffe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+Giraffe.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/8/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | import UIKit 13 | 14 | extension UIFont { 15 | class func giraffeScreenTitleFont() -> UIFont { 16 | return UIFont.giraffeScreenTitleFont(ofSize: 20.0) 17 | } 18 | 19 | class func giraffeScreenTitleFont(ofSize fontSize: CGFloat) -> UIFont { 20 | return UIFont.systemFontOfSize(fontSize, weight: UIFontWeightMedium) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/UIKitExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitExtensions.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ReactiveCocoa 11 | 12 | struct AssociationKey { 13 | static var hidden: UInt8 = 1 14 | static var alpha: UInt8 = 2 15 | static var text: UInt8 = 3 16 | static var image: UInt8 = 4 17 | static var title: UInt8 = 5 18 | static var enabled: UInt8 = 6 19 | static var selected: UInt8 = 7 20 | static var animated: UInt8 = 8 21 | } 22 | 23 | // lazily creates a gettable associated property via the given factory 24 | func lazyAssociatedProperty(host: AnyObject, key: UnsafePointer, factory: ()->T) -> T { 25 | var associatedProperty = objc_getAssociatedObject(host, key) as? T 26 | 27 | if associatedProperty == nil { 28 | associatedProperty = factory() 29 | objc_setAssociatedObject(host, key, associatedProperty, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 30 | } 31 | return associatedProperty! 32 | } 33 | 34 | func lazyMutableProperty(host: AnyObject, key: UnsafePointer, setter: T -> (), getter: () -> T) -> MutableProperty { 35 | return lazyAssociatedProperty(host, key: key) { 36 | let property = MutableProperty(getter()) 37 | property.producer 38 | .startWithNext { newValue in 39 | setter(newValue) 40 | } 41 | return property 42 | } 43 | } 44 | 45 | extension UIView { 46 | public var rac_alpha: MutableProperty { 47 | return lazyMutableProperty(self, key: &AssociationKey.alpha, setter: { self.alpha = $0 }, getter: { self.alpha }) 48 | } 49 | 50 | public var rac_hidden: MutableProperty { 51 | return lazyMutableProperty(self, key: &AssociationKey.hidden, setter: { self.hidden = $0 }, getter: { self.hidden }) 52 | } 53 | } 54 | 55 | extension UIImageView { 56 | public var rac_image: MutableProperty { 57 | return lazyMutableProperty(self, key: &AssociationKey.image, setter: { self.image = $0 }, getter: { self.image }) 58 | } 59 | } 60 | 61 | extension UILabel { 62 | public var rac_text: MutableProperty { 63 | return lazyMutableProperty(self, key: &AssociationKey.text, setter: { self.text = $0 }, getter: { self.text ?? "" }) 64 | } 65 | } 66 | 67 | extension UITextField { 68 | public var rac_text: MutableProperty { 69 | return lazyAssociatedProperty(self, key: &AssociationKey.text) { 70 | 71 | self.addTarget(self, action: #selector(UITextField.changed), forControlEvents: UIControlEvents.EditingChanged) 72 | 73 | let property = MutableProperty(self.text ?? "") 74 | property.producer 75 | .startWithNext { newValue in 76 | self.text = newValue 77 | } 78 | return property 79 | } 80 | } 81 | 82 | func changed() { 83 | rac_text.value = self.text! 84 | } 85 | } 86 | 87 | extension UINavigationItem { 88 | public var rac_title: MutableProperty { 89 | return lazyMutableProperty(self, key: &AssociationKey.title, setter: { self.title = $0 }, getter: { self.title }) 90 | } 91 | } 92 | 93 | extension UIBarButtonItem { 94 | public var rac_enabled: MutableProperty { 95 | return lazyMutableProperty(self, key: &AssociationKey.enabled, setter: { self.enabled = $0 }, getter: { self.enabled }) 96 | } 97 | 98 | // TODO: return valid value in rac_selected getter 99 | public var rac_selected: MutableProperty { 100 | return lazyMutableProperty(self, key: &AssociationKey.selected, 101 | setter: { self.image = $0 ? UIImage(named: "familyhl") : UIImage(named: "family") }, 102 | getter: { self.enabled /*fake*/ } 103 | ) 104 | } 105 | } 106 | 107 | extension UIActivityIndicatorView { 108 | public var rac_animated: MutableProperty { 109 | return lazyMutableProperty(self, key: &AssociationKey.animated, setter: { $0 ? self.startAnimating() : self.stopAnimating() }, getter: { self.isAnimating() }) 110 | } 111 | } 112 | 113 | extension UISearchBar: UISearchBarDelegate { 114 | public var rac_text: MutableProperty { 115 | return lazyAssociatedProperty(self, key: &AssociationKey.text, factory: { 116 | self.delegate = self; 117 | self.rac_signalForSelector(#selector(UISearchBarDelegate.searchBar(_:textDidChange:)), fromProtocol: UISearchBarDelegate.self) 118 | .toSignalProducer() 119 | .startWithResult { [weak self] _ in 120 | self?.changed() 121 | } 122 | 123 | let property = MutableProperty(self.text ?? "") 124 | property.producer.startWithResult { result in 125 | self.text = result.value 126 | } 127 | return property 128 | }) 129 | } 130 | 131 | private func changed() { 132 | rac_text.value = self.text ?? "" 133 | } 134 | 135 | func rac_searchBarSearchButtonClicked(callback: ((UISearchBar) -> Void)) { 136 | self.rac_signalForSelector(#selector(UISearchBarDelegate.searchBarSearchButtonClicked(_:)), fromProtocol: UISearchBarDelegate.self) 137 | .toSignalProducer() 138 | .startWithResult { result in 139 | guard let tuple = result.value as? RACTuple else { return } 140 | guard let searchBar = tuple.first as? UISearchBar else { return } 141 | 142 | callback(searchBar) 143 | } 144 | } 145 | 146 | func rac_searchBarCancelButtonClicked(callback: ((UISearchBar) -> Void)) { 147 | self.rac_signalForSelector(#selector(UISearchBarDelegate.searchBarCancelButtonClicked(_:)), fromProtocol: UISearchBarDelegate.self) 148 | .toSignalProducer() 149 | .startWithResult { result in 150 | guard let tuple = result.value as? RACTuple else { return } 151 | guard let searchBar = tuple.first as? UISearchBar else { return } 152 | 153 | callback(searchBar) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/UIViewController+ViewType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ViewType.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ReactiveCocoa 11 | 12 | extension ViewType where Self: UIViewController { 13 | func setupViewBindings() { 14 | let active = NSNotificationCenter.defaultCenter().rac_notifications(UIApplicationDidBecomeActiveNotification) 15 | .map { _ in 16 | true 17 | } 18 | 19 | let inactive = NSNotificationCenter.defaultCenter().rac_notifications(UIApplicationWillResignActiveNotification) 20 | .map { _ in 21 | false 22 | } 23 | 24 | self.viewModel!.isActive <~ merge([active, inactive]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import GiraffeKit 12 | 13 | protocol ViewModelType { 14 | var isActive: MutableProperty { get } 15 | var message: MutableProperty { get } 16 | var shouldHideItemsView: MutableProperty { get } 17 | var itemViewModels: MutableProperty<[AnimatedImageViewModel]> { get } 18 | var shouldDenoteTrending: ConstantProperty { get } 19 | var isLoading: MutableProperty { get } 20 | var statusImage: MutableProperty { get } 21 | } 22 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/ViewType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewType.swift 3 | // Giraffe-iOS 4 | // 5 | // Created by Evgen Dubinin on 7/16/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewType { 12 | associatedtype VM: ViewModelType 13 | var viewModel: VM? { get } 14 | func setupBindings() 15 | } 16 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOS/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Giraffe-iOS 4 | 5 | Created by Evgen Dubinin on 7/9/16. 6 | Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | */ 8 | 9 | "CFBundleDisplayName" = "GIraFfe"; 10 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOSTests/Giraffe_iOSTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Giraffe_iOSTests.swift 3 | // Giraffe-iOSTests 4 | // 5 | // Created by Evgen Dubinin on 7/3/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Giraffe_iOS 11 | 12 | class Giraffe_iOSTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOSUITests/Giraffe_iOSUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Giraffe_iOSUITests.swift 3 | // Giraffe-iOSUITests 4 | // 5 | // Created by Evgen Dubinin on 7/3/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Giraffe_iOSUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Giraffe-iOS/Giraffe-iOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Giraffe.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 30061BC41D2920FE00ABEF7F /* GiraffeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 30061BC31D2920FE00ABEF7F /* GiraffeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 30061BCB1D2920FE00ABEF7F /* GiraffeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30061BC01D2920FE00ABEF7F /* GiraffeKit.framework */; }; 12 | 30061BD01D2920FE00ABEF7F /* GiraffeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30061BCF1D2920FE00ABEF7F /* GiraffeKitTests.swift */; }; 13 | 30502EA21D2D19A70092A8AD /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30502EA11D2D19A60092A8AD /* Service.swift */; }; 14 | 30502EA31D2D19A70092A8AD /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30502EA11D2D19A60092A8AD /* Service.swift */; }; 15 | 30502EA51D2D1AC60092A8AD /* TrendingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30502EA41D2D1AC60092A8AD /* TrendingService.swift */; }; 16 | 30502EA61D2D1AC60092A8AD /* TrendingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30502EA41D2D1AC60092A8AD /* TrendingService.swift */; }; 17 | 305B7C231D4F97A8009B3FF0 /* Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F2AE991D4E6F4D00E3496F /* Parameters.swift */; }; 18 | 307DF0EA1D3ECB5B003EFE41 /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 307DF0E91D3ECB5B003EFE41 /* Unbox.framework */; }; 19 | 307DF0F11D3ED215003EFE41 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307DF0F01D3ED215003EFE41 /* Response.swift */; }; 20 | 307DF0F51D3EDF01003EFE41 /* Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307DF0F41D3EDF01003EFE41 /* Decodable.swift */; }; 21 | 3089E8C21D2D943E00726903 /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089E8C11D2D943E00726903 /* SearchService.swift */; }; 22 | 3089E8C31D2D943E00726903 /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089E8C11D2D943E00726903 /* SearchService.swift */; }; 23 | 3089E8C51D2D9F9400726903 /* ServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089E8C41D2D9F9400726903 /* ServiceTests.swift */; }; 24 | 30A0C2291D3C4E6400627ECB /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A0C2281D3C4E6300627ECB /* Error.swift */; }; 25 | 30A357F01D3EF06F00B6183F /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357EF1D3EF06F00B6183F /* Item.swift */; }; 26 | 30A357F21D3EF0F100B6183F /* Response+Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357F11D3EF0F100B6183F /* Response+Decodable.swift */; }; 27 | 30A357F41D3EF1BE00B6183F /* Framable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357F31D3EF1BE00B6183F /* Framable.swift */; }; 28 | 30A357F61D3EF4E100B6183F /* NSDateFormatter+GiraffeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357F51D3EF4E100B6183F /* NSDateFormatter+GiraffeKit.swift */; }; 29 | 30A357F81D3EF66B00B6183F /* NSDateFormatter+GiraffeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357F71D3EF66B00B6183F /* NSDateFormatter+GiraffeKitTests.swift */; }; 30 | 30A357F91D3EF78800B6183F /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357EF1D3EF06F00B6183F /* Item.swift */; }; 31 | 30A357FA1D3EF7EA00B6183F /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 307DF0E91D3ECB5B003EFE41 /* Unbox.framework */; }; 32 | 30A357FB1D3EF91A00B6183F /* NSDateFormatter+GiraffeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A357F51D3EF4E100B6183F /* NSDateFormatter+GiraffeKit.swift */; }; 33 | 30A357FD1D3EFA6500B6183F /* Unbox.framework in Carthage Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 307DF0E91D3ECB5B003EFE41 /* Unbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 34 | 30F2AE9A1D4E6F4D00E3496F /* Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F2AE991D4E6F4D00E3496F /* Parameters.swift */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXContainerItemProxy section */ 38 | 30061BCC1D2920FE00ABEF7F /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = 30061BB71D2920FE00ABEF7F /* Project object */; 41 | proxyType = 1; 42 | remoteGlobalIDString = 30061BBF1D2920FE00ABEF7F; 43 | remoteInfo = GiraffeKit; 44 | }; 45 | /* End PBXContainerItemProxy section */ 46 | 47 | /* Begin PBXCopyFilesBuildPhase section */ 48 | 30A357FC1D3EFA2900B6183F /* Carthage Copy Frameworks */ = { 49 | isa = PBXCopyFilesBuildPhase; 50 | buildActionMask = 2147483647; 51 | dstPath = ""; 52 | dstSubfolderSpec = 10; 53 | files = ( 54 | 30A357FD1D3EFA6500B6183F /* Unbox.framework in Carthage Copy Frameworks */, 55 | ); 56 | name = "Carthage Copy Frameworks"; 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXCopyFilesBuildPhase section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 30061BC01D2920FE00ABEF7F /* GiraffeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GiraffeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | 30061BC31D2920FE00ABEF7F /* GiraffeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GiraffeKit.h; sourceTree = ""; }; 64 | 30061BC51D2920FE00ABEF7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 65 | 30061BCA1D2920FE00ABEF7F /* GiraffeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GiraffeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 30061BCF1D2920FE00ABEF7F /* GiraffeKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiraffeKitTests.swift; sourceTree = ""; }; 67 | 30061BD11D2920FE00ABEF7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68 | 30502EA11D2D19A60092A8AD /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; 69 | 30502EA41D2D1AC60092A8AD /* TrendingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingService.swift; sourceTree = ""; }; 70 | 307DF0E91D3ECB5B003EFE41 /* Unbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Unbox.framework; path = ../Carthage/Build/iOS/Unbox.framework; sourceTree = ""; }; 71 | 307DF0F01D3ED215003EFE41 /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 72 | 307DF0F41D3EDF01003EFE41 /* Decodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decodable.swift; sourceTree = ""; }; 73 | 3089E8C11D2D943E00726903 /* SearchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchService.swift; sourceTree = ""; }; 74 | 3089E8C41D2D9F9400726903 /* ServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTests.swift; sourceTree = ""; }; 75 | 30A0C2281D3C4E6300627ECB /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 76 | 30A357EF1D3EF06F00B6183F /* Item.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 77 | 30A357F11D3EF0F100B6183F /* Response+Decodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Response+Decodable.swift"; sourceTree = ""; }; 78 | 30A357F31D3EF1BE00B6183F /* Framable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Framable.swift; sourceTree = ""; }; 79 | 30A357F51D3EF4E100B6183F /* NSDateFormatter+GiraffeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSDateFormatter+GiraffeKit.swift"; sourceTree = ""; }; 80 | 30A357F71D3EF66B00B6183F /* NSDateFormatter+GiraffeKitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSDateFormatter+GiraffeKitTests.swift"; sourceTree = ""; }; 81 | 30F2AE991D4E6F4D00E3496F /* Parameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parameters.swift; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 30061BBC1D2920FE00ABEF7F /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 307DF0EA1D3ECB5B003EFE41 /* Unbox.framework in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | 30061BC71D2920FE00ABEF7F /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | 30061BCB1D2920FE00ABEF7F /* GiraffeKit.framework in Frameworks */, 98 | 30A357FA1D3EF7EA00B6183F /* Unbox.framework in Frameworks */, 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | /* End PBXFrameworksBuildPhase section */ 103 | 104 | /* Begin PBXGroup section */ 105 | 30061BB61D2920FE00ABEF7F = { 106 | isa = PBXGroup; 107 | children = ( 108 | 30061BC21D2920FE00ABEF7F /* GiraffeKit */, 109 | 30061BCE1D2920FE00ABEF7F /* GiraffeKitTests */, 110 | 30061BC11D2920FE00ABEF7F /* Products */, 111 | 307DF0E81D3ECB5A003EFE41 /* Frameworks */, 112 | ); 113 | sourceTree = ""; 114 | }; 115 | 30061BC11D2920FE00ABEF7F /* Products */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 30061BC01D2920FE00ABEF7F /* GiraffeKit.framework */, 119 | 30061BCA1D2920FE00ABEF7F /* GiraffeKitTests.xctest */, 120 | ); 121 | name = Products; 122 | sourceTree = ""; 123 | }; 124 | 30061BC21D2920FE00ABEF7F /* GiraffeKit */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 30061BC31D2920FE00ABEF7F /* GiraffeKit.h */, 128 | 307DF0ED1D3ED1C2003EFE41 /* Service */, 129 | 307DF0EE1D3ED1D3003EFE41 /* Model */, 130 | 307DF0F61D3EDF1E003EFE41 /* Common */, 131 | 307DF0EF1D3ED1E8003EFE41 /* Supporting Files */, 132 | ); 133 | path = GiraffeKit; 134 | sourceTree = ""; 135 | }; 136 | 30061BCE1D2920FE00ABEF7F /* GiraffeKitTests */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 30061BCF1D2920FE00ABEF7F /* GiraffeKitTests.swift */, 140 | 3089E8C41D2D9F9400726903 /* ServiceTests.swift */, 141 | 30A357F71D3EF66B00B6183F /* NSDateFormatter+GiraffeKitTests.swift */, 142 | 30061BD11D2920FE00ABEF7F /* Info.plist */, 143 | ); 144 | path = GiraffeKitTests; 145 | sourceTree = ""; 146 | }; 147 | 307DF0E81D3ECB5A003EFE41 /* Frameworks */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 307DF0E91D3ECB5B003EFE41 /* Unbox.framework */, 151 | ); 152 | name = Frameworks; 153 | sourceTree = ""; 154 | }; 155 | 307DF0ED1D3ED1C2003EFE41 /* Service */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 30502EA11D2D19A60092A8AD /* Service.swift */, 159 | 30502EA41D2D1AC60092A8AD /* TrendingService.swift */, 160 | 3089E8C11D2D943E00726903 /* SearchService.swift */, 161 | 30F2AE991D4E6F4D00E3496F /* Parameters.swift */, 162 | ); 163 | name = Service; 164 | sourceTree = ""; 165 | }; 166 | 307DF0EE1D3ED1D3003EFE41 /* Model */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 30A357EF1D3EF06F00B6183F /* Item.swift */, 170 | 307DF0F01D3ED215003EFE41 /* Response.swift */, 171 | 30A357F11D3EF0F100B6183F /* Response+Decodable.swift */, 172 | ); 173 | name = Model; 174 | sourceTree = ""; 175 | }; 176 | 307DF0EF1D3ED1E8003EFE41 /* Supporting Files */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 30061BC51D2920FE00ABEF7F /* Info.plist */, 180 | ); 181 | name = "Supporting Files"; 182 | sourceTree = ""; 183 | }; 184 | 307DF0F61D3EDF1E003EFE41 /* Common */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 307DF0F41D3EDF01003EFE41 /* Decodable.swift */, 188 | 30A0C2281D3C4E6300627ECB /* Error.swift */, 189 | 30A357F31D3EF1BE00B6183F /* Framable.swift */, 190 | 30A357F51D3EF4E100B6183F /* NSDateFormatter+GiraffeKit.swift */, 191 | ); 192 | name = Common; 193 | sourceTree = ""; 194 | }; 195 | /* End PBXGroup section */ 196 | 197 | /* Begin PBXHeadersBuildPhase section */ 198 | 30061BBD1D2920FE00ABEF7F /* Headers */ = { 199 | isa = PBXHeadersBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 30061BC41D2920FE00ABEF7F /* GiraffeKit.h in Headers */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXHeadersBuildPhase section */ 207 | 208 | /* Begin PBXNativeTarget section */ 209 | 30061BBF1D2920FE00ABEF7F /* GiraffeKit */ = { 210 | isa = PBXNativeTarget; 211 | buildConfigurationList = 30061BD41D2920FE00ABEF7F /* Build configuration list for PBXNativeTarget "GiraffeKit" */; 212 | buildPhases = ( 213 | 30061BBB1D2920FE00ABEF7F /* Sources */, 214 | 30061BBC1D2920FE00ABEF7F /* Frameworks */, 215 | 30061BBD1D2920FE00ABEF7F /* Headers */, 216 | 30061BBE1D2920FE00ABEF7F /* Resources */, 217 | 307DF0EC1D3ECF83003EFE41 /* Carthage Copy Frameworks */, 218 | ); 219 | buildRules = ( 220 | ); 221 | dependencies = ( 222 | ); 223 | name = GiraffeKit; 224 | productName = GiraffeKit; 225 | productReference = 30061BC01D2920FE00ABEF7F /* GiraffeKit.framework */; 226 | productType = "com.apple.product-type.framework"; 227 | }; 228 | 30061BC91D2920FE00ABEF7F /* GiraffeKitTests */ = { 229 | isa = PBXNativeTarget; 230 | buildConfigurationList = 30061BD71D2920FE00ABEF7F /* Build configuration list for PBXNativeTarget "GiraffeKitTests" */; 231 | buildPhases = ( 232 | 30061BC61D2920FE00ABEF7F /* Sources */, 233 | 30061BC71D2920FE00ABEF7F /* Frameworks */, 234 | 30061BC81D2920FE00ABEF7F /* Resources */, 235 | 30A357FC1D3EFA2900B6183F /* Carthage Copy Frameworks */, 236 | ); 237 | buildRules = ( 238 | ); 239 | dependencies = ( 240 | 30061BCD1D2920FE00ABEF7F /* PBXTargetDependency */, 241 | ); 242 | name = GiraffeKitTests; 243 | productName = GiraffeKitTests; 244 | productReference = 30061BCA1D2920FE00ABEF7F /* GiraffeKitTests.xctest */; 245 | productType = "com.apple.product-type.bundle.unit-test"; 246 | }; 247 | /* End PBXNativeTarget section */ 248 | 249 | /* Begin PBXProject section */ 250 | 30061BB71D2920FE00ABEF7F /* Project object */ = { 251 | isa = PBXProject; 252 | attributes = { 253 | LastSwiftUpdateCheck = 0800; 254 | LastUpgradeCheck = 0800; 255 | ORGANIZATIONNAME = "Yevhen Dubinin"; 256 | TargetAttributes = { 257 | 30061BBF1D2920FE00ABEF7F = { 258 | CreatedOnToolsVersion = 8.0; 259 | LastSwiftMigration = 0800; 260 | ProvisioningStyle = Automatic; 261 | }; 262 | 30061BC91D2920FE00ABEF7F = { 263 | CreatedOnToolsVersion = 8.0; 264 | ProvisioningStyle = Automatic; 265 | }; 266 | }; 267 | }; 268 | buildConfigurationList = 30061BBA1D2920FE00ABEF7F /* Build configuration list for PBXProject "GiraffeKit" */; 269 | compatibilityVersion = "Xcode 3.2"; 270 | developmentRegion = English; 271 | hasScannedForEncodings = 0; 272 | knownRegions = ( 273 | en, 274 | ); 275 | mainGroup = 30061BB61D2920FE00ABEF7F; 276 | productRefGroup = 30061BC11D2920FE00ABEF7F /* Products */; 277 | projectDirPath = ""; 278 | projectRoot = ""; 279 | targets = ( 280 | 30061BBF1D2920FE00ABEF7F /* GiraffeKit */, 281 | 30061BC91D2920FE00ABEF7F /* GiraffeKitTests */, 282 | ); 283 | }; 284 | /* End PBXProject section */ 285 | 286 | /* Begin PBXResourcesBuildPhase section */ 287 | 30061BBE1D2920FE00ABEF7F /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | 30061BC81D2920FE00ABEF7F /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | /* End PBXResourcesBuildPhase section */ 302 | 303 | /* Begin PBXShellScriptBuildPhase section */ 304 | 307DF0EC1D3ECF83003EFE41 /* Carthage Copy Frameworks */ = { 305 | isa = PBXShellScriptBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | ); 309 | inputPaths = ( 310 | "$(SRCROOT)/../Carthage/Build/iOS/Unbox.framework", 311 | ); 312 | name = "Carthage Copy Frameworks"; 313 | outputPaths = ( 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | shellPath = /bin/sh; 317 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 318 | }; 319 | /* End PBXShellScriptBuildPhase section */ 320 | 321 | /* Begin PBXSourcesBuildPhase section */ 322 | 30061BBB1D2920FE00ABEF7F /* Sources */ = { 323 | isa = PBXSourcesBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | 30A357F01D3EF06F00B6183F /* Item.swift in Sources */, 327 | 30A357F21D3EF0F100B6183F /* Response+Decodable.swift in Sources */, 328 | 307DF0F51D3EDF01003EFE41 /* Decodable.swift in Sources */, 329 | 3089E8C21D2D943E00726903 /* SearchService.swift in Sources */, 330 | 30502EA51D2D1AC60092A8AD /* TrendingService.swift in Sources */, 331 | 30502EA21D2D19A70092A8AD /* Service.swift in Sources */, 332 | 30A357F41D3EF1BE00B6183F /* Framable.swift in Sources */, 333 | 30A0C2291D3C4E6400627ECB /* Error.swift in Sources */, 334 | 30A357F61D3EF4E100B6183F /* NSDateFormatter+GiraffeKit.swift in Sources */, 335 | 307DF0F11D3ED215003EFE41 /* Response.swift in Sources */, 336 | 30F2AE9A1D4E6F4D00E3496F /* Parameters.swift in Sources */, 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | 30061BC61D2920FE00ABEF7F /* Sources */ = { 341 | isa = PBXSourcesBuildPhase; 342 | buildActionMask = 2147483647; 343 | files = ( 344 | 305B7C231D4F97A8009B3FF0 /* Parameters.swift in Sources */, 345 | 30502EA61D2D1AC60092A8AD /* TrendingService.swift in Sources */, 346 | 3089E8C31D2D943E00726903 /* SearchService.swift in Sources */, 347 | 30A357FB1D3EF91A00B6183F /* NSDateFormatter+GiraffeKit.swift in Sources */, 348 | 30061BD01D2920FE00ABEF7F /* GiraffeKitTests.swift in Sources */, 349 | 30A357F81D3EF66B00B6183F /* NSDateFormatter+GiraffeKitTests.swift in Sources */, 350 | 30A357F91D3EF78800B6183F /* Item.swift in Sources */, 351 | 30502EA31D2D19A70092A8AD /* Service.swift in Sources */, 352 | 3089E8C51D2D9F9400726903 /* ServiceTests.swift in Sources */, 353 | ); 354 | runOnlyForDeploymentPostprocessing = 0; 355 | }; 356 | /* End PBXSourcesBuildPhase section */ 357 | 358 | /* Begin PBXTargetDependency section */ 359 | 30061BCD1D2920FE00ABEF7F /* PBXTargetDependency */ = { 360 | isa = PBXTargetDependency; 361 | target = 30061BBF1D2920FE00ABEF7F /* GiraffeKit */; 362 | targetProxy = 30061BCC1D2920FE00ABEF7F /* PBXContainerItemProxy */; 363 | }; 364 | /* End PBXTargetDependency section */ 365 | 366 | /* Begin XCBuildConfiguration section */ 367 | 30061BD21D2920FE00ABEF7F /* Debug */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | ALWAYS_SEARCH_USER_PATHS = NO; 371 | CLANG_ANALYZER_NONNULL = YES; 372 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 373 | CLANG_CXX_LIBRARY = "libc++"; 374 | CLANG_ENABLE_MODULES = YES; 375 | CLANG_ENABLE_OBJC_ARC = YES; 376 | CLANG_WARN_BOOL_CONVERSION = YES; 377 | CLANG_WARN_CONSTANT_CONVERSION = YES; 378 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 379 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 380 | CLANG_WARN_EMPTY_BODY = YES; 381 | CLANG_WARN_ENUM_CONVERSION = YES; 382 | CLANG_WARN_INT_CONVERSION = YES; 383 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 387 | COPY_PHASE_STRIP = NO; 388 | CURRENT_PROJECT_VERSION = 1; 389 | DEBUG_INFORMATION_FORMAT = dwarf; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | ENABLE_TESTABILITY = YES; 392 | GCC_C_LANGUAGE_STANDARD = gnu99; 393 | GCC_DYNAMIC_NO_PIC = NO; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_OPTIMIZATION_LEVEL = 0; 396 | GCC_PREPROCESSOR_DEFINITIONS = ( 397 | "DEBUG=1", 398 | "$(inherited)", 399 | ); 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 407 | MTL_ENABLE_DEBUG_INFO = YES; 408 | ONLY_ACTIVE_ARCH = YES; 409 | SDKROOT = iphoneos; 410 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 411 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 412 | SWIFT_VERSION = 2.3; 413 | TARGETED_DEVICE_FAMILY = "1,2"; 414 | VERSIONING_SYSTEM = "apple-generic"; 415 | VERSION_INFO_PREFIX = ""; 416 | }; 417 | name = Debug; 418 | }; 419 | 30061BD31D2920FE00ABEF7F /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ALWAYS_SEARCH_USER_PATHS = NO; 423 | CLANG_ANALYZER_NONNULL = YES; 424 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 425 | CLANG_CXX_LIBRARY = "libc++"; 426 | CLANG_ENABLE_MODULES = YES; 427 | CLANG_ENABLE_OBJC_ARC = YES; 428 | CLANG_WARN_BOOL_CONVERSION = YES; 429 | CLANG_WARN_CONSTANT_CONVERSION = YES; 430 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 431 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 432 | CLANG_WARN_EMPTY_BODY = YES; 433 | CLANG_WARN_ENUM_CONVERSION = YES; 434 | CLANG_WARN_INT_CONVERSION = YES; 435 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 436 | CLANG_WARN_UNREACHABLE_CODE = YES; 437 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 438 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 439 | COPY_PHASE_STRIP = NO; 440 | CURRENT_PROJECT_VERSION = 1; 441 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 442 | ENABLE_NS_ASSERTIONS = NO; 443 | ENABLE_STRICT_OBJC_MSGSEND = YES; 444 | GCC_C_LANGUAGE_STANDARD = gnu99; 445 | GCC_NO_COMMON_BLOCKS = YES; 446 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 447 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 448 | GCC_WARN_UNDECLARED_SELECTOR = YES; 449 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 450 | GCC_WARN_UNUSED_FUNCTION = YES; 451 | GCC_WARN_UNUSED_VARIABLE = YES; 452 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 453 | MTL_ENABLE_DEBUG_INFO = NO; 454 | SDKROOT = iphoneos; 455 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 456 | SWIFT_VERSION = 2.3; 457 | TARGETED_DEVICE_FAMILY = "1,2"; 458 | VALIDATE_PRODUCT = YES; 459 | VERSIONING_SYSTEM = "apple-generic"; 460 | VERSION_INFO_PREFIX = ""; 461 | }; 462 | name = Release; 463 | }; 464 | 30061BD51D2920FE00ABEF7F /* Debug */ = { 465 | isa = XCBuildConfiguration; 466 | buildSettings = { 467 | CLANG_ENABLE_MODULES = YES; 468 | DEFINES_MODULE = YES; 469 | DYLIB_COMPATIBILITY_VERSION = 1; 470 | DYLIB_CURRENT_VERSION = 1; 471 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 472 | FRAMEWORK_SEARCH_PATHS = ( 473 | "$(BUILT_PRODUCTS_DIR)/**", 474 | "$(SRCROOT)/../Carthage/Build/iOS", 475 | ); 476 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../Carthage/Build/iOS/**"; 477 | INFOPLIST_FILE = GiraffeKit/Info.plist; 478 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 480 | PRODUCT_BUNDLE_IDENTIFIER = com.devgeniy.GiraffeKit; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | SKIP_INSTALL = YES; 483 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 484 | SWIFT_VERSION = 2.3; 485 | }; 486 | name = Debug; 487 | }; 488 | 30061BD61D2920FE00ABEF7F /* Release */ = { 489 | isa = XCBuildConfiguration; 490 | buildSettings = { 491 | CLANG_ENABLE_MODULES = YES; 492 | DEFINES_MODULE = YES; 493 | DYLIB_COMPATIBILITY_VERSION = 1; 494 | DYLIB_CURRENT_VERSION = 1; 495 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 496 | FRAMEWORK_SEARCH_PATHS = ( 497 | "$(BUILT_PRODUCTS_DIR)/**", 498 | "$(SRCROOT)/../Carthage/Build/iOS", 499 | ); 500 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../Carthage/Build/iOS/**"; 501 | INFOPLIST_FILE = GiraffeKit/Info.plist; 502 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 503 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 504 | PRODUCT_BUNDLE_IDENTIFIER = com.devgeniy.GiraffeKit; 505 | PRODUCT_NAME = "$(TARGET_NAME)"; 506 | SKIP_INSTALL = YES; 507 | SWIFT_VERSION = 2.3; 508 | }; 509 | name = Release; 510 | }; 511 | 30061BD81D2920FE00ABEF7F /* Debug */ = { 512 | isa = XCBuildConfiguration; 513 | buildSettings = { 514 | FRAMEWORK_SEARCH_PATHS = ( 515 | "$(BUILT_PRODUCTS_DIR)/**", 516 | "$(SRCROOT)/../Carthage/Build/iOS", 517 | ); 518 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../Carthage/Build/iOS"; 519 | INFOPLIST_FILE = GiraffeKitTests/Info.plist; 520 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 521 | PRODUCT_BUNDLE_IDENTIFIER = com.devgeniy.GiraffeKitTests; 522 | PRODUCT_NAME = "$(TARGET_NAME)"; 523 | SWIFT_VERSION = 2.3; 524 | }; 525 | name = Debug; 526 | }; 527 | 30061BD91D2920FE00ABEF7F /* Release */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | FRAMEWORK_SEARCH_PATHS = ( 531 | "$(BUILT_PRODUCTS_DIR)/**", 532 | "$(SRCROOT)/../Carthage/Build/iOS", 533 | ); 534 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../Carthage/Build/iOS"; 535 | INFOPLIST_FILE = GiraffeKitTests/Info.plist; 536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 537 | PRODUCT_BUNDLE_IDENTIFIER = com.devgeniy.GiraffeKitTests; 538 | PRODUCT_NAME = "$(TARGET_NAME)"; 539 | SWIFT_VERSION = 2.3; 540 | }; 541 | name = Release; 542 | }; 543 | /* End XCBuildConfiguration section */ 544 | 545 | /* Begin XCConfigurationList section */ 546 | 30061BBA1D2920FE00ABEF7F /* Build configuration list for PBXProject "GiraffeKit" */ = { 547 | isa = XCConfigurationList; 548 | buildConfigurations = ( 549 | 30061BD21D2920FE00ABEF7F /* Debug */, 550 | 30061BD31D2920FE00ABEF7F /* Release */, 551 | ); 552 | defaultConfigurationIsVisible = 0; 553 | defaultConfigurationName = Release; 554 | }; 555 | 30061BD41D2920FE00ABEF7F /* Build configuration list for PBXNativeTarget "GiraffeKit" */ = { 556 | isa = XCConfigurationList; 557 | buildConfigurations = ( 558 | 30061BD51D2920FE00ABEF7F /* Debug */, 559 | 30061BD61D2920FE00ABEF7F /* Release */, 560 | ); 561 | defaultConfigurationIsVisible = 0; 562 | defaultConfigurationName = Release; 563 | }; 564 | 30061BD71D2920FE00ABEF7F /* Build configuration list for PBXNativeTarget "GiraffeKitTests" */ = { 565 | isa = XCConfigurationList; 566 | buildConfigurations = ( 567 | 30061BD81D2920FE00ABEF7F /* Debug */, 568 | 30061BD91D2920FE00ABEF7F /* Release */, 569 | ); 570 | defaultConfigurationIsVisible = 0; 571 | defaultConfigurationName = Release; 572 | }; 573 | /* End XCConfigurationList section */ 574 | }; 575 | rootObject = 30061BB71D2920FE00ABEF7F /* Project object */; 576 | } 577 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum DecodingResult { 12 | case Success(ValueType) 13 | case Failure(ErrorType) 14 | } 15 | 16 | public protocol Decodable { 17 | associatedtype Value 18 | associatedtype Error 19 | static func decodedFrom(data data: NSData, response: NSURLResponse) -> DecodingResult 20 | } 21 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/18/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | ////////////////// 12 | // Error 13 | ////////////////// 14 | private let GiraffeErrorDomain = "com.devgeniy.GiraffeKit.Error" 15 | 16 | public enum GiraffeError: ErrorType, CustomStringConvertible { 17 | case UnknownError 18 | case NetworkError 19 | case ParserError 20 | case LoadImageError(String) 21 | // TODO: extend to more cases 22 | 23 | public var nsError: NSError { 24 | // TODO: add custom error code 25 | return NSError(domain: GiraffeErrorDomain, code: 0, userInfo: nil) 26 | } 27 | 28 | // MARK: CustomStringConvertible - 29 | 30 | public var description: String { 31 | switch self { 32 | case .NetworkError: 33 | return "Network Error" 34 | case ParserError: 35 | return "Parser Error" 36 | case LoadImageError(let localizedDescription): 37 | return "Cannot load image. \(localizedDescription)" 38 | default: 39 | return "Unknown Error" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Framable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Framable.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The protocol helps to incapsulate the image data variant 12 | /// required by current framework user 13 | public protocol Framable { 14 | func singleFrame() -> Image 15 | func multiFrame() -> Image 16 | } 17 | 18 | /// Default implementation 19 | 20 | extension Item: Framable { 21 | public func singleFrame() -> Image { 22 | return self.images 23 | .indexOf { $0.variant == ImageVariant.fixedHeightStill } 24 | .map { self.images[$0]}! 25 | } 26 | 27 | public func multiFrame() -> Image { 28 | return self.images 29 | .indexOf { $0.variant == ImageVariant.fixedHeight } 30 | .map { self.images[$0]}! 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/GiraffeKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // GiraffeKit.h 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/3/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for GiraffeKit. 12 | FOUNDATION_EXPORT double GiraffeKitVersionNumber; 13 | 14 | //! Project version string for GiraffeKit. 15 | FOUNDATION_EXPORT const unsigned char GiraffeKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Item.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Unbox 11 | import CoreGraphics 12 | 13 | //////////////////////////////////// 14 | // MARK: Main Model - 15 | //////////////////////////////////// 16 | public struct Item { 17 | public var id: String 18 | public var type: ContentType 19 | public var rating: Rating 20 | public var trendingDate: NSDate? 21 | public var images: [Image] 22 | 23 | // MARK: FamilyFriendlyDetectable - 24 | 25 | public func isFamilyFriendly() -> Bool { 26 | return self.rating.isFamilyFriendly() 27 | } 28 | } 29 | 30 | //////////////////////////////////// 31 | // MARK: Supporting Models - 32 | //////////////////////////////////// 33 | 34 | public enum ContentType: String { 35 | case gif 36 | case unknown 37 | } 38 | 39 | public enum Rating: String, CustomStringConvertible, FamilyFriendlyDetectable { 40 | // Source: https://www.quora.com/What-do-G-PG-PG-13-R-NC-17-movie-ratings-mean 41 | case y // ??? 42 | case g // General Audiences. All ages admitted. 43 | case pg // Parental Guidance Suggested. Some material may not be suitable for children. 44 | case pg13 = "pg-13" // Parents Strongly Cautioned. Some material may be inappropriate for children under 13. 45 | case r // Restricted. Under 17 requires accompanying parent or adult guardian. 46 | case unspecified = "" 47 | 48 | // MARK: FamilyFriendlyDetectable - 49 | 50 | public func isFamilyFriendly() -> Bool { 51 | //rated y,g, or pg 52 | switch self { 53 | case .y,.g,.pg: 54 | return true 55 | default: 56 | return false 57 | } 58 | } 59 | 60 | // MARK: CustomStringConvertible - 61 | 62 | public var description: String { 63 | get { 64 | return self.rawValue 65 | } 66 | } 67 | } 68 | 69 | public struct Image { 70 | public var variant: ImageVariant = .unknown 71 | public var url: NSURL 72 | public var width: CGFloat 73 | public var height: CGFloat 74 | 75 | // MARK: Convenience methods - 76 | 77 | public var size: CGSize { 78 | get { return CGSizeMake(self.width, self.height); } 79 | } 80 | 81 | mutating func specify(variant newVariant: ImageVariant) { 82 | self.variant = newVariant 83 | } 84 | } 85 | 86 | public enum ImageVariant { 87 | case fixedHeight 88 | case fixedHeightStill 89 | /* 90 | fixed_height_downsampled 91 | 92 | case fixedHeightSmall 93 | case fixedHeightSmallStill 94 | 95 | fixed_width 96 | fixed_width_still 97 | fixed_width_downsampled 98 | 99 | fixed_height_small 100 | fixed_height_small_still 101 | 102 | fixed_width_small 103 | fixed_width_small_still 104 | 105 | downsized 106 | downsized_still 107 | downsized_large 108 | downsized_medium 109 | 110 | original 111 | original_still 112 | 113 | looping 114 | */ 115 | 116 | case unknown 117 | } 118 | 119 | //////////////////////////////////// 120 | // MARK: Protocols - 121 | //////////////////////////////////// 122 | 123 | public protocol FamilyFriendlyDetectable { 124 | func isFamilyFriendly() -> Bool 125 | } 126 | 127 | //////////////////////////////////// 128 | // MARK: Unboxable extension - 129 | //////////////////////////////////// 130 | 131 | private struct Static { 132 | static let dateFormatter : NSDateFormatter = { 133 | let formatter = NSDateFormatter.giraffeDateFormatter() 134 | return formatter 135 | }() 136 | } 137 | 138 | 139 | extension Item: Unboxable { 140 | public init(unboxer: Unboxer) { 141 | self.id = unboxer.unbox("id") 142 | self.type = unboxer.unbox("type") 143 | self.rating = unboxer.unbox("rating") 144 | self.trendingDate = unboxer.unbox("trending_datetime", formatter: Static.dateFormatter) 145 | 146 | self.images = [] 147 | 148 | // unboxing image variants 149 | 150 | var fixedHeightImage: Image = unboxer.unbox("images.fixed_height", isKeyPath: true) 151 | fixedHeightImage.specify(variant: .fixedHeight) 152 | self.images.append(fixedHeightImage) 153 | 154 | var fixedHeightStill: Image = unboxer.unbox("images.fixed_height_still", isKeyPath: true) 155 | fixedHeightStill.specify(variant: .fixedHeightStill) 156 | self.images.append(fixedHeightStill) 157 | } 158 | } 159 | 160 | extension ContentType: UnboxableEnum { 161 | public static func unboxFallbackValue() -> ContentType { 162 | return .unknown 163 | } 164 | } 165 | 166 | extension Rating: UnboxableEnum { 167 | public static func unboxFallbackValue() -> Rating { 168 | return .unspecified 169 | } 170 | } 171 | 172 | extension Image: Unboxable { 173 | public init(unboxer: Unboxer) { 174 | self.url = unboxer.unbox("url") 175 | self.variant = .unknown 176 | self.width = unboxer.unbox("width") 177 | self.height = unboxer.unbox("height") 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/NSDateFormatter+GiraffeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateFormatter+GiraffeKit.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSDateFormatter { 12 | static func giraffeDateFormatter() -> NSDateFormatter { 13 | let dateFormatter = NSDateFormatter() 14 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 15 | return dateFormatter 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Parameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameters.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/31/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Parameters: ServiceActionBodyTransformable { 12 | public let limit: Int? // (optional) number of results to return, maximum 100. Default 25. 13 | public let offset: Int? // (optional) results offset, defaults to 0. 14 | public let rating: Rating? // limit results to those rated (y,g, pg, pg-13 or r). 15 | // TODO: add fmt (fmt - (optional) return results in html or json format (useful for viewing responses as GIFs to debug/test) ) 16 | 17 | public init(limit: Int? = nil, offset: Int? = nil, rating: Rating? = nil) { 18 | self.limit = limit 19 | self.offset = offset 20 | self.rating = rating 21 | } 22 | 23 | // MARK: ServiceActionBodyTransformable - 24 | 25 | func serviceActionBody() -> ServiceActionBody? { 26 | var result: ServiceActionBody = [:] 27 | if let _ = limit { result["limit"] = limit!.description } 28 | if let _ = offset { result["offset"] = offset!.description } 29 | if let _ = rating { result["rating"] = rating!.description } 30 | return result.isEmpty ? nil : result // convert empty to nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Response+Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Decodable.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Unbox 11 | 12 | //////////////////////////////////// 13 | // MARK: Decodable extension - 14 | //////////////////////////////////// 15 | 16 | // TODO: make this reactive: 17 | // public static func decodedFrom(data data: NSData, response: NSURLResponse) -> SignalProducer 18 | 19 | extension Response: Decodable { 20 | // TODO: make this throw 21 | public static func decodedFrom(data data: NSData, response URLResponse: NSURLResponse) -> DecodingResult { 22 | do { 23 | let response: Response = try Unbox(data) 24 | return .Success(response) 25 | } catch let unboxError as UnboxError { 26 | print("Unbox error during decoding: \(unboxError)") 27 | print(unboxError.description) 28 | // TODO: throw UnboxError 29 | return .Failure(GiraffeError.ParserError) 30 | } catch let unknownError { 31 | print("Unknown error during decoding: \(unknownError)") 32 | return .Failure(GiraffeError.ParserError) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Unbox 11 | 12 | //////////////////////////////////// 13 | // MARK: Model - 14 | //////////////////////////////////// 15 | 16 | public struct Meta { 17 | public var status: Int 18 | public var message: String 19 | } 20 | 21 | public struct Page { 22 | public var totalCount: Int? 23 | public var count: Int 24 | public var offset: Int 25 | 26 | // MARK: Convinient Methods - 27 | 28 | public func hasNextPage() -> Bool { 29 | guard let total = totalCount else { return true } // no limit 30 | let index = indexOfFirstElementOnNextPage() 31 | return (index > total) ? false : true 32 | } 33 | 34 | public func indexOfFirstElementOnNextPage() -> Int { 35 | return offset + count 36 | } 37 | } 38 | 39 | public struct Response { 40 | public var data: [Item] 41 | public var meta: Meta 42 | public var pagination: Page 43 | } 44 | 45 | //////////////////////////////////// 46 | // MARK: Convenience methods - 47 | //////////////////////////////////// 48 | 49 | extension Response { 50 | public var zeroItems: Bool { 51 | get { 52 | return (self.data.count == 0) 53 | } 54 | } 55 | } 56 | 57 | //////////////////////////////////// 58 | // MARK: Unboxable extension - 59 | //////////////////////////////////// 60 | 61 | extension Meta: Unboxable { 62 | public init(unboxer: Unboxer) { 63 | self.status = unboxer.unbox("status") 64 | self.message = unboxer.unbox("msg") 65 | } 66 | } 67 | 68 | extension Page: Unboxable { 69 | public init(unboxer: Unboxer) { 70 | self.count = unboxer.unbox("count") 71 | self.offset = unboxer.unbox("offset") 72 | self.totalCount = unboxer.unbox("total_count") 73 | } 74 | } 75 | 76 | extension Response: Unboxable { 77 | public init(unboxer: Unboxer) { 78 | self.data = unboxer.unbox("data") 79 | self.meta = unboxer.unbox("meta") 80 | self.pagination = unboxer.unbox("pagination") 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/SearchService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchService.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/6/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Search: ServiceActionBodyTransformable { 12 | private let query: String // search query term or phrase 13 | private let parameters: Parameters? 14 | 15 | init(query: String, parameters: Parameters? = nil) { 16 | self.query = query 17 | self.parameters = parameters 18 | } 19 | 20 | func serviceActionBody() -> ServiceActionBody? { 21 | let escapedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) 22 | var actionBody = ["q": escapedQuery!] 23 | guard let _ = self.parameters else { 24 | return actionBody 25 | } 26 | guard let paramBody = self.parameters!.serviceActionBody() else { 27 | return actionBody 28 | } 29 | 30 | for key in paramBody.keys { 31 | actionBody[key] = paramBody[key] 32 | } 33 | return actionBody 34 | } 35 | } 36 | 37 | public struct SearchService: ServiceProtocol { 38 | public let actionPath = "search" 39 | public let actionBody: ServiceActionBody? 40 | 41 | public init(query: String, parameters: Parameters? = nil) { 42 | self.init(search: Search(query: query, parameters: parameters)) 43 | } 44 | 45 | public init(search: Search) { 46 | actionBody = search.serviceActionBody() 47 | } 48 | } 49 | 50 | extension SearchService: ServiceRequestable {} 51 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Service.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/6/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: Action Body Type/Proto - 12 | 13 | public typealias ServiceActionBody = [String: String] 14 | 15 | protocol ServiceActionBodyTransformable { 16 | func serviceActionBody() -> ServiceActionBody? 17 | } 18 | 19 | // MARK: Transfer Protocol Type - 20 | 21 | public enum TransferProtocol: String { 22 | case http, https 23 | } 24 | 25 | // MARK: Main Service Protocols - 26 | 27 | public protocol ServiceProtocol { 28 | var transferProtocol: TransferProtocol { get } 29 | var basePath: String { get } 30 | var apiKey: String { get } 31 | var actionPath: String { get } 32 | var actionBody: ServiceActionBody? { get } 33 | } 34 | 35 | /// By implementing this protocol, 36 | /// type is able to be passed as NSURLRequest 37 | public protocol ServiceRequestable { 38 | // TODO: actionMethod() -> ServiceActionMethod (e.g. GET, POST, PUT, etc.) - YAGNI 39 | func request() -> NSURLRequest 40 | } 41 | 42 | // MARK: ServiceProtocol Default - 43 | 44 | extension ServiceProtocol { // ???: Do we need some constraints to this proto extension? 45 | public var transferProtocol: TransferProtocol { 46 | get { return .https } 47 | } 48 | 49 | public var basePath: String { 50 | get { return "api.giphy.com/v1/gifs/" } 51 | } 52 | 53 | public var apiKey: String { 54 | get { return "dc6zaTOxFJmzC" } 55 | } 56 | 57 | public var actionPath: String { 58 | get { return "" } 59 | } 60 | 61 | public var actionBody: [String: String]? { 62 | get { return nil } 63 | } 64 | } 65 | 66 | // MARK: ServiceRequestable Default - 67 | 68 | extension ServiceRequestable where Self: ServiceProtocol { 69 | public func request() -> NSURLRequest { 70 | var requestPath: String = "\(self.transferProtocol)://" + basePath + actionPath 71 | requestPath += "?api_key=\(apiKey)" 72 | if let actionBody = self.actionBody { 73 | for (paramKey, paramValue) in actionBody { 74 | requestPath.appendContentsOf("&\(paramKey)=\(paramValue)") 75 | } 76 | } 77 | return NSURLRequest(URL: NSURL(string: requestPath)!) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKit/TrendingService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrendingService.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/6/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct TrendingService: ServiceProtocol { 12 | public let actionPath = "trending" 13 | public let actionBody: ServiceActionBody? 14 | public init() { 15 | actionBody = nil 16 | } 17 | public init(parameters: Parameters) { 18 | guard let body = parameters.serviceActionBody() where body.isEmpty == false else { 19 | actionBody = nil 20 | return; 21 | } 22 | actionBody = parameters.serviceActionBody() 23 | } 24 | } 25 | 26 | extension TrendingService: ServiceRequestable {} 27 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKitTests/GiraffeKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GiraffeKitTests.swift 3 | // GiraffeKitTests 4 | // 5 | // Created by Evgen Dubinin on 7/3/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GiraffeKit 11 | 12 | class GiraffeKitTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKitTests/NSDateFormatter+GiraffeKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateFormatter+GiraffeKitTests.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/20/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GiraffeKit 11 | 12 | class NSDateFormatter_GiraffeKitTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testFormat() { 25 | let formatter = NSDateFormatter.giraffeDateFormatter() 26 | let testDate = formatter.dateFromString("2016-07-19 21:15:02") 27 | XCTAssert((testDate) != nil) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /GiraffeKit/GiraffeKitTests/ServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceTests.swift 3 | // GiraffeKit 4 | // 5 | // Created by Evgen Dubinin on 7/6/16. 6 | // Copyright © 2016 Yevhen Dubinin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ServiceTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testTrendingService() { 24 | let service = TrendingService() 25 | let req = service.request() 26 | XCTAssertNotNil(req.URL) 27 | 28 | let testString = "https://api.giphy.com/v1/gifs/trending?api_key=dc6zaTOxFJmzC" 29 | XCTAssertTrue(req.URL!.absoluteString == testString) 30 | } 31 | 32 | func testSearchService() { 33 | let service = SearchService(query: "cat") 34 | let req = service.request() 35 | XCTAssertNotNil(req.URL) 36 | 37 | let testString = "https://api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&q=cat" 38 | XCTAssertTrue(req.URL!.absoluteString == testString) 39 | } 40 | 41 | func testSearchServiceCyrillic() { 42 | let service = SearchService(query: "кот") 43 | let req = service.request() 44 | XCTAssertNotNil(req.URL) 45 | 46 | let testString = "https://api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&q=%D0%BA%D0%BE%D1%82" 47 | XCTAssertTrue(req.URL!.absoluteString == testString) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Evgeniy Dubinin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Repo is archived by the author] 2 | 3 | # Giraffe 4 | 5 | iOS client for Giphy (http://giphy.com) written in Swift with ReactiveCocoa 6 | 7 | ![image](App_Preview_Giraffe_Screens.png) 8 | 9 | # Requirements 10 | - iOS 10.0 Beta 11 | - Xcode 8 Beta 2 12 | - Swift 2.3 13 | - Carthage 14 | 15 | # Why Swift 2.3 ? 16 | 17 | Swift 2.3 was chosen for initial version of the app, because at the time, not many open-source libraries were migrated to Swift 3. However, author's intention was to use latest tools and migrate 3rd party frameworks by himself (when needed) with less effort than same migration from Swift 2.2 to Swift 3 might have been required. 18 | 19 | The following quote from [Swift 3 and Xcode 8](https://developer.apple.com/swift/blog/?id=36) blogpost explains Swift 2.3 support in Xcode 8. 20 | 21 | > Swift 3 is the primary development language supported within Xcode 8 so there are a couple notes to consider if you chose to continue using Swift 2.3. First, Swift 2.3 and Swift 3 are not binary compatible so your app's entire code base needs to pick one version of Swift. Both versions are fully supported by the compiler, SDKs, and debugger, but other features of the IDE may not work with Swift 2.3. For instance, Playgrounds in Xcode only work with Swift 3, and notably the Swift Playgrounds app for iPad also uses Swift 3. Xcode project templates all use Swift 3, and all documentation is presented in a format appropriate for Swift 3. 22 | 23 | Eventually, author admits that using Swift 2.3 slowed down the development, because: 24 | * [Nuke](https://github.com/kean/Nuke) required [migration to Swift 2.3](https://github.com/evgeniyd/Nuke/tree/Swift_2_3) 25 | * [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)'s Swift 2.3 branch was under development at the time by contributors (now merged in [v4.2.2 release](https://github.com/ReactiveCocoa/ReactiveCocoa/releases/tag/v4.2.2)) 26 | * Setup with Carthage took more time to figure out 27 | * Xcode 8 (Beta) & Xcode 7, both had difficulties working simultaneously 28 | 29 | # Setup 30 | 31 | As mentioned above, the current version of the app is written in Swift 2.3 Xcode 8 (both are in beta now). Because of this, setup takes several steps (not just few). 32 | 33 | Open Terminal and change the default `xcodebuild` to Xcode 8 beta 34 | 35 | $ sudo xcode-select --switch /Applications/Xcode-beta.app/Contents/Developer 36 | 37 | `cd` to the folder you've cloned the repo to 38 | 39 | $ cd /Path/To/Repo/Giraffe/ 40 | 41 | Bootstrap but don't build 42 | 43 | $ carthage bootstrap --no-build 44 | 45 | With Swift 2.3's toolchain, build 3rd party libraries 46 | 47 | $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 carthage build --platform iOS 48 | 49 | In order to build & run the app on a device, set the Development Team for the following targets: 50 | * GiraffeKit framework 51 | * Giraffe-iOS 52 | 53 | # What 54 | *Architecture:* 55 | * [x] MVVM 56 | * [x] [FRP](https://en.wikipedia.org/wiki/Functional_reactive_programming) via [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) 57 | * [x] iOS Dynamic Framework with common code 58 | * [x] Protocol Extensions, Generics, Closures, etc.. 59 | * [ ] Migrate to Swift 3 60 | * [ ] Pagination for search results 61 | * [ ] Refactoring ViewModels [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 62 | * [ ] Error handling (SignalProducer in particular) 63 | * [ ] Retry request after failure 64 | * [ ] Unit-tests 65 | * [ ] Reactive object mapping 66 | * [ ] Document the GiraffeKit framework's code 67 | * [ ] WebP support 68 | 69 | *Features:* 70 | * [x] Trending list 71 | * [x] Search GIF by term 72 | * [x] Denote trending GIFs in search results 73 | * [x] Filter to only family friendly GIFs 74 | * [ ] Pull-to-refresh 75 | * [ ] Download still images prior to downloading animated ones 76 | * [ ] GIF preview via [Peek and Pop](http://www.apple.com/iphone-6s/3d-touch/) 77 | * [ ] Sharing 78 | * [ ] Play/Stop animation control 79 | * [ ] SiriKit for searching GIFs? 80 | 81 | # Credits 82 | 83 | The app's look and feel wouldn't be possible without these guys: 84 | * [Lina Kononenko](https://www.facebook.com/linakononenko) (UI/UX) 85 | * [Max Semenenko](https://www.facebook.com/max.nitsa) (Giraffe arts) 86 | 87 | # License 88 | 89 | Giraffe is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 90 | --------------------------------------------------------------------------------