├── .codecov.yml ├── .github └── workflows │ ├── pull_request.yml │ └── push.yml ├── .gitignore ├── .hound.yml ├── .ruby-version ├── .swift-version ├── .swiftlint.yml ├── CHANGELOG.md ├── Dangerfile ├── Demo-Info.plist ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── Podfile ├── Podfile.lock ├── README.md ├── fastlane ├── Matchfile └── Scanfile ├── iCookTV.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── iCookTV Demo.xcscheme │ └── iCookTV.xcscheme ├── iCookTV.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── iCookTV ├── AppDelegate.swift ├── Assets.xcassets │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - Large.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── icook-tvOS-large-icon-3.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── icook-tvOS-large-icon-1.png │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── icook-tvOS-large-icon-2.png │ │ │ │ └── Contents.json │ │ ├── App Icon - Small.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── icook-tvOS-small-icon-3.png │ │ │ │ │ └── icook-tvOS-small-icon-3@2x.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── icook-tvOS-small-icon-1.png │ │ │ │ │ └── icook-tvOS-small-icon-1@2x.png │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── icook-tvOS-small-icon-2.png │ │ │ │ └── icook-tvOS-small-icon-2@2x.png │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Wide_2320x720.png │ │ │ └── Top Shelf Wide_2320x720@2x.png │ │ └── Top Shelf Image.imageset │ │ │ ├── Contents.json │ │ │ ├── Top Shelf_1920x720.png │ │ │ └── Top Shelf_1920x720@2x.png │ ├── Contents.json │ ├── DemoLaunchScreenImage.imageset │ │ ├── Contents.json │ │ ├── LaunchScreen3_Demo.png │ │ └── LaunchScreen3_Demo@2x.png │ ├── LaunchScreenImage.imageset │ │ ├── Contents.json │ │ ├── LaunchScreen3.png │ │ └── LaunchScreen3@2x.png │ ├── icook-tv-banner-food-back.imageset │ │ ├── Contents.json │ │ └── icook-tv-banner-food-back.pdf │ ├── icook-tv-banner-food-front.imageset │ │ ├── Contents.json │ │ └── icook-tv-banner-food-front.pdf │ ├── icook-tv-cat.imageset │ │ ├── Contents.json │ │ └── icook-tv-cat.pdf │ └── icook-tv-logo.imageset │ │ ├── Contents.json │ │ └── icook-tv-logo.pdf ├── Base.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── Controllers │ ├── CategoriesViewController.swift │ ├── HistoryViewController.swift │ ├── LaunchViewController.swift │ ├── TrackableNavigationController.swift │ ├── VideoPlayerController.swift │ └── VideosViewController.swift ├── Debug.swift ├── DemoLaunchScreen.storyboard ├── Extensions │ ├── CGRect+Grid.swift │ ├── DataRequest+Result.swift │ ├── UIColor+TV.swift │ ├── UIFont+TV.swift │ ├── UIImage+Grid.swift │ ├── UIViewController+Alert.swift │ └── Video+PlayerItem.swift ├── Helpers │ ├── CoverBuilder.swift │ ├── GroundControl.swift │ ├── HistoryManager.swift │ ├── KeyPathDecoding.swift │ └── Tracker.swift ├── Info.plist ├── LaunchScreen.storyboard ├── Metrics.swift ├── Models │ ├── CategoriesCollection.swift │ ├── CategoriesDataSource.swift │ ├── Category.swift │ ├── DataCollection.swift │ ├── DataSource.swift │ ├── SourceType.swift │ ├── Video.swift │ ├── VideosCollection.swift │ └── VideosDataSource.swift ├── Protocols │ ├── BlurBackgroundPresentable.swift │ ├── DataFetching.swift │ ├── DropdownMenuPresentable.swift │ ├── LoadingIndicatorPresentable.swift │ ├── OverlayViewPresentable.swift │ ├── Reusable.swift │ ├── Trackable.swift │ └── VideosGridLayout.swift ├── Views │ ├── CategoryCell.swift │ ├── EmptyStateView.swift │ ├── InsetLabel.swift │ ├── MainMenuView.swift │ ├── MenuButton.swift │ ├── MenuView.swift │ ├── SectionHeaderView.swift │ └── VideoCell.swift ├── en.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── iCookTVKeys.swift ├── zh-Hans.lproj │ ├── InfoPlist.strings │ └── Localizable.strings └── zh-Hant.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── iCookTVTests ├── Category.json ├── CategorySpec.swift ├── DataCollectionSpec.swift ├── DataSourceSpec.swift ├── Info.plist ├── ResourceHelper.swift ├── Video.json └── VideoSpec.swift ├── mock-GoogleService-Info.plist └── scripts └── crashlytics.sh /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | changes: off 6 | comment: 7 | layout: "files" 8 | ignore: 9 | - "*Tests/**/*" 10 | - "Pods/**/*" 11 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: iOS review 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Apply Ruby 11 | uses: actions/setup-ruby@v1 12 | with: 13 | ruby-version: '2.x' 14 | - uses: actions/cache@v1 15 | with: 16 | path: vendor/bundle 17 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 18 | restore-keys: | 19 | ${{ runner.os }}-gems- 20 | - name: Bundle install 21 | run: | 22 | bundle config path vendor/bundle 23 | bundle install --jobs 4 --retry 3 24 | - name: Run Danger 25 | run: bundle exec danger 26 | env: 27 | DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | - name: Fetch base ref 29 | run: git fetch --no-tags --prune --depth=1 origin ${{ github.base_ref }} 30 | - name: Run SwiftLint 31 | uses: norio-nomura/action-swiftlint@3.1.0 32 | with: 33 | args: --force-exclude 34 | env: 35 | DIFF_BASE: origin/${{ github.base_ref }} -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: iOS build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Apply Ruby 11 | uses: actions/setup-ruby@v1 12 | with: 13 | ruby-version: '2.x' 14 | - uses: actions/cache@v1 15 | with: 16 | path: vendor/bundle 17 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 18 | restore-keys: | 19 | ${{ runner.os }}-gems- 20 | - uses: actions/cache@v1 21 | with: 22 | path: Pods 23 | key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-pods- 26 | - name: Install and setup 27 | run: | 28 | bundle config path vendor/bundle 29 | make bootstrap 30 | - name: Run Fastlane Build 31 | run: bundle exec fastlane scan 32 | - name: Send coverage to Codecov 33 | run: bundle exec fastlane run codecov_reporter 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # https://github.com/github/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 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | *.generated.swift 26 | test_output 27 | .DS_Store 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | 33 | ## CocoaPods 34 | Pods/ 35 | 36 | ## Carthage 37 | Carthage/ 38 | 39 | ## Production 40 | icook-tv-top-shelf-image.png 41 | iCookTV/GoogleService-Info.plist 42 | keys/ 43 | gc_keys.json 44 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | swiftlint: 2 | enabled: true 3 | config_file: .swiftlint.yml 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.2 2 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.2 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - nesting 4 | - identifier_name 5 | - vertical_whitespace 6 | type_name: 7 | excluded: 8 | - iCookTVKeys 9 | - iCookTVTests 10 | identifier_name: 11 | excluded: 12 | - id 13 | - URL 14 | included: 15 | - iCookTV 16 | - iCookTVTests 17 | excluded: 18 | - Carthage 19 | - Pods 20 | - iCookTV/R.generated.swift 21 | - iCookTVTests 22 | - vendor 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Develop Branch 4 | 5 | ## v1.1.1 6 | 7 | * Update pods [#18](https://github.com/polydice/iCook-tvOS/pull/18) 8 | * Update fastlane and gems [#19](https://github.com/polydice/iCook-tvOS/pull/19) 9 | 10 | ## v1.1.0 11 | 12 | * Syntax check with SwiftLint [#3](https://github.com/polydice/iCook-tvOS/pull/3) 13 | * Code coverage 14 | * A little help from [Danger](http://danger.systems/) [#4](https://github.com/polydice/iCook-tvOS/pull/4) 15 | * Protocol extended features 16 | * Swift 3 syntax updates [#5](https://github.com/polydice/iCook-tvOS/pull/5) 17 | * Simpler project quick start [#6](https://github.com/polydice/iCook-tvOS/pull/6) 18 | * Replace Freddy with Swift Codable [#11](https://github.com/polydice/iCook-tvOS/pull/11) 19 | * Replace Quick and Nimble with XCTest [#12](https://github.com/polydice/iCook-tvOS/pull/12) 20 | * Swift 5 [#13](https://github.com/polydice/iCook-tvOS/pull/13) 21 | * Update gems and migrate to GitHub Actions [#14](https://github.com/polydice/iCook-tvOS/pull/14) 22 | * Clean up tracking [#15](https://github.com/polydice/iCook-tvOS/pull/15) 23 | * Clean up Fastlane and CI [#16](https://github.com/polydice/iCook-tvOS/pull/16) 24 | * Add ComScore SDK [#17](https://github.com/polydice/iCook-tvOS/pull/17) 25 | 26 | ## v1.0.0 27 | 28 | * Initial release written in Swift 2.2 29 | * Play high quality iCook TV videos 30 | * Blurred background based on focus items 31 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Sometimes it's a README fix, or something like that - which isn't relevant for 4 | # including in a project's CHANGELOG for example 5 | declared_trivial = (github.pr_title + github.pr_body).include? "#trivial" 6 | 7 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet 8 | warn "PR is classed as Work in Progress" if github.pr_title.include? "[WIP]" 9 | 10 | # Warn when there is a big PR 11 | warn "Big PR" if git.lines_of_code > 500 12 | 13 | # Ensure there is a summary for a PR 14 | fail "Please provide a summary in the Pull Request description" if github.pr_body.length < 5 15 | 16 | # Add a CHANGELOG entry for app changes 17 | if git.lines_of_code > 50 && !git.modified_files.include?("CHANGELOG.md") && !declared_trivial 18 | fail "Please update [CHANGELOG.md](https://github.com/polydice/iCook-tvOS/blob/develop/CHANGELOG.md).", sticky: true 19 | end 20 | 21 | # Ensure a clean commits history 22 | if git.commits.any? { |c| c.message =~ /^Merge branch/ } 23 | fail "Please rebase to get rid of the merge commits in this PR", sticky: true 24 | end 25 | -------------------------------------------------------------------------------- /Demo-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 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | FacebookAutoInitEnabled 24 | 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | DemoLaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | arm64 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "cocoapods" 4 | gem "cocoapods-keys" 5 | gem "danger" 6 | gem "fastlane" 7 | gem "fastlane-plugin-codecov_reporter", github: 'dlackty/fastlane-plugin-codecov_reporter', branch: 'update-config-item' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 bcylin 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bootstrap: 2 | gem install bundler -v 2.1.4 3 | bundle install 4 | # pod install 5 | bundle exec pod keys set BaseAPIURL "https://cdn.jsdelivr.net/gh/polydice/iCook-tvOS@gh-pages/demo/" 6 | bundle exec pod keys set FacebookAppID "APP_ID" 7 | bundle exec pod keys set ComScorePublisherID "1000001" 8 | bundle exec pod install 9 | # mock Google Services plist 10 | cp -n mock-GoogleService-Info.plist iCookTV/GoogleService-Info.plist 11 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://cdn.cocoapods.org/' 2 | 3 | platform :tvos, "10.0" 4 | use_frameworks! 5 | inhibit_all_warnings! 6 | 7 | workspace "iCookTV" 8 | project "iCookTV" 9 | 10 | target :iCookTV do 11 | pod "Alamofire", "4.8.2" 12 | pod "Firebase/Crashlytics" 13 | pod "Hue", "5.0.0" 14 | pod "Kingfisher" 15 | pod "FBSDKTVOSKit" 16 | pod "ComScore" 17 | 18 | target :iCookTVTests do 19 | pod "SwiftLint" 20 | end 21 | end 22 | 23 | 24 | plugin "cocoapods-keys", { 25 | project: "iCookTV", 26 | keys: ["BaseAPIURL", "FacebookAppID", "ComScorePublisherID"] 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.8.2) 3 | - ComScore (6.3.1): 4 | - ComScore/Dynamic (= 6.3.1) 5 | - ComScore/Dynamic (6.3.1) 6 | - FBSDKCoreKit (6.3.0): 7 | - FBSDKCoreKit/Basics (= 6.3.0) 8 | - FBSDKCoreKit/Core (= 6.3.0) 9 | - FBSDKCoreKit/Basics (6.3.0) 10 | - FBSDKCoreKit/Core (6.3.0): 11 | - FBSDKCoreKit/Basics 12 | - FBSDKLoginKit (6.3.0): 13 | - FBSDKLoginKit/Login (= 6.3.0) 14 | - FBSDKLoginKit/Login (6.3.0): 15 | - FBSDKCoreKit (~> 6.3.0) 16 | - FBSDKShareKit (6.3.0): 17 | - FBSDKShareKit/Share (= 6.3.0) 18 | - FBSDKShareKit/Share (6.3.0): 19 | - FBSDKCoreKit (~> 6.3.0) 20 | - FBSDKTVOSKit (6.3.0): 21 | - FBSDKCoreKit (~> 6.3.0) 22 | - FBSDKLoginKit (~> 6.3.0) 23 | - FBSDKShareKit (~> 6.3.0) 24 | - Firebase/CoreOnly (6.21.0): 25 | - FirebaseCore (= 6.6.5) 26 | - Firebase/Crashlytics (6.21.0): 27 | - Firebase/CoreOnly 28 | - FirebaseCrashlytics (~> 4.0.0-beta.6) 29 | - FirebaseAnalyticsInterop (1.5.0) 30 | - FirebaseCore (6.6.5): 31 | - FirebaseCoreDiagnostics (~> 1.2) 32 | - FirebaseCoreDiagnosticsInterop (~> 1.2) 33 | - GoogleUtilities/Environment (~> 6.5) 34 | - GoogleUtilities/Logger (~> 6.5) 35 | - FirebaseCoreDiagnostics (1.2.2): 36 | - FirebaseCoreDiagnosticsInterop (~> 1.2) 37 | - GoogleDataTransportCCTSupport (~> 2.0) 38 | - GoogleUtilities/Environment (~> 6.5) 39 | - GoogleUtilities/Logger (~> 6.5) 40 | - nanopb (~> 0.3.901) 41 | - FirebaseCoreDiagnosticsInterop (1.2.0) 42 | - FirebaseCrashlytics (4.0.0-beta.6): 43 | - FirebaseAnalyticsInterop (~> 1.2) 44 | - FirebaseCore (~> 6.6) 45 | - FirebaseInstallations (~> 1.1) 46 | - GoogleDataTransport (~> 5.1) 47 | - GoogleDataTransportCCTSupport (>= 2.0.1, ~> 2.0) 48 | - nanopb (~> 0.3.901) 49 | - PromisesObjC (~> 1.2) 50 | - FirebaseInstallations (1.1.1): 51 | - FirebaseCore (~> 6.6) 52 | - GoogleUtilities/UserDefaults (~> 6.5) 53 | - PromisesObjC (~> 1.2) 54 | - GoogleDataTransport (5.1.0) 55 | - GoogleDataTransportCCTSupport (2.0.1): 56 | - GoogleDataTransport (~> 5.1) 57 | - nanopb (~> 0.3.901) 58 | - GoogleUtilities/Environment (6.5.2) 59 | - GoogleUtilities/Logger (6.5.2): 60 | - GoogleUtilities/Environment 61 | - GoogleUtilities/UserDefaults (6.5.2): 62 | - GoogleUtilities/Logger 63 | - Hue (5.0.0) 64 | - Keys (1.0.1) 65 | - Kingfisher (5.14.0): 66 | - Kingfisher/Core (= 5.14.0) 67 | - Kingfisher/Core (5.14.0) 68 | - nanopb (0.3.9011): 69 | - nanopb/decode (= 0.3.9011) 70 | - nanopb/encode (= 0.3.9011) 71 | - nanopb/decode (0.3.9011) 72 | - nanopb/encode (0.3.9011) 73 | - PromisesObjC (1.2.8) 74 | - SwiftLint (0.39.2) 75 | 76 | DEPENDENCIES: 77 | - Alamofire (= 4.8.2) 78 | - ComScore 79 | - FBSDKTVOSKit 80 | - Firebase/Crashlytics 81 | - Hue (= 5.0.0) 82 | - Keys (from `Pods/CocoaPodsKeys`) 83 | - Kingfisher 84 | - SwiftLint 85 | 86 | SPEC REPOS: 87 | trunk: 88 | - Alamofire 89 | - ComScore 90 | - FBSDKCoreKit 91 | - FBSDKLoginKit 92 | - FBSDKShareKit 93 | - FBSDKTVOSKit 94 | - Firebase 95 | - FirebaseAnalyticsInterop 96 | - FirebaseCore 97 | - FirebaseCoreDiagnostics 98 | - FirebaseCoreDiagnosticsInterop 99 | - FirebaseCrashlytics 100 | - FirebaseInstallations 101 | - GoogleDataTransport 102 | - GoogleDataTransportCCTSupport 103 | - GoogleUtilities 104 | - Hue 105 | - Kingfisher 106 | - nanopb 107 | - PromisesObjC 108 | - SwiftLint 109 | 110 | EXTERNAL SOURCES: 111 | Keys: 112 | :path: Pods/CocoaPodsKeys 113 | 114 | SPEC CHECKSUMS: 115 | Alamofire: ae5c501addb7afdbb13687d7f2f722c78734c2d3 116 | ComScore: 2d6206a44c233c08b1f1891e8f138e8f80c07503 117 | FBSDKCoreKit: 5d55c8f3007c9c49b793617b9102e46355fc7e17 118 | FBSDKLoginKit: d46aa04d9bb9990a4deb6441736fae24a8c94496 119 | FBSDKShareKit: cbd309f29d00e596bc28319724a7519940e804fa 120 | FBSDKTVOSKit: f75cef4d46175dcc010278ef252edb8447bb1f3a 121 | Firebase: f378c80340dd41c0ad0914af740c021eb282a04b 122 | FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae 123 | FirebaseCore: 9f495d3afacb7b558711e6218ebb14b1c51b5802 124 | FirebaseCoreDiagnostics: e9b4cd8ba60dee0f2d13347332e4b7898cca5b61 125 | FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 126 | FirebaseCrashlytics: b9e729da8b80d9c45f234f791a73b5fe647d4c31 127 | FirebaseInstallations: acb3216eb9784d3b1d2d2d635ff74fa892cc0c44 128 | GoogleDataTransport: b29a21d813e906014ca16c00897827e40e4a24ab 129 | GoogleDataTransportCCTSupport: 6f15a89b0ca35d6fa523e1f752ef818588885988 130 | GoogleUtilities: ad0f3b691c67909d03a3327cc205222ab8f42e0e 131 | Hue: c129cb67be7d093a82bbbc30ce8a96757bf6f37a 132 | Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 133 | Kingfisher: 7b64389a43139c903ec434788344c288217c792d 134 | nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd 135 | PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6 136 | SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 137 | 138 | PODFILE CHECKSUM: ccd34ec2f02d9e98562b6280d03df6e1c2a43352 139 | 140 | COCOAPODS: 1.9.1 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iCook tvOS App 2 | 3 | ![iOS build](https://github.com/polydice/iCook-tvOS/workflows/iOS%20build/badge.svg) 4 | ![Swift 5](https://img.shields.io/badge/Swift-5-orange.svg) 5 | [![codecov.io](https://codecov.io/github/polydice/iCook-tvOS/coverage.svg?branch=develop)](https://codecov.io/github/polydice/iCook-tvOS?branch=develop) 6 | 7 | A tvOS app that plays [iCook TV](https://tv.icook.tw/) videos. 8 | 9 | 10 | 11 | ## Quick Start 12 | 13 | Run the following commands to install dependencies: 14 | 15 | ``` 16 | make bootstrap 17 | ``` 18 | 19 | ## Production Setups 20 | 21 | If you work at Polydice, instead of `make bootstrap`, set up the project step by step with the following commands. Fill in the credentials and ask admin for required files. 22 | 23 | ``` 24 | bundle install 25 | bundle exec pod install 26 | ``` 27 | 28 | #### API 29 | 30 | `pod install` will prompt for the required configuration to run the app: 31 | 32 | ``` 33 | CocoaPods-Keys has detected a keys mismatch for your setup. 34 | What is the key for BaseAPIURL 35 | > 36 | ``` 37 | 38 | > TBD: API details are hidden for now due to proprietary reasons. 39 | 40 | #### Required Keys 41 | 42 | Managed by [CocoaPods-Keys](https://github.com/orta/cocoapods-keys): 43 | 44 | * BaseAPIURL 45 | * FacebookAppID 46 | 47 | 48 | #### Required Files 49 | 50 | * Required by the Firebase SDK for the `Release` configuration: 51 | 52 | ``` 53 | iCookTV/GoogleService-Info.plist 54 | ``` 55 | 56 | ## Demo 57 | 58 | * Download the tvOS app from [App Store](https://itunes.apple.com/tw/app/ai-liao-li/id554065086). 59 | 60 | ## Contact 61 | 62 | [![Twitter](https://img.shields.io/badge/twitter-@polydice-blue.svg?style=flat)](https://twitter.com/polydice) 63 | 64 | ## License 65 | 66 | The names and icons for iCook are trademarks of [Polydice, Inc.](https://polydice.com/) Please refer to the guidelines at [iCook Newsroom](https://newsroom.icook.tw/downloads). 67 | 68 | * All image assets are Copyright © 2016 Polydice, Inc. All rights reserved. 69 | * The source code is released under the MIT license. See [LICENSE](https://github.com/bcylin/Try-tvOS/blob/master/LICENSE) for more info. 70 | -------------------------------------------------------------------------------- /fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | storage_mode("google_cloud") 2 | 3 | google_cloud_bucket_name("icook-tvos-certificates") 4 | 5 | app_identifier ["com.thepolydice.icook"] 6 | platform 'tvos' 7 | 8 | username "bot@polydice.com" 9 | 10 | # For all available options run `match --help` 11 | -------------------------------------------------------------------------------- /fastlane/Scanfile: -------------------------------------------------------------------------------- 1 | # For more information about this configuration visit 2 | # https://github.com/fastlane/scan#scanfile 3 | 4 | # In general, you can use the options available 5 | # scan --help 6 | 7 | workspace "iCookTV.xcworkspace" 8 | scheme "iCookTV" 9 | clean true 10 | skip_build true 11 | code_coverage true 12 | output_types "junit" 13 | -------------------------------------------------------------------------------- /iCookTV.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iCookTV.xcodeproj/xcshareddata/xcschemes/iCookTV Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /iCookTV.xcodeproj/xcshareddata/xcschemes/iCookTV.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 83 | 84 | 85 | 91 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /iCookTV.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /iCookTV.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iCookTV/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 16/09/2015. 6 | // Copyright © 2015 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | @UIApplicationMain 30 | class AppDelegate: UIResponder, UIApplicationDelegate { 31 | 32 | var window: UIWindow? 33 | let tabBarController = UITabBarController() 34 | private var backgroundTask = UIBackgroundTaskIdentifier.invalid 35 | 36 | // MARK: - UIApplicationDelegate 37 | 38 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 39 | GroundControl.sync() 40 | Tracker.setUpAnalytics() 41 | 42 | window = UIWindow(frame: UIScreen.main.bounds) 43 | window?.rootViewController = TrackableNavigationController(rootViewController: LaunchViewController()) 44 | window?.makeKeyAndVisible() 45 | 46 | return true 47 | } 48 | 49 | func applicationWillResignActive(_ application: UIApplication) { 50 | #if DEMO 51 | // Do not store any histroy for demo version 52 | HistoryManager.deleteCache() 53 | exit(1) 54 | #endif 55 | } 56 | 57 | // MARK: - Private Methods 58 | 59 | private func endBackgroundTask(inApplication application: UIApplication) { 60 | application.endBackgroundTask(backgroundTask) 61 | backgroundTask = UIBackgroundTaskIdentifier.invalid 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tvOS-large-icon-3.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/icook-tvOS-large-icon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/icook-tvOS-large-icon-3.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tvOS-large-icon-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/icook-tvOS-large-icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/icook-tvOS-large-icon-1.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tvOS-large-icon-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/icook-tvOS-large-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/icook-tvOS-large-icon-2.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tvOS-small-icon-3.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "icook-tvOS-small-icon-3@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/icook-tvOS-small-icon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/icook-tvOS-small-icon-3.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/icook-tvOS-small-icon-3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/icook-tvOS-small-icon-3@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tvOS-small-icon-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "icook-tvOS-small-icon-1@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/icook-tvOS-small-icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/icook-tvOS-small-icon-1.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/icook-tvOS-small-icon-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/icook-tvOS-small-icon-1@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tvOS-small-icon-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "icook-tvOS-small-icon-2@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/icook-tvOS-small-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/icook-tvOS-small-icon-2.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/icook-tvOS-small-icon-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/icook-tvOS-small-icon-2@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - Large.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon - Small.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "Top Shelf Wide_2320x720.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "Top Shelf Wide_2320x720@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide_2320x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide_2320x720.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide_2320x720@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Top Shelf Wide_2320x720@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "Top Shelf_1920x720.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "Top Shelf_1920x720@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf_1920x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf_1920x720.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf_1920x720@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Top Shelf_1920x720@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/DemoLaunchScreenImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchScreen3_Demo.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchScreen3_Demo@2x.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | }, 18 | "properties" : { 19 | "localizable" : true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/DemoLaunchScreenImage.imageset/LaunchScreen3_Demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/DemoLaunchScreenImage.imageset/LaunchScreen3_Demo.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/DemoLaunchScreenImage.imageset/LaunchScreen3_Demo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/DemoLaunchScreenImage.imageset/LaunchScreen3_Demo@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/LaunchScreenImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchScreen3.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchScreen3@2x.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | }, 18 | "properties" : { 19 | "localizable" : true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/LaunchScreenImage.imageset/LaunchScreen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/LaunchScreenImage.imageset/LaunchScreen3.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/LaunchScreenImage.imageset/LaunchScreen3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/LaunchScreenImage.imageset/LaunchScreen3@2x.png -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-banner-food-back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tv-banner-food-back.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-banner-food-back.imageset/icook-tv-banner-food-back.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/icook-tv-banner-food-back.imageset/icook-tv-banner-food-back.pdf -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-banner-food-front.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tv-banner-food-front.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-banner-food-front.imageset/icook-tv-banner-food-front.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/icook-tv-banner-food-front.imageset/icook-tv-banner-food-front.pdf -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tv-cat.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-cat.imageset/icook-tv-cat.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/icook-tv-cat.imageset/icook-tv-cat.pdf -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "icook-tv-logo.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /iCookTV/Assets.xcassets/icook-tv-logo.imageset/icook-tv-logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/iCook-tvOS/fef935cc289405bffb5f65838f6f52f7a5f97136/iCookTV/Assets.xcassets/icook-tv-logo.imageset/icook-tv-logo.pdf -------------------------------------------------------------------------------- /iCookTV/Base.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | "CFBundleDisplayName" = "iCook"; 4 | -------------------------------------------------------------------------------- /iCookTV/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iCookTV 4 | 5 | Created by Ben on 09/04/2016. 6 | Copyright © 2016 Polydice, Inc. All rights reserved. 7 | */ 8 | 9 | "icook-tv" = "iCook TV"; 10 | 11 | "history" = "History"; 12 | 13 | "home" = "Home"; 14 | 15 | "retry" = "Retry"; 16 | 17 | "ok" = "OK"; 18 | 19 | "launch-screen-upper-tagline" = "icook.tw"; 20 | 21 | "launch-screen-lower-tagline" = ""; 22 | 23 | "no-history-found" = "You haven't watched any video."; 24 | 25 | "no-video-found" = "No video found."; 26 | 27 | "error-title" = "Error\n"; 28 | 29 | "video-error" = "There's something wrong with this video."; 30 | 31 | "contact-info" = "Please contact us at hi@icook.tw if this issue keeps happening."; 32 | -------------------------------------------------------------------------------- /iCookTV/Controllers/CategoriesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoriesViewController.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 22/03/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class CategoriesViewController: UIViewController, 30 | UICollectionViewDelegate, 31 | UICollectionViewDelegateFlowLayout, 32 | BlurBackgroundPresentable, 33 | Trackable { 34 | 35 | private var dataSource: CategoriesDataSource { 36 | didSet { 37 | collectionView.reloadData() 38 | } 39 | } 40 | 41 | private lazy var titleView: MainMenuView = { 42 | let _menu = MainMenuView() 43 | _menu.button.setTitle(NSLocalizedString("history", comment: ""), for: .normal) 44 | _menu.button.addTarget(self, action: .showHistory, for: .primaryActionTriggered) 45 | return _menu 46 | }() 47 | 48 | private(set) lazy var collectionView: UICollectionView = { 49 | let _collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: Metrics.showcaseLayout) 50 | _collectionView.register(cell: CategoryCell.self) 51 | _collectionView.remembersLastFocusedIndexPath = true 52 | _collectionView.dataSource = self.dataSource 53 | _collectionView.delegate = self 54 | return _collectionView 55 | }() 56 | 57 | // MARK: - BlurBackgroundPresentable 58 | 59 | let backgroundImageView = UIImageView() 60 | 61 | // MARK: - Initialization 62 | 63 | init(categories: [Category]) { 64 | dataSource = CategoriesDataSource(categories: categories) 65 | super.init(nibName: nil, bundle: nil) 66 | } 67 | 68 | required init?(coder aDecoder: NSCoder) { 69 | dataSource = CategoriesDataSource(categories: []) 70 | super.init(coder: aDecoder) 71 | } 72 | 73 | // MARK: - UIViewController 74 | 75 | override func loadView() { 76 | super.loadView() 77 | setUpBlurBackground() 78 | navigationItem.titleView = UIView() 79 | 80 | let divided = view.bounds.divided(atDistance: 800, from: .maxYEdge) 81 | titleView.frame = divided.remainder 82 | titleView.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin] 83 | collectionView.frame = divided.slice 84 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] 85 | 86 | view.addSubview(titleView) 87 | view.addSubview(collectionView) 88 | } 89 | 90 | override func viewDidLoad() { 91 | super.viewDidLoad() 92 | NotificationCenter.default.addObserver( 93 | self, 94 | selector: .handleCreatedCover, 95 | name: NSNotification.Name(rawValue: CoverBuilder.DidCreateCoverNotification), 96 | object: nil 97 | ) 98 | } 99 | 100 | // MARK: - UIFocusEnvironment 101 | 102 | override var preferredFocusedView: UIView? { 103 | return collectionView 104 | } 105 | 106 | // MARK: - UICollectionViewDelegate 107 | 108 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 109 | let category = dataSource[indexPath.row] 110 | let controller = VideosViewController(categoryID: category.id, title: category.name) 111 | navigationController?.pushViewController(controller, animated: true) 112 | } 113 | 114 | func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 115 | if let cell = context.nextFocusedView as? CategoryCell, let cover = cell.imageView.image, cell.hasDisplayedCover { 116 | animateBackgroundTransition(to: cover) 117 | } 118 | } 119 | 120 | // MARK: - Trackable 121 | 122 | var pageView: PageView? { 123 | return PageView(name: "Categories") 124 | } 125 | 126 | // MARK: - NSNotification Callbacks 127 | 128 | @objc fileprivate func handleCreatedCover(_ notification: Notification) { 129 | // Update the background image when the first mosaic cover is created. 130 | if let cover = notification.userInfo?[CoverBuilder.NotificationUserInfoCoverKey] as? UIImage { 131 | DispatchQueue.main.async { 132 | self.animateBackgroundTransition(to: cover) 133 | } 134 | } 135 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: CoverBuilder.DidCreateCoverNotification), object: nil) 136 | } 137 | 138 | // MARK: - UIResponder Callbacks 139 | 140 | @objc fileprivate func showHistory(_ sender: UIButton) { 141 | navigationController?.pushViewController(HistoryViewController(), animated: true) 142 | } 143 | 144 | @objc fileprivate func updateBackground(with image: UIImage) { 145 | animateBackgroundTransition(to: image) 146 | } 147 | 148 | } 149 | 150 | 151 | //////////////////////////////////////////////////////////////////////////////// 152 | 153 | 154 | private extension Selector { 155 | static let handleCreatedCover = #selector(CategoriesViewController.handleCreatedCover(_:)) 156 | static let showHistory = #selector(CategoriesViewController.showHistory(_:)) 157 | static let updateBackground = #selector(CategoriesViewController.updateBackground(with:)) 158 | } 159 | -------------------------------------------------------------------------------- /iCookTV/Controllers/HistoryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryViewController.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 21/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class HistoryViewController: UIViewController, 30 | UICollectionViewDelegate, 31 | UICollectionViewDelegateFlowLayout, 32 | BlurBackgroundPresentable, 33 | DropdownMenuPresentable, 34 | LoadingIndicatorPresentable, 35 | OverlayViewPresentable, 36 | VideosGridLayout { 37 | 38 | private let dataSource = VideosDataSource() 39 | 40 | // MARK: - BlurBackgroundPresentable 41 | 42 | let backgroundImageView = UIImageView() 43 | 44 | // MARK: - DropdownMenuPresentable 45 | 46 | private(set) lazy var dropdownMenuView: MenuView = type(of: self).defaultMenuView() 47 | 48 | // MARK: - LoadingIndicatorPresentable 49 | 50 | private(set) lazy var loadingIndicator: UIActivityIndicatorView = type(of: self).defaultLoadingIndicator() 51 | 52 | // MARK: - VideosGridLayout 53 | 54 | private(set) lazy var collectionView: UICollectionView = type(of: self).defaultCollectionView(dataSource: self.dataSource, delegate: self) 55 | 56 | // MARK: - UIViewController 57 | 58 | override var title: String? { 59 | get { 60 | return NSLocalizedString("history", comment: "") 61 | } 62 | set {} // swiftlint:disable:this unused_setter_value 63 | } 64 | 65 | override func loadView() { 66 | super.loadView() 67 | setUpBlurBackground() 68 | setUpCollectionView() 69 | setUpDropdownMenuView() 70 | dropdownMenuView.button.setTitle(NSLocalizedString("home", comment: ""), for: .normal) 71 | dropdownMenuView.button.addTarget(self, action: .backToHome, for: .primaryActionTriggered) 72 | } 73 | 74 | override func viewDidLoad() { 75 | super.viewDidLoad() 76 | setOverlayViewHidden(false, animated: false) 77 | isLoading = true 78 | DispatchQueue.global().async { 79 | do { 80 | let decoder = JSONDecoder() 81 | let history = try HistoryManager.history.map { try decoder.decode(Video.self, from: $0) } 82 | DispatchQueue.main.sync { 83 | self.dataSource.append(history, toCollectionView: self.collectionView) 84 | self.setOverlayViewHidden(self.dataSource.numberOfItems > 0, animated: true) 85 | self.isLoading = false 86 | } 87 | } catch { 88 | Tracker.track(error) 89 | // Remove the malformed cache. 90 | HistoryManager.deleteCache { _ in 91 | self.isLoading = false 92 | } 93 | } 94 | Tracker.track(PageView(name: "history", details: [ 95 | TrackableKey.numberOfItems: NSNumber(value: self.dataSource.numberOfItems) 96 | ])) 97 | } 98 | } 99 | 100 | override func viewWillAppear(_ animated: Bool) { 101 | super.viewWillAppear(animated) 102 | if dataSource.numberOfItems > 0 { 103 | collectionView.reloadData() 104 | } 105 | } 106 | 107 | // MARK: - UIFocusEnvironment 108 | 109 | override var preferredFocusedView: UIView? { 110 | return collectionView 111 | } 112 | 113 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 114 | animateDropdownMenuView(in: context, with: coordinator) 115 | } 116 | 117 | // MARK: - UICollectionViewDelegate 118 | 119 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 120 | // Reorder current displayed contents after the video player is presented. 121 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0.5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { 122 | self.dataSource.moveItem(atIndexPathToTop: indexPath, inCollectionView: collectionView) 123 | } 124 | } 125 | 126 | // MARK: - OverlayViewPresentable 127 | 128 | private(set) lazy var overlayView: UIView = { 129 | let _empty = EmptyStateView() 130 | _empty.textLabel.text = NSLocalizedString("no-history-found", comment: "") 131 | return _empty 132 | }() 133 | 134 | func containerViewForOverlayView(_ overlayView: UIView) -> UIView { 135 | return view 136 | } 137 | 138 | func constraintsForOverlayView(_ overlayView: UIView) -> [NSLayoutConstraint] { 139 | return [ 140 | overlayView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 141 | overlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 142 | ] 143 | } 144 | 145 | func updateBackground(with image: UIImage?) { 146 | animateBackgroundTransition(to: image) 147 | } 148 | 149 | // MARK: - UIResponder Callbacks 150 | 151 | @objc fileprivate func backToHome(_ sender: UIButton) { 152 | _ = navigationController?.popToRootViewController(animated: true) 153 | } 154 | 155 | } 156 | 157 | 158 | //////////////////////////////////////////////////////////////////////////////// 159 | 160 | 161 | private extension Selector { 162 | static let backToHome = #selector(HistoryViewController.backToHome(_:)) 163 | } 164 | -------------------------------------------------------------------------------- /iCookTV/Controllers/TrackableNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrackableNavigationController.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 02/05/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class TrackableNavigationController: UINavigationController { 30 | 31 | // MARK: - UIViewController 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | setNavigationBarHidden(true, animated: false) 36 | 37 | // Track the root view controller. 38 | for controller in viewControllers { 39 | track(controller as? Trackable) 40 | } 41 | } 42 | 43 | // MARK: - UINavigationController 44 | 45 | override func pushViewController(_ viewController: UIViewController, animated: Bool) { 46 | super.pushViewController(viewController, animated: animated) 47 | track(viewController as? Trackable) 48 | } 49 | 50 | override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { 51 | super.setViewControllers(viewControllers, animated: animated) 52 | for controller in viewControllers { 53 | track(controller as? Trackable) 54 | } 55 | } 56 | 57 | // MARK: - Private Methods 58 | 59 | private func track(_ navigation: Trackable?) { 60 | guard let pageView = navigation?.pageView else { 61 | return 62 | } 63 | Tracker.track(pageView) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /iCookTV/Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 21/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | struct Debug { 31 | 32 | private static let dateFormatter: DateFormatter = { 33 | let _formatter = DateFormatter() 34 | _formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" 35 | return _formatter 36 | }() 37 | 38 | static func print(_ items: Any..., separator: String = " ", terminator: String = "\n", file: String = #file, function: String = #function, line: Int = #line) { 39 | #if DEBUG 40 | let prefix = dateFormatter.string(from: Date()) + " \(file.typeName).\(function):[\(line)]" 41 | let content = items.map { "\($0)" } .joined(separator: separator) 42 | Swift.print("\(prefix) \(content)", terminator: terminator) 43 | #endif 44 | } 45 | 46 | } 47 | 48 | 49 | //////////////////////////////////////////////////////////////////////////////// 50 | 51 | 52 | extension String { 53 | 54 | var typeName: String { 55 | return lastPathComponent.stringByDeletingPathExtension 56 | } 57 | 58 | private var lastPathComponent: String { 59 | return (self as NSString).lastPathComponent 60 | } 61 | 62 | private var stringByDeletingPathExtension: String { 63 | return (self as NSString).deletingPathExtension 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /iCookTV/DemoLaunchScreen.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 | -------------------------------------------------------------------------------- /iCookTV/Extensions/CGRect+Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+Grid.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 04/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | enum Grid: Int, Equatable { 30 | case topLeft, topRight, bottomLeft, bottomRight 31 | 32 | static let numberOfGrids: Int = { 33 | var count = 0 34 | while let _ = Grid(rawValue: count) { count += 1 } 35 | return count 36 | }() 37 | 38 | static func == (lhs: Grid, rhs: Grid) -> Bool { 39 | return lhs.rawValue == rhs.rawValue 40 | } 41 | 42 | } 43 | 44 | extension CGRect { 45 | 46 | func rect(with size: CGSize, in grid: Grid) -> CGRect { 47 | let target = CGSize(width: min(width, size.width), height: min(height, size.height)) 48 | switch grid { 49 | case .topLeft: 50 | return divided(atDistance: target.height, from: .maxYEdge).remainder.divided(atDistance: target.width, from: .maxXEdge).remainder 51 | case .topRight: 52 | return divided(atDistance: target.height, from: .maxYEdge).remainder.divided(atDistance: target.width, from: .maxXEdge).slice 53 | case .bottomLeft: 54 | return divided(atDistance: target.height, from: .maxYEdge).slice.divided(atDistance: target.width, from: .maxXEdge).remainder 55 | case .bottomRight: 56 | return divided(atDistance: target.height, from: .maxYEdge).slice.divided(atDistance: target.width, from: .maxXEdge).slice 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /iCookTV/Extensions/DataRequest+Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataRequest+Result.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 11/15/16. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import Alamofire 29 | 30 | enum Result { 31 | case success(T) 32 | case failure(Error) 33 | 34 | func mapSuccess(_ transform: (T) -> U) -> Result { 35 | switch self { 36 | case .success(let value): return .success(transform(value)) 37 | case .failure(let error): return .failure(error) 38 | } 39 | } 40 | 41 | func mapError(_ transform: (Error) -> Void) -> Result { 42 | switch self { 43 | case .success: break 44 | case .failure(let error): transform(error) 45 | } 46 | return self 47 | } 48 | 49 | } 50 | 51 | 52 | //////////////////////////////////////////////////////////////////////////////// 53 | 54 | 55 | extension DataRequest { 56 | 57 | func responseResult( 58 | queue: DispatchQueue? = nil, 59 | options: JSONSerialization.ReadingOptions = .allowFragments, 60 | completion: @escaping (Result) -> Void 61 | ) -> Self { 62 | let serializer = DataRequest.jsonResponseSerializer(options: options) 63 | 64 | return response(queue: queue, responseSerializer: serializer) { response in 65 | guard let data = response.data else { 66 | let error = response.result.error ?? NSError(domain: "io.github.bcylin.try-tvos", code: NSURLErrorUnknown, userInfo: nil) 67 | completion(.failure(error)) 68 | return 69 | } 70 | completion(.success(data)) 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /iCookTV/Extensions/UIColor+TV.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+TV.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 22/03/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import Hue 29 | 30 | extension UIColor { 31 | 32 | enum Palette { 33 | static let White = UIColor.white 34 | static let LightGray = UIColor(hex: "#EFEDE8") 35 | static let GreyishBrown = UIColor(hex: "#564E4A") 36 | 37 | enum Button { 38 | static let TitleColor = White 39 | static let BackgroundColor = GreyishBrown.withAlphaComponent(0.6) 40 | } 41 | 42 | enum FocusedButton { 43 | static let TitleColor = GreyishBrown 44 | static let BackgroundColor = White 45 | } 46 | } 47 | 48 | class func tvTaglineColor() -> UIColor { 49 | return Palette.GreyishBrown 50 | } 51 | 52 | class func tvTextColor() -> UIColor { 53 | return Palette.GreyishBrown.withAlphaComponent(0.8) 54 | } 55 | 56 | class func tvFocusedTextColor() -> UIColor { 57 | return Palette.GreyishBrown 58 | } 59 | 60 | class func tvHeaderTitleColor() -> UIColor { 61 | return Palette.GreyishBrown 62 | } 63 | 64 | class func tvBackgroundColor() -> UIColor { 65 | return Palette.LightGray 66 | } 67 | 68 | class func tvMenuBarColor() -> UIColor { 69 | return Palette.LightGray 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /iCookTV/Extensions/UIFont+TV.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+TV.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 05/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | extension UIFont { 30 | 31 | private enum FontFamily { 32 | static let PingFangTCRegular = "PingFangTC-Regular" 33 | static let PingFangTCMedium = "PingFangTC-Medium" 34 | } 35 | 36 | // MARK: - Private Methods 37 | 38 | private class func tvFont(ofSize fontSize: CGFloat) -> UIFont { 39 | return UIFont(name: FontFamily.PingFangTCRegular, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize) 40 | } 41 | 42 | private class func tvBoldFont(ofSize fontSize: CGFloat) -> UIFont { 43 | return UIFont(name: FontFamily.PingFangTCMedium, size: fontSize) ?? UIFont.boldSystemFont(ofSize: fontSize) 44 | } 45 | 46 | // MARK: - Public Methods 47 | 48 | class func tvFontForTagline() -> UIFont { 49 | return UIFont.tvFont(ofSize: 44) 50 | } 51 | 52 | class func tvFontForCategoryCell() -> UIFont { 53 | return UIFont.tvFont(ofSize: 35) 54 | } 55 | 56 | class func tvFontForFocusedCategoryCell() -> UIFont { 57 | return UIFont.tvFont(ofSize: 40) 58 | } 59 | 60 | class func tvFontForVideoCell() -> UIFont { 61 | return UIFont.tvFont(ofSize: 29) 62 | } 63 | 64 | class func tvFontForFocusedVideoCell() -> UIFont { 65 | return UIFont.tvBoldFont(ofSize: 29) 66 | } 67 | 68 | class func tvFontForVideoLength() -> UIFont { 69 | return UIFont.systemFont(ofSize: 20) 70 | } 71 | 72 | class func tvFontForLogo() -> UIFont { 73 | return UIFont.tvFont(ofSize: 65) 74 | } 75 | 76 | class func tvFontForMenuButton() -> UIFont { 77 | return UIFont.tvFont(ofSize: 30) 78 | } 79 | 80 | class func tvFontForHeaderTitle() -> UIFont { 81 | return UIFont.tvFont(ofSize: 35) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /iCookTV/Extensions/UIImage+Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Grid.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 28/03/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | extension UIImage { 30 | 31 | func image(byReplacingImage image: UIImage, in grid: Grid) -> UIImage? { 32 | UIGraphicsBeginImageContextWithOptions(size, true, 0) 33 | 34 | let canvas = CGRect(origin: CGPoint.zero, size: size) 35 | self.draw(in: canvas) 36 | 37 | let rect = canvas.rect(with: image.size, in: grid) 38 | image.draw(in: rect) 39 | 40 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 41 | UIGraphicsEndImageContext() 42 | 43 | return newImage 44 | } 45 | 46 | class func placeholderImage(with size: CGSize) -> UIImage? { 47 | let layer = CAGradientLayer() 48 | layer.frame = CGRect(origin: CGPoint.zero, size: size) 49 | layer.colors = [UIColor.white.cgColor, UIColor.Palette.LightGray.cgColor] 50 | layer.startPoint = CGPoint(x: 0, y: 0) 51 | layer.endPoint = CGPoint(x: 1, y: 1) 52 | 53 | UIGraphicsBeginImageContext(size) 54 | 55 | guard let context = UIGraphicsGetCurrentContext() else { 56 | return nil 57 | } 58 | 59 | layer.render(in: context) 60 | let image = UIGraphicsGetImageFromCurrentImageContext() 61 | UIGraphicsEndImageContext() 62 | 63 | return image 64 | } 65 | 66 | class func resizableImage(filledWith color: UIColor) -> UIImage? { 67 | UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), true, 0) 68 | 69 | color.setFill() 70 | UIRectFill(CGRect(x: 0, y: 0, width: 1, height: 1)) 71 | 72 | let image = UIGraphicsGetImageFromCurrentImageContext() 73 | UIGraphicsEndImageContext() 74 | 75 | return image?.resizableImage(withCapInsets: UIEdgeInsets.zero) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /iCookTV/Extensions/UIViewController+Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Alert.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 12/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | extension UIViewController { 30 | 31 | func showAlert(_ error: Error?, retry: (() -> Void)? = nil) { 32 | Tracker.track(error) 33 | 34 | let alert = UIAlertController( 35 | title: NSLocalizedString("error-title", comment: ""), 36 | message: NSLocalizedString("contact-info", comment: ""), 37 | preferredStyle: .alert 38 | ) 39 | alert.addAction(UIAlertAction(title: NSLocalizedString("retry", comment: ""), style: .default) { _ in 40 | retry?() 41 | }) 42 | present(alert, animated: true, completion: nil) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /iCookTV/Extensions/Video+PlayerItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Video+PlayerItem.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 30/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import AVKit 28 | import Foundation 29 | 30 | extension Video { 31 | 32 | // MARK: - Private Helpers 33 | 34 | private var titleMetaData: AVMetadataItem { 35 | let _title = AVMutableMetadataItem() 36 | _title.key = AVMetadataKey.commonKeyTitle as (NSCopying & NSObjectProtocol)? 37 | _title.keySpace = .common 38 | _title.locale = Locale.current 39 | _title.value = title as (NSCopying & NSObjectProtocol)? 40 | return _title 41 | } 42 | 43 | private var descriptionMetaData: AVMetadataItem { 44 | let _description = AVMutableMetadataItem() 45 | _description.key = AVMetadataKey.commonKeyDescription as (NSCopying & NSObjectProtocol)? 46 | _description.keySpace = .common 47 | _description.locale = Locale.current 48 | _description.value = (description ?? "") 49 | .components(separatedBy: CharacterSet.newlines) 50 | .joined(separator: "") as (NSCopying & NSObjectProtocol)? 51 | return _description 52 | } 53 | 54 | // MARK: - Public Methods 55 | 56 | func convertToPlayerItemWithCover(_ image: UIImage?, completion: @escaping (AVPlayerItem?) -> Void) { 57 | DispatchQueue.global().async { 58 | guard let source = self.source, let url = URL(string: source) else { 59 | DispatchQueue.main.async { 60 | completion(nil) 61 | } 62 | return 63 | } 64 | 65 | let playerItem = AVPlayerItem(url: url) 66 | var metadata = [ 67 | self.titleMetaData, 68 | self.descriptionMetaData 69 | ] 70 | if let cover = image { 71 | metadata.append(cover.metadataItem) 72 | } 73 | playerItem.externalMetadata = metadata 74 | 75 | DispatchQueue.main.async { 76 | completion(playerItem) 77 | } 78 | } 79 | } 80 | 81 | } 82 | 83 | 84 | //////////////////////////////////////////////////////////////////////////////// 85 | 86 | 87 | private extension UIImage { 88 | 89 | static let JPEGLeastCompressionQuality = CGFloat(1) 90 | 91 | var metadataItem: AVMetadataItem { 92 | let _item = AVMutableMetadataItem() 93 | _item.key = AVMetadataKey.commonKeyArtwork as (NSCopying & NSObjectProtocol)? 94 | _item.keySpace = .common 95 | _item.locale = Locale.current 96 | _item.value = jpegData(compressionQuality: UIImage.JPEGLeastCompressionQuality) as (NSCopying & NSObjectProtocol)? 97 | return _item 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /iCookTV/Helpers/CoverBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoverBuilder.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 09/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class CoverBuilder { 30 | 31 | static let DidCreateCoverNotification = "CoverBuilderDidCreateCoverNotification" 32 | static let NotificationUserInfoCoverKey = "CoverBuilderNotificationUserInfoCoverKey" 33 | 34 | private lazy var operationQueue: OperationQueue = { 35 | let _queue = OperationQueue() 36 | _queue.maxConcurrentOperationCount = 1 37 | return _queue 38 | }() 39 | 40 | private(set) var cover: UIImage? 41 | 42 | private static let imageCache = NSCache() 43 | 44 | private var filledGrids = Set() 45 | 46 | // MARK: - Private Methods 47 | 48 | private func cacheImage(_ image: UIImage, forKey key: String) { 49 | type(of: self).imageCache.setObject(image, forKey: key as NSString) 50 | 51 | NotificationCenter.default.post( 52 | name: Notification.Name(rawValue: type(of: self).DidCreateCoverNotification), 53 | object: self, 54 | userInfo: [type(of: self).NotificationUserInfoCoverKey: image] 55 | ) 56 | } 57 | 58 | // MARK: - Public Methods 59 | 60 | func add(image: UIImage, to grid: Grid, categoryID id: String? = nil, completion: @escaping (_ newCover: UIImage?) -> Void) { 61 | operationQueue.addOperation(BlockOperation { [weak self] in 62 | let imageSize = CGSize(width: image.size.width * 2, height: image.size.height * 2) 63 | var canvas: UIImage? 64 | 65 | if let currentImage = self?.cover, currentImage.size == imageSize { 66 | canvas = currentImage 67 | } else { 68 | canvas = UIImage.placeholderImage(with: imageSize) 69 | } 70 | 71 | let cover = canvas?.image(byReplacingImage: image, in: grid) 72 | self?.cover = cover 73 | self?.filledGrids.insert(grid) 74 | 75 | if let key = id, let image = cover, self?.filledGrids.count == Grid.numberOfGrids { 76 | self?.cacheImage(image, forKey: key) 77 | } 78 | 79 | DispatchQueue.main.sync { 80 | completion(cover) 81 | } 82 | }) 83 | } 84 | 85 | func coverForCategory(withID id: String) -> UIImage? { 86 | return type(of: self).imageCache.object(forKey: id as NSString) 87 | } 88 | 89 | func resetCover() { 90 | cover = nil 91 | filledGrids.removeAll() 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /iCookTV/Helpers/GroundControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroundControl.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 28/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import Alamofire 29 | 30 | struct GroundControl { 31 | 32 | enum VideoSource { 33 | case hls, youTube 34 | } 35 | 36 | /// Returns the URL of default background image URL. 37 | static var defaultBackgroundURL: URL? { 38 | if let url = UserDefaults.standard.string(forKey: Keys.DefaultBackgroundURL) { 39 | return URL(string: url) 40 | } else { 41 | return nil 42 | } 43 | } 44 | 45 | /// Returns the preferred video source. 46 | static var videoSource: VideoSource { 47 | if UserDefaults.standard.string(forKey: Keys.VideoSource) == "youtube" { 48 | return .youTube 49 | } else { 50 | return .hls 51 | } 52 | } 53 | 54 | // MARK: - Private Constants 55 | 56 | private struct Keys { 57 | static let DefaultBackgroundURL = "default-background-url" 58 | static let VideoSource = "video-source" 59 | } 60 | 61 | private static let groundControlURL = "https://polydice.com/iCook-tvOS/ground-control.json" 62 | 63 | // MARK: - Public Methods 64 | 65 | static func sync() { 66 | Alamofire.request(groundControlURL, method: .get).responseJSON { response in 67 | guard let results = response.result.value as? NSDictionary, response.result.error == nil else { 68 | return 69 | } 70 | Debug.print(results) 71 | for key in [Keys.DefaultBackgroundURL, Keys.VideoSource] { 72 | UserDefaults.standard.set(results[key], forKey: key) 73 | } 74 | UserDefaults.standard.synchronize() 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /iCookTV/Helpers/HistoryManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryManager.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 20/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | struct HistoryManager { 30 | 31 | // MARK: - Private Properties 32 | 33 | private static var cache: URL? { 34 | guard let 35 | directory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first, 36 | let url = URL(string: directory) 37 | else { 38 | return nil 39 | } 40 | return url.appendingPathComponent("history.dat") 41 | } 42 | 43 | private static let savingQueue = DispatchQueue(label: "io.github.bcylin.savingQueue", attributes: []) 44 | 45 | // MARK: - Public Methods 46 | 47 | /// Returns the deserialized video history read from the cache directory. 48 | static var history: [Data] { 49 | if let path = cache?.path, let records = NSArray(contentsOfFile: path) as? [Data] { 50 | return records 51 | } else { 52 | return [] 53 | } 54 | } 55 | 56 | /** 57 | Converts the video to JSON and saves an array of serialized video history to the cache directory in the background. 58 | 59 | - parameter video: A video object. 60 | */ 61 | static func save(video: Video) { 62 | savingQueue.async { 63 | guard let path = self.cache?.path else { 64 | return 65 | } 66 | Debug.print(path) 67 | 68 | let decoder = JSONDecoder() 69 | var records: [Video] = history.compactMap { try? decoder.decode(Video.self, from: $0) } 70 | 71 | // Keep the latest video at top. 72 | records = records.filter { $0.id != video.id } 73 | records.insert(video, at: 0) 74 | Debug.print("records.count =", records.count) 75 | 76 | do { 77 | let encoder = JSONEncoder() 78 | let data = try records.map { try encoder.encode($0) } as NSArray 79 | data.write(toFile: path, atomically: true) 80 | } catch { 81 | Tracker.track(error) 82 | } 83 | } 84 | } 85 | 86 | /** 87 | Deletes the video history in the background. 88 | 89 | - parameter completion: A completion block that's called in the main thread when the action finishes. 90 | */ 91 | static func deleteCache(_ completion: ((_ success: Bool) -> Void)? = nil) { 92 | if let path = cache?.path { 93 | savingQueue.async { 94 | do { 95 | try FileManager.default.removeItem(atPath: path) 96 | DispatchQueue.main.async { 97 | completion?(true) 98 | } 99 | } catch { 100 | Tracker.track(error) 101 | DispatchQueue.main.async { 102 | completion?(false) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /iCookTV/Helpers/KeyPathDecoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPathDecoding.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 10/08/2019. 6 | // Copyright © 2019 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// A type for decoding nested data structure. 30 | struct DataKeyPathDecoding: Decodable { 31 | 32 | let data: T 33 | let links: Links? 34 | 35 | struct Links: Decodable { 36 | let next: String? 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iCookTV/Helpers/Tracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tracker.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 28/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import ComScore 28 | import FBSDKTVOSKit 29 | import Firebase 30 | import Foundation 31 | 32 | enum Tracker { 33 | static func setUpAnalytics() { 34 | Settings.isAutoInitEnabled = false 35 | #if TRACKING 36 | Settings.appID = iCookTVKeys.FacebookAppID 37 | ApplicationDelegate.initializeSDK(nil) 38 | FirebaseApp.configure() 39 | let comScoreConfiguration = SCORPublisherConfiguration { (configurationBuilder) in 40 | configurationBuilder?.publisherId = iCookTVKeys.ComScorePublisherID 41 | } 42 | SCORAnalytics.configuration().addClient(with: comScoreConfiguration) 43 | SCORAnalytics.start() 44 | #endif 45 | } 46 | 47 | static func track(_ pageView: PageView) { 48 | DispatchQueue.global().async { 49 | Debug.print(pageView) 50 | #if TRACKING 51 | 52 | #endif 53 | } 54 | } 55 | 56 | static func track(_ event: Event) { 57 | DispatchQueue.global().async { 58 | Debug.print(event) 59 | #if TRACKING 60 | 61 | #endif 62 | } 63 | } 64 | 65 | static func track(_ error: Error?, file: String = #file, function: String = #function, line: Int = #line) { 66 | guard let error = error else { 67 | return 68 | } 69 | 70 | DispatchQueue.global().async { 71 | let description = String(describing: error) 72 | Debug.print(description, file: file, function: function, line: line) 73 | 74 | #if TRACKING 75 | 76 | #endif 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /iCookTV/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 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | FacebookAutoInitEnabled 24 | 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | arm64 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /iCookTV/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 | -------------------------------------------------------------------------------- /iCookTV/Metrics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Metrics.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 28/02/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | struct Metrics { 30 | 31 | static let EdgePadding = UIEdgeInsets(top: 60, left: 90, bottom: 60, right: 90) 32 | 33 | static var horizontalFlowLayout: UICollectionViewFlowLayout { 34 | let _horizontal = UICollectionViewFlowLayout() 35 | _horizontal.scrollDirection = .horizontal 36 | _horizontal.sectionInset = UIEdgeInsets(top: 0, left: EdgePadding.left, bottom: 0, right: EdgePadding.right) 37 | _horizontal.minimumInteritemSpacing = 0 38 | _horizontal.minimumLineSpacing = 50 39 | _horizontal.itemSize = CGSize(width: 308, height: 308) 40 | return _horizontal 41 | } 42 | 43 | static var verticalFlowLayout: UICollectionViewFlowLayout { 44 | let _vertical = UICollectionViewFlowLayout() 45 | _vertical.scrollDirection = .vertical 46 | _vertical.sectionInset = UIEdgeInsets.zero 47 | _vertical.minimumInteritemSpacing = 0 48 | _vertical.minimumLineSpacing = 50 49 | _vertical.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: 100) 50 | _vertical.itemSize = CGSize(width: UIScreen.main.bounds.width, height: 308) 51 | return _vertical 52 | } 53 | 54 | static var gridFlowLayout: UICollectionViewFlowLayout { 55 | let _grid = UICollectionViewFlowLayout() 56 | _grid.scrollDirection = .vertical 57 | _grid.sectionInset = UIEdgeInsets(top: 90, left: 80, bottom: 90, right: 80) 58 | _grid.minimumInteritemSpacing = 40 59 | _grid.minimumLineSpacing = 130 60 | 61 | let numberOfItemsPerRow = 5 62 | let paddings = EdgePadding.left + EdgePadding.right 63 | let spaces = _grid.minimumInteritemSpacing * CGFloat(numberOfItemsPerRow - 1) 64 | let contentWidth = UIScreen.main.bounds.width - paddings - spaces 65 | let itemWidth = contentWidth / CGFloat(numberOfItemsPerRow) 66 | _grid.itemSize = CGSize(width: itemWidth, height: 200) 67 | 68 | return _grid 69 | } 70 | 71 | static var showcaseLayout: UICollectionViewFlowLayout { 72 | let _showcase = UICollectionViewFlowLayout() 73 | _showcase.scrollDirection = .horizontal 74 | _showcase.sectionInset = UIEdgeInsets(top: 100, left: 100, bottom: 220, right: 100) 75 | _showcase.minimumLineSpacing = 80 76 | _showcase.itemSize = CGSize(width: 640, height: 480) 77 | return _showcase 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /iCookTV/Models/CategoriesCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoriesCollection.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | struct CategoriesCollection: DataCollection { 30 | 31 | typealias DataType = Category 32 | 33 | private(set) var items: [Category] 34 | 35 | } 36 | -------------------------------------------------------------------------------- /iCookTV/Models/CategoriesDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoriesDataSource.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 04/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class CategoriesDataSource: DataSource { 30 | 31 | // MARK: - Initialization 32 | 33 | init(categories: [Category]) { 34 | super.init(dataCollection: CategoriesCollection(items: categories)) 35 | } 36 | 37 | // MARK: - UICollectionViewDataSource 38 | 39 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 40 | let cell = collectionView.dequeueReusableCell(for: indexPath) as CategoryCell 41 | cell.configure(with: dataCollection[indexPath.row]) 42 | return cell 43 | } 44 | 45 | } 46 | 47 | 48 | extension CategoriesDataSource { 49 | 50 | static var requestForCategories: URLRequest { 51 | let url = iCookTVKeys.baseAPIURL + "categories.json" 52 | do { 53 | return try URLRequest(url: url, method: .get) 54 | } catch { 55 | fatalError("\(error)") 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /iCookTV/Models/Category.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Category.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 25/03/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | struct Category { 30 | 31 | let id: String 32 | let name: String 33 | let coverURLs: [String] 34 | 35 | } 36 | 37 | 38 | extension Category: Codable { 39 | 40 | private enum CodingKeys: String, CodingKey { 41 | case id 42 | case attributes 43 | } 44 | 45 | private enum AttributesCodingKeys: String, CodingKey { 46 | case name 47 | case coverURLs = "cover-urls" 48 | } 49 | 50 | // MARK: - Decodable 51 | 52 | init(from decoder: Decoder) throws { 53 | let container = try decoder.container(keyedBy: CodingKeys.self) 54 | id = try container.decode(String.self, forKey: .id) 55 | 56 | let attributes = try container.nestedContainer(keyedBy: AttributesCodingKeys.self, forKey: .attributes) 57 | name = try attributes.decode(String.self, forKey: .name) 58 | coverURLs = try attributes.decode([String].self, forKey: .coverURLs) 59 | } 60 | 61 | // MARK: - Encodable 62 | 63 | func encode(to encoder: Encoder) throws { 64 | var container = encoder.container(keyedBy: CodingKeys.self) 65 | try container.encode(id, forKey: .id) 66 | 67 | var attributes = container.nestedContainer(keyedBy: AttributesCodingKeys.self, forKey: .attributes) 68 | try attributes.encode(name, forKey: .name) 69 | try attributes.encode(coverURLs, forKey: .coverURLs) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /iCookTV/Models/DataCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCollection.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | protocol DataCollection { 30 | 31 | associatedtype DataType 32 | 33 | init(items: [DataType]) 34 | 35 | var items: [DataType] { get } 36 | var count: Int { get } 37 | 38 | subscript(index: Int) -> DataType { get } 39 | 40 | func append(_ items: [DataType]) -> Self 41 | func insert(_ item: DataType, atIndex index: Int) -> Self 42 | func deleteItem(atIndex index: Int) -> Self 43 | func moveItem(fromIndex: Int, toIndex: Int) -> Self 44 | } 45 | 46 | 47 | //////////////////////////////////////////////////////////////////////////////// 48 | 49 | 50 | extension DataCollection { 51 | 52 | var count: Int { 53 | return items.count 54 | } 55 | 56 | subscript(index: Int) -> DataType { 57 | return items[index] 58 | } 59 | 60 | func append(_ items: [DataType]) -> Self { 61 | var mutableCollection = self.items 62 | mutableCollection += items 63 | return Self(items: mutableCollection) 64 | } 65 | 66 | func insert(_ item: DataType, atIndex index: Int) -> Self { 67 | var mutableCollection = items 68 | mutableCollection.insert(item, at: index) 69 | return Self(items: mutableCollection) 70 | } 71 | 72 | func deleteItem(atIndex index: Int) -> Self { 73 | var mutableCollection = items 74 | mutableCollection.remove(at: index) 75 | return Self(items: mutableCollection) 76 | } 77 | 78 | func moveItem(fromIndex: Int, toIndex: Int) -> Self { 79 | return deleteItem(atIndex: fromIndex).insert(items[fromIndex], atIndex: toIndex) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /iCookTV/Models/DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSource.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class DataSource: NSObject, SourceType { 30 | 31 | // MARK: - Initialization 32 | 33 | init(dataCollection: Collection) { 34 | self.dataCollection = dataCollection 35 | } 36 | 37 | // MARK: - SourceType 38 | 39 | private(set) var dataCollection: Collection 40 | 41 | var numberOfItems: Int { 42 | return dataCollection.count 43 | } 44 | 45 | subscript(index: Int) -> Collection.DataType { 46 | return dataCollection[index] 47 | } 48 | 49 | // MARK: - UICollectionViewDataSource 50 | 51 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 52 | return numberOfItems 53 | } 54 | 55 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | fatalError("Subclass must override this method.") 57 | } 58 | 59 | // MARK: - Public Methods 60 | 61 | func append(_ items: [Collection.DataType], toCollectionView collectionView: UICollectionView) { 62 | dataCollection = dataCollection.append(items) 63 | collectionView.reloadData() 64 | } 65 | 66 | func moveItem(atIndexPathToTop indexPath: IndexPath, inCollectionView collectionView: UICollectionView) { 67 | dataCollection = dataCollection.moveItem(fromIndex: indexPath.row, toIndex: 0) 68 | collectionView.reloadData() 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /iCookTV/Models/SourceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceType.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 05/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | protocol SourceType: UICollectionViewDataSource { 30 | 31 | associatedtype Collection: DataCollection 32 | 33 | var dataCollection: Collection { get } 34 | var numberOfItems: Int { get } 35 | 36 | subscript(index: Int) -> Collection.DataType { get } 37 | } 38 | -------------------------------------------------------------------------------- /iCookTV/Models/Video.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Video.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 19/02/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | struct Video { 30 | 31 | let id: String 32 | let title: String 33 | let subtitle: String? 34 | let description: String? 35 | let length: Int 36 | let youtube: String 37 | let source: String? 38 | let cover: String 39 | 40 | var coverURL: URL? { 41 | return URL(string: cover) 42 | } 43 | 44 | var timestamp: String { 45 | let seconds = length % 60 46 | let minutes = (length / 60) % 60 47 | let hours = length / 3600 48 | return (hours > 0 ? "\(hours):" : "") + String(format: "%d:%02d", minutes, seconds) 49 | } 50 | 51 | } 52 | 53 | 54 | extension Video: Codable { 55 | 56 | private enum CodingKeys: String, CodingKey { 57 | case id 58 | case attributes 59 | } 60 | 61 | private enum AttributesCodingKeys: String, CodingKey { 62 | case title 63 | case subtitle 64 | case description 65 | case length 66 | case youtube = "embed-url" 67 | case source = "video-url" 68 | case cover = "cover-url" 69 | } 70 | 71 | // MARK: - Decodable 72 | 73 | init(from decoder: Decoder) throws { 74 | let container = try decoder.container(keyedBy: CodingKeys.self) 75 | id = try container.decode(String.self, forKey: .id) 76 | 77 | let attributes = try container.nestedContainer(keyedBy: AttributesCodingKeys.self, forKey: .attributes) 78 | title = try attributes.decode(String.self, forKey: .title) 79 | subtitle = try? attributes.decode(String.self, forKey: .subtitle) 80 | description = try? attributes.decode(String.self, forKey: .description) 81 | length = (try? attributes.decode(Int.self, forKey: .length)) ?? 0 82 | youtube = try attributes.decode(String.self, forKey: .youtube) 83 | source = try? attributes.decode(String.self, forKey: .source) 84 | cover = try attributes.decode(String.self, forKey: .cover) 85 | } 86 | 87 | // MARK: - Encodable 88 | 89 | func encode(to encoder: Encoder) throws { 90 | var container = encoder.container(keyedBy: CodingKeys.self) 91 | try container.encode(id, forKey: .id) 92 | 93 | var attributes = container.nestedContainer(keyedBy: AttributesCodingKeys.self, forKey: .attributes) 94 | try attributes.encode(title, forKey: .title) 95 | try attributes.encode(length, forKey: .length) 96 | try attributes.encode(youtube, forKey: .youtube) 97 | try attributes.encode(cover, forKey: .cover) 98 | 99 | // Optional values 100 | try subtitle.map { try attributes.encode($0, forKey: .subtitle) } 101 | try description.map { try attributes.encode($0, forKey: .description) } 102 | try source.map { try attributes.encode($0, forKey: .source) } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /iCookTV/Models/VideosCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideosCollection.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | struct VideosCollection: DataCollection { 30 | 31 | typealias DataType = Video 32 | 33 | private(set) var items = [Video]() 34 | 35 | } 36 | -------------------------------------------------------------------------------- /iCookTV/Models/VideosDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideosDataSource.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import Alamofire 29 | 30 | class VideosDataSource: DataSource { 31 | 32 | var currentPage: Int { 33 | return Int(numberOfItems / type(of: self).pageSize) 34 | } 35 | 36 | static let pageSize = 20 37 | 38 | private let title: String 39 | fileprivate let categoryID: String 40 | 41 | // MARK: - Initialization 42 | 43 | init(categoryID: String = "", title: String? = nil) { 44 | self.title = title ?? "" 45 | self.categoryID = categoryID 46 | super.init(dataCollection: VideosCollection()) 47 | } 48 | 49 | // MARK: - UICollectionViewDataSource 50 | 51 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 52 | let cell = collectionView.dequeueReusableCell(for: indexPath) as VideoCell 53 | cell.configure(with: dataCollection[indexPath.row]) 54 | return cell 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: IndexPath) -> UICollectionReusableView { 58 | if kind == UICollectionView.elementKindSectionHeader { 59 | let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, for: indexPath) as SectionHeaderView 60 | headerView.accessoryLabel.text = title 61 | return headerView 62 | } 63 | 64 | return UICollectionReusableView() 65 | } 66 | 67 | } 68 | 69 | 70 | extension VideosDataSource { 71 | 72 | var requestForNextPage: URLRequest { 73 | let url = iCookTVKeys.baseAPIURL + "categories/\(categoryID)/videos.json" 74 | let parameters = [ 75 | "page[size]": type(of: self).pageSize, 76 | "page[number]": currentPage + 1 77 | ] 78 | 79 | do { 80 | let urlRequest = try URLRequest(url: url, method: .get) 81 | let encodedURLRequest = try URLEncoding.default.encode(urlRequest, with: parameters) 82 | return encodedURLRequest 83 | } catch { 84 | fatalError("\(error)") 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /iCookTV/Protocols/BlurBackgroundPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlurBackgroundPresentable.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 25/10/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | protocol BlurBackgroundPresentable: class { 30 | /// A reference to the background image view. 31 | var backgroundImageView: UIImageView { get } 32 | 33 | /// Sets up the background image view with a blur effect on top of it. 34 | func setUpBlurBackground() 35 | 36 | /// Animates the image transition of the background image view. 37 | func animateBackgroundTransition(to image: UIImage?) 38 | } 39 | 40 | 41 | extension BlurBackgroundPresentable where Self: UIViewController { 42 | 43 | func setUpBlurBackground() { 44 | view.backgroundColor = UIColor.tvBackgroundColor() 45 | 46 | backgroundImageView.frame = view.bounds 47 | backgroundImageView.contentMode = .scaleAspectFill 48 | backgroundImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 49 | 50 | let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight)) 51 | blurEffectView.frame = view.bounds 52 | blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 53 | 54 | view.insertSubview(backgroundImageView, at: 0) 55 | view.insertSubview(blurEffectView, at: 1) 56 | } 57 | 58 | func animateBackgroundTransition(to image: UIImage?) { 59 | // Throttle background image transition to avoid extensive changes in a short period of time. 60 | let selector = #selector(UIImageView.transition(to:)) 61 | NSObject.cancelPreviousPerformRequests(withTarget: backgroundImageView) 62 | backgroundImageView.perform(selector, with: image, afterDelay: 0.2) 63 | } 64 | 65 | } 66 | 67 | 68 | //////////////////////////////////////////////////////////////////////////////// 69 | 70 | 71 | extension UIImageView { 72 | 73 | @objc fileprivate func transition(to image: UIImage?) { 74 | Debug.print(#function) 75 | UIView.transition( 76 | with: self, 77 | duration: 0.5, 78 | options: [.beginFromCurrentState, .transitionCrossDissolve, .curveEaseIn], 79 | animations: { 80 | self.image = image 81 | }, completion: nil 82 | ) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /iCookTV/Protocols/DataFetching.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataFetching.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 30/10/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Alamofire 28 | 29 | protocol DataFetching: class { 30 | /// A reference to the pagination object that prevents duplicate requests. 31 | var pagination: Pagination { get } 32 | 33 | /// Performs request and parses the response as an array of T. 34 | /// 35 | /// - Parameters: 36 | /// - request: An Alamofire URL request. 37 | /// - sessionManager: A session manager to send the request. Optional, default is SessionManager.default. 38 | /// - completion: A closure to be executed once the request has finished. 39 | func fetch(request: URLRequestConvertible, with sessionManager: SessionManager, completion: @escaping (Result<[T]>) -> Void) 40 | } 41 | 42 | 43 | extension DataFetching { 44 | 45 | func fetch(request: URLRequestConvertible, with sessionManager: SessionManager = SessionManager.default, completion: @escaping (Result<[T]>) -> Void) { 46 | guard pagination.isReady else { 47 | return 48 | } 49 | 50 | pagination.currentRequest = sessionManager.request(request).responseResult(queue: pagination.queue) { [weak self] result in 51 | switch result { 52 | case let .success(data): 53 | do { 54 | let decoder = JSONDecoder() 55 | let parsed = try decoder.decode(DataKeyPathDecoding<[T]>.self, from: data) 56 | let items = parsed.data 57 | self?.pagination.hasNextPage = parsed.links?.next != nil 58 | 59 | DispatchQueue.main.sync { completion(.success(items)) } 60 | } catch { 61 | DispatchQueue.main.sync { completion(.failure(error)) } 62 | } 63 | case let .failure(error): 64 | DispatchQueue.main.sync { completion(.failure(error)) } 65 | } 66 | } 67 | } 68 | 69 | } 70 | 71 | 72 | //////////////////////////////////////////////////////////////////////////////// 73 | 74 | 75 | class Pagination { 76 | 77 | fileprivate weak var currentRequest: Request? 78 | fileprivate var hasNextPage = true 79 | fileprivate let queue: DispatchQueue 80 | 81 | var isReady: Bool { 82 | return currentRequest == nil && hasNextPage 83 | } 84 | 85 | /// Returns an initialized pagination object. 86 | /// 87 | /// - Parameter name: The label of serial queue for pagination. 88 | init(name: String) { 89 | queue = DispatchQueue(label: name, attributes: []) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /iCookTV/Protocols/DropdownMenuPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropdownMenuPresentable.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 30/10/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | protocol DropdownMenuPresentable: class { 30 | /// A reference to the dropdown menu view. 31 | var dropdownMenuView: MenuView { get } 32 | 33 | /// Returns a configured menu view. 34 | static func defaultMenuView() -> MenuView 35 | 36 | /// Sets up the dropdown menu view at the top. 37 | func setUpDropdownMenuView() 38 | 39 | /// Shows or hides the dropdown menu view according to the context. 40 | func animateDropdownMenuView(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) 41 | } 42 | 43 | 44 | extension DropdownMenuPresentable where Self: UIViewController { 45 | 46 | static func defaultMenuView() -> MenuView { 47 | let menu = MenuView() 48 | menu.backgroundColor = UIColor.tvMenuBarColor() 49 | return menu 50 | } 51 | 52 | func setUpDropdownMenuView() { 53 | dropdownMenuView.frame = CGRect(x: 0, y: -140, width: view.bounds.width, height: 140) 54 | view.addSubview(dropdownMenuView) 55 | } 56 | 57 | func animateDropdownMenuView(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 58 | if context.nextFocusedView == dropdownMenuView.button { 59 | let revealedFrame = CGRect(origin: CGPoint.zero, size: dropdownMenuView.frame.size) 60 | coordinator.addCoordinatedAnimations({ 61 | self.dropdownMenuView.frame = revealedFrame 62 | }, completion: nil) 63 | } else if context.previouslyFocusedView == dropdownMenuView.button { 64 | let hiddenFrame = dropdownMenuView.frame.offsetBy(dx: 0, dy: -dropdownMenuView.frame.height) 65 | coordinator.addCoordinatedAnimations({ 66 | self.dropdownMenuView.frame = hiddenFrame 67 | }, completion: nil) 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /iCookTV/Protocols/LoadingIndicatorPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingIndicatorPresentable.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 29/10/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | protocol LoadingIndicatorPresentable: class { 30 | /// A reference to the background image view. 31 | var loadingIndicator: UIActivityIndicatorView { get } 32 | 33 | /// Toggles the loading indicator. 34 | var isLoading: Bool { get set } 35 | 36 | /// Returns a configured loading indicator. 37 | static func defaultLoadingIndicator() -> UIActivityIndicatorView 38 | 39 | /// Sets up the activity indicator view in the center of view. 40 | func setUpLoadingIndicator() 41 | } 42 | 43 | 44 | extension LoadingIndicatorPresentable where Self: UIViewController { 45 | 46 | var isLoading: Bool { 47 | get { 48 | return loadingIndicator.isAnimating 49 | } 50 | set { 51 | if newValue { 52 | loadingIndicator.startAnimating() 53 | } else { 54 | loadingIndicator.stopAnimating() 55 | } 56 | } 57 | } 58 | 59 | static func defaultLoadingIndicator() -> UIActivityIndicatorView { 60 | let indicator = UIActivityIndicatorView(style: .whiteLarge) 61 | indicator.color = UIColor.Palette.GreyishBrown 62 | indicator.hidesWhenStopped = true 63 | return indicator 64 | } 65 | 66 | func setUpLoadingIndicator() { 67 | view.addSubview(loadingIndicator) 68 | loadingIndicator.translatesAutoresizingMaskIntoConstraints = false 69 | loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 70 | loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /iCookTV/Protocols/OverlayViewPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayViewPresentable.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 28/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import Kingfisher 29 | 30 | protocol OverlayViewPresentable { 31 | /// A view used as the decorative overlay. 32 | var overlayView: UIView { get } 33 | 34 | /// Provides the superview of a given overlay view. 35 | func containerViewForOverlayView(_ overlayView: UIView) -> UIView 36 | 37 | /// Provides the constraints for a given overlay view. 38 | func constraintsForOverlayView(_ overlayView: UIView) -> [NSLayoutConstraint] 39 | 40 | /// Required method to update the background image behind the overlay view. 41 | func updateBackground(with image: UIImage?) 42 | } 43 | 44 | 45 | extension OverlayViewPresentable where Self: UIViewController { 46 | 47 | func setOverlayViewHidden(_ hidden: Bool, animated: Bool) { 48 | if !hidden { 49 | layoutOverlayViewIfNeeded() 50 | overlayView.superview?.bringSubviewToFront(overlayView) 51 | } 52 | 53 | let transition = { 54 | self.overlayView.alpha = hidden ? 0 : 1 55 | } 56 | 57 | if animated { 58 | UIView.animate(withDuration: 0.3, animations: transition) 59 | } else { 60 | transition() 61 | } 62 | 63 | if let url = GroundControl.defaultBackgroundURL, !hidden { 64 | ImageDownloader.default.downloadImage(with: url, options: []) { [weak self] in 65 | let image = try? $0.get().image 66 | self?.updateBackground(with: image) 67 | } 68 | } 69 | } 70 | 71 | private func layoutOverlayViewIfNeeded() { 72 | if overlayView.superview != nil { 73 | return 74 | } 75 | 76 | containerViewForOverlayView(overlayView).addSubview(overlayView) 77 | overlayView.translatesAutoresizingMaskIntoConstraints = false 78 | NSLayoutConstraint.activate(constraintsForOverlayView(overlayView)) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /iCookTV/Protocols/Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reusable.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 30/10/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | protocol Reusable: class { 30 | /// A string identifying the view object to be reused. 31 | static var reuseIdentifier: String { get } 32 | } 33 | 34 | 35 | extension Reusable { 36 | 37 | static var reuseIdentifier: String { 38 | return String(describing: self) 39 | } 40 | 41 | } 42 | 43 | 44 | //////////////////////////////////////////////////////////////////////////////// 45 | 46 | 47 | extension UICollectionReusableView: Reusable {} 48 | 49 | extension UICollectionView { 50 | 51 | func register(cell type: T.Type) { 52 | register(type, forCellWithReuseIdentifier: type.reuseIdentifier) 53 | } 54 | 55 | func register(supplementaryView type: T.Type, ofKind kind: String) { 56 | register(type, forSupplementaryViewOfKind: kind, withReuseIdentifier: type.reuseIdentifier) 57 | } 58 | 59 | func dequeueReusableCell(type: T.Type = T.self, for indexPath: IndexPath) -> T { 60 | if let cell = dequeueReusableCell(withReuseIdentifier: type.reuseIdentifier, for: indexPath) as? T { 61 | return cell 62 | } else { 63 | return type.init() 64 | } 65 | } 66 | 67 | func dequeueReusableSupplementaryView(type: T.Type = T.self, ofKind kind: String, for indexPath: IndexPath) -> T { 68 | if let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: type.reuseIdentifier, for: indexPath) as? T { 69 | return view 70 | } else { 71 | return type.init() 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /iCookTV/Protocols/Trackable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Trackable.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 03/05/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// Required properties of a trackable view controller class. 30 | protocol Trackable: class { 31 | var pageView: PageView? { get } 32 | } 33 | 34 | //////////////////////////////////////////////////////////////////////////////// 35 | 36 | 37 | protocol TrackableAttributes { 38 | var name: String { get } 39 | var details: [String: Any] { get } 40 | } 41 | 42 | 43 | extension TrackableAttributes where Self: CustomStringConvertible { 44 | var description: String { 45 | return "{\n name: \(name),\n details: \(details)\n}" 46 | } 47 | 48 | var attributes: [String: Any] { 49 | var attributes = details 50 | attributes["name"] = name 51 | attributes[TrackableKey.categoryTitle] = nil 52 | attributes[TrackableKey.videoTitle] = nil 53 | return attributes 54 | } 55 | } 56 | 57 | 58 | struct PageView: TrackableAttributes, CustomStringConvertible { 59 | let name: String 60 | let details: [String: Any] 61 | 62 | init(name: String, details: [String: Any] = [:]) { 63 | self.name = name 64 | self.details = details 65 | } 66 | } 67 | 68 | 69 | struct Event: TrackableAttributes, CustomStringConvertible { 70 | let name: String 71 | let details: [String: Any] 72 | } 73 | 74 | //////////////////////////////////////////////////////////////////////////////// 75 | 76 | 77 | struct TrackableKey { 78 | static let numberOfItems = "number_of_items" 79 | static let categoryID = "category_id" 80 | static let categoryTitle = "category_title" 81 | static let videoID = "video_id" 82 | static let videoTitle = "video_title" 83 | static let page = "page" 84 | static let currentTime = "current_time" 85 | static let duration = "duration" 86 | static let percentage = "percentage" 87 | } 88 | -------------------------------------------------------------------------------- /iCookTV/Protocols/VideosGridLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideosGridLayout.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 30/10/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | protocol VideosGridLayout: class { 30 | /// A reference to the collection view. 31 | var collectionView: UICollectionView { get } 32 | 33 | /// Returns a configured collection view. 34 | static func defaultCollectionView(dataSource: UICollectionViewDataSource, delegate: UICollectionViewDelegate) -> UICollectionView 35 | 36 | /// Sets up the collection view in the view hierarchy. 37 | func setUpCollectionView() 38 | } 39 | 40 | 41 | extension VideosGridLayout where Self: UIViewController { 42 | 43 | static func defaultCollectionView(dataSource: UICollectionViewDataSource, delegate: UICollectionViewDelegate) -> UICollectionView { 44 | let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: Metrics.gridFlowLayout) 45 | collectionView.register(cell: VideoCell.self) 46 | collectionView.register(supplementaryView: SectionHeaderView.self, ofKind: UICollectionView.elementKindSectionHeader) 47 | collectionView.remembersLastFocusedIndexPath = true 48 | collectionView.dataSource = dataSource 49 | collectionView.delegate = delegate 50 | return collectionView 51 | } 52 | 53 | func setUpCollectionView() { 54 | collectionView.frame = view.bounds 55 | view.addSubview(collectionView) 56 | if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { 57 | flowLayout.headerReferenceSize = CGSize(width: collectionView.frame.width, height: SectionHeaderView.requiredHeight) 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /iCookTV/Views/CategoryCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryCell.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 23/03/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import Kingfisher 29 | 30 | class CategoryCell: UICollectionViewCell { 31 | 32 | var hasDisplayedCover: Bool { 33 | return tasks.isEmpty 34 | } 35 | 36 | // MARK: - Private Properties 37 | 38 | private(set) lazy var imageView: UIImageView = { 39 | let _imageView = UIImageView() 40 | _imageView.image = UIImage.placeholderImage(with: self.bounds.size) 41 | _imageView.contentMode = .scaleAspectFill 42 | return _imageView 43 | }() 44 | 45 | private(set) lazy var textLabel: UILabel = { 46 | let _label = UILabel() 47 | _label.font = UIFont.tvFontForCategoryCell() 48 | _label.textColor = UIColor.tvTextColor() 49 | _label.textAlignment = .center 50 | return _label 51 | }() 52 | 53 | private let coverBuilder = CoverBuilder() 54 | 55 | private var tasks = [Grid: DownloadTask]() 56 | 57 | // MARK: - Initialization 58 | 59 | override init(frame: CGRect) { 60 | super.init(frame: frame) 61 | setUpSubviews() 62 | } 63 | 64 | required init?(coder aDecoder: NSCoder) { 65 | super.init(coder: aDecoder) 66 | setUpSubviews() 67 | } 68 | 69 | // MARK: - UICollectionViewCell 70 | 71 | override func prepareForReuse() { 72 | super.prepareForReuse() 73 | for (index, task) in tasks { 74 | task.cancel() 75 | tasks[index] = nil 76 | } 77 | coverBuilder.resetCover() 78 | imageView.image = UIImage.placeholderImage(with: bounds.size) 79 | textLabel.text = nil 80 | } 81 | 82 | // MARK: - UIFocusEnvironment 83 | 84 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 85 | let focused = (context.nextFocusedView == self) 86 | 87 | let color = focused ? UIColor.tvFocusedTextColor() : UIColor.tvTextColor() 88 | let transform = focused ? CGAffineTransform(translationX: 0, y: 25) : CGAffineTransform.identity 89 | let font = focused ? UIFont.tvFontForFocusedCategoryCell() : UIFont.tvFontForCategoryCell() 90 | 91 | coordinator.addCoordinatedAnimations({ 92 | self.textLabel.textColor = color 93 | self.textLabel.transform = transform 94 | self.textLabel.font = font 95 | }, completion: nil) 96 | } 97 | 98 | // MARK: - Private Methods 99 | 100 | fileprivate func setUpSubviews() { 101 | clipsToBounds = false 102 | imageView.adjustsImageWhenAncestorFocused = true 103 | 104 | contentView.addSubview(imageView) 105 | contentView.addSubview(textLabel) 106 | 107 | imageView.translatesAutoresizingMaskIntoConstraints = false 108 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true 109 | imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true 110 | imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true 111 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true 112 | 113 | textLabel.translatesAutoresizingMaskIntoConstraints = false 114 | textLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 22).isActive = true 115 | textLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true 116 | textLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true 117 | } 118 | 119 | func setUpCover(_ urls: [Grid: URL], forCategory category: Category) { 120 | // Cancel previous tasks 121 | for (grid, task) in tasks { 122 | task.cancel() 123 | tasks[grid] = nil 124 | } 125 | 126 | for (grid, url) in urls { 127 | let downloading = ImageDownloader.default.downloadImage(with: url, options: []) { [weak self] in 128 | self?.tasks[grid] = nil 129 | guard let result = try? $0.get(), result.url == url else { 130 | return 131 | } 132 | let image = result.image 133 | 134 | self?.coverBuilder.add(image: image, to: grid, categoryID: category.id) { newCover in 135 | if let current = self { 136 | UIView.transition( 137 | with: current.imageView, 138 | duration: 0.3, 139 | options: [.beginFromCurrentState, .transitionCrossDissolve, .curveEaseIn], 140 | animations: { 141 | self?.imageView.image = newCover 142 | }, completion: nil 143 | ) 144 | } 145 | } 146 | } 147 | if let task = downloading { 148 | tasks[grid] = task 149 | } 150 | } 151 | } 152 | 153 | // MARK: - Public Methods 154 | 155 | func configure(with category: Category) { 156 | textLabel.text = category.name 157 | 158 | if let cached = coverBuilder.coverForCategory(withID: category.id) { 159 | imageView.image = cached 160 | return 161 | } 162 | 163 | var urls = [Grid: URL]() 164 | for (index, value) in category.coverURLs.enumerated() { 165 | guard let grid = Grid(rawValue: index), let url = URL(string: value) else { continue } 166 | urls[grid] = url 167 | } 168 | 169 | setUpCover(urls, forCategory: category) 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /iCookTV/Views/EmptyStateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyStateView.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 28/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class EmptyStateView: UIView { 30 | 31 | private(set) lazy var imageView: UIImageView = { 32 | let _imageView = UIImageView() 33 | _imageView.image = UIImage(named: "icook-tv-cat") 34 | _imageView.contentMode = .scaleAspectFill 35 | return _imageView 36 | }() 37 | 38 | private(set) lazy var textLabel: UILabel = { 39 | let _label = UILabel() 40 | _label.font = UIFont.tvFontForTagline() 41 | _label.textColor = UIColor.tvTaglineColor() 42 | _label.text = NSLocalizedString("no-video-found", comment: "") 43 | _label.textAlignment = .center 44 | return _label 45 | }() 46 | 47 | // MARK: - Initialization 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | setUpSubviews() 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | super.init(coder: aDecoder) 56 | setUpSubviews() 57 | } 58 | 59 | // MARK: - Private Methods 60 | 61 | private func setUpSubviews() { 62 | addSubview(imageView) 63 | addSubview(textLabel) 64 | 65 | imageView.translatesAutoresizingMaskIntoConstraints = false 66 | textLabel.translatesAutoresizingMaskIntoConstraints = false 67 | 68 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 69 | 70 | let views: [String: Any] = [ 71 | "image": imageView, 72 | "text": textLabel 73 | ] 74 | addConstraints(NSLayoutConstraint.constraints( 75 | withVisualFormat: "H:|-(>=0)-[image(280)]-(>=0)-|", 76 | options: [], 77 | metrics: nil, 78 | views: views 79 | )) 80 | addConstraints(NSLayoutConstraint.constraints( 81 | withVisualFormat: "H:|[text]|", 82 | options: [], 83 | metrics: nil, 84 | views: views 85 | )) 86 | addConstraints(NSLayoutConstraint.constraints( 87 | withVisualFormat: "V:|[image(350)]-50-[text]|", 88 | options: [], 89 | metrics: nil, 90 | views: views 91 | )) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /iCookTV/Views/InsetLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICInsetLabel.swift 3 | // iCook 4 | // 5 | // Created by Ben on 10/07/2015. 6 | // Copyright (c) 2015 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class InsetLabel: UILabel { 30 | 31 | enum CornerRadius { 32 | case dynamic 33 | case constant(CGFloat) 34 | } 35 | 36 | var contentEdgeInsets = UIEdgeInsets.zero 37 | var cornerRadius = CornerRadius.constant(0) 38 | 39 | convenience init(contentEdgeInsets: UIEdgeInsets, cornerRadius: CornerRadius = .constant(0)) { 40 | self.init(frame: CGRect.zero) 41 | self.contentEdgeInsets = contentEdgeInsets 42 | self.cornerRadius = cornerRadius 43 | 44 | switch cornerRadius { 45 | case .constant(let radius) where radius > 0: 46 | layer.cornerRadius = radius 47 | fallthrough 48 | case .dynamic: 49 | layer.masksToBounds = true 50 | layer.shouldRasterize = true 51 | layer.rasterizationScale = UIScreen.main.scale 52 | default: 53 | break 54 | } 55 | } 56 | 57 | // MARK: - UIView 58 | 59 | override var intrinsicContentSize: CGSize { 60 | let size = super.intrinsicContentSize 61 | return CGSize( 62 | width: contentEdgeInsets.left + size.width + contentEdgeInsets.right, 63 | height: contentEdgeInsets.top + size.height + contentEdgeInsets.bottom 64 | ) 65 | } 66 | 67 | override func layoutSubviews() { 68 | super.layoutSubviews() 69 | if case .dynamic = cornerRadius { 70 | layer.cornerRadius = frame.height / 2 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /iCookTV/Views/MainMenuView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainMenuView.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 09/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class MainMenuView: UIView { 30 | 31 | private let frontBanner = UIImageView(image: UIImage(named: "icook-tv-banner-food-front")) 32 | private let backBanner = UIImageView(image: UIImage(named: "icook-tv-banner-food-back")) 33 | private let imageView = UIImageView(image: UIImage(named: "icook-tv-logo")) 34 | 35 | private(set) lazy var titleLabel: UILabel = { 36 | let _title = UILabel() 37 | _title.font = UIFont.tvFontForLogo() 38 | _title.textColor = UIColor.tvHeaderTitleColor() 39 | _title.text = NSLocalizedString("icook-tv", comment: "") 40 | return _title 41 | }() 42 | 43 | private(set) lazy var button: UIButton = { 44 | let _button = MenuButton(type: .system) 45 | _button.titleLabel?.font = UIFont.tvFontForHeaderTitle() 46 | return _button 47 | }() 48 | 49 | private let focusGuide = UIFocusGuide() 50 | private var frontBannerConstraint: NSLayoutConstraint? 51 | private var backBannerConstraint: NSLayoutConstraint? 52 | 53 | private let bannerOffset = ( 54 | front: (normal: CGFloat(0), focused: CGFloat(-20)), 55 | back: (normal: CGFloat(-20), focused: CGFloat(0)) 56 | ) 57 | 58 | // MARK: - Initialization 59 | 60 | override init(frame: CGRect) { 61 | super.init(frame: frame) 62 | setUpSubviews() 63 | } 64 | 65 | required init?(coder aDecoder: NSCoder) { 66 | super.init(coder: aDecoder) 67 | setUpSubviews() 68 | } 69 | 70 | // MARK: - UIFocusEnvironment 71 | 72 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 73 | let focused = (context.nextFocusedView == button) 74 | 75 | coordinator.addCoordinatedAnimations({ 76 | self.frontBannerConstraint?.constant = focused ? self.bannerOffset.front.focused : self.bannerOffset.front.normal 77 | self.backBannerConstraint?.constant = focused ? self.bannerOffset.back.focused : self.bannerOffset.back.normal 78 | self.layoutIfNeeded() 79 | }, completion: nil) 80 | } 81 | 82 | // MARK: - Private Methods 83 | 84 | private func setUpSubviews() { 85 | addSubview(backBanner) 86 | addSubview(frontBanner) 87 | addSubview(imageView) 88 | addSubview(titleLabel) 89 | addSubview(button) 90 | addLayoutGuide(focusGuide) 91 | 92 | frontBanner.translatesAutoresizingMaskIntoConstraints = false 93 | backBanner.translatesAutoresizingMaskIntoConstraints = false 94 | imageView.translatesAutoresizingMaskIntoConstraints = false 95 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 96 | button.translatesAutoresizingMaskIntoConstraints = false 97 | 98 | frontBanner.topAnchor.constraint(equalTo: topAnchor).isActive = true 99 | frontBannerConstraint = frontBanner.leadingAnchor.constraint(equalTo: leadingAnchor, constant: bannerOffset.front.normal) 100 | frontBannerConstraint?.isActive = true 101 | 102 | backBanner.topAnchor.constraint(equalTo: topAnchor).isActive = true 103 | backBannerConstraint = backBanner.leadingAnchor.constraint(equalTo: leadingAnchor, constant: bannerOffset.back.normal) 104 | backBannerConstraint?.isActive = true 105 | 106 | focusGuide.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 107 | focusGuide.trailingAnchor.constraint(equalTo: button.leadingAnchor).isActive = true 108 | focusGuide.heightAnchor.constraint(equalTo: heightAnchor).isActive = true 109 | focusGuide.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 110 | 111 | if #available(tvOS 10.0, *) { 112 | focusGuide.preferredFocusEnvironments = [button] 113 | } else { 114 | focusGuide.preferredFocusedView = button 115 | } 116 | 117 | imageView.contentMode = .scaleAspectFill 118 | imageView.widthAnchor.constraint(equalToConstant: 120).isActive = true 119 | imageView.heightAnchor.constraint(equalToConstant: 88).isActive = true 120 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 222).isActive = true 121 | imageView.topAnchor.constraint(equalTo: topAnchor, constant: 136).isActive = true 122 | 123 | titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 20).isActive = true 124 | titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 150).isActive = true 125 | 126 | button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -148).isActive = true 127 | button.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /iCookTV/Views/MenuButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuButton.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 09/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | class MenuButton: UIButton { 30 | 31 | override init(frame: CGRect) { 32 | super.init(frame: frame) 33 | setUpAppearance() 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | super.init(coder: aDecoder) 38 | setUpAppearance() 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private func setUpAppearance() { 44 | contentEdgeInsets = UIEdgeInsets(top: 15, left: 40, bottom: 15, right: 40) 45 | setTitleColor(UIColor.Palette.Button.TitleColor, for: UIControl.State()) 46 | setTitleColor(UIColor.Palette.FocusedButton.TitleColor, for: .focused) 47 | setImage(UIImage.resizableImage(filledWith: UIColor.Palette.Button.BackgroundColor), for: .normal) 48 | setImage(UIImage.resizableImage(filledWith: UIColor.Palette.FocusedButton.BackgroundColor), for: .focused) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /iCookTV/Views/MenuView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuView.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 05/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A customized view with a layout of `H:|[focusGuide][button]-|`. 30 | class MenuView: UIView { 31 | 32 | private let imageView = UIImageView(image: UIImage(named: "icook-tv-logo")) 33 | 34 | private lazy var titleLabel: UILabel = { 35 | let _label = UILabel() 36 | _label.font = UIFont.tvFontForHeaderTitle() 37 | _label.textColor = UIColor.tvHeaderTitleColor() 38 | _label.text = NSLocalizedString("icook-tv", comment: "") 39 | return _label 40 | }() 41 | 42 | private(set) lazy var button: UIButton = { 43 | let _button = MenuButton(type: .system) 44 | _button.titleLabel?.font = UIFont.tvFontForMenuButton() 45 | return _button 46 | }() 47 | 48 | private let focusGuide = UIFocusGuide() 49 | 50 | // MARK: - Initialization 51 | 52 | override init(frame: CGRect) { 53 | super.init(frame: frame) 54 | setUpSubviews() 55 | } 56 | 57 | required init?(coder aDecoder: NSCoder) { 58 | super.init(coder: aDecoder) 59 | setUpSubviews() 60 | } 61 | 62 | // MARK: - Private Methods 63 | 64 | private func setUpSubviews() { 65 | addSubview(imageView) 66 | addSubview(titleLabel) 67 | addSubview(button) 68 | addLayoutGuide(focusGuide) 69 | 70 | imageView.translatesAutoresizingMaskIntoConstraints = false 71 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 72 | button.translatesAutoresizingMaskIntoConstraints = false 73 | 74 | focusGuide.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 75 | focusGuide.trailingAnchor.constraint(equalTo: button.leadingAnchor).isActive = true 76 | focusGuide.heightAnchor.constraint(equalTo: heightAnchor).isActive = true 77 | focusGuide.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 78 | 79 | if #available(tvOS 10.0, *) { 80 | focusGuide.preferredFocusEnvironments = [button] 81 | } else { 82 | focusGuide.preferredFocusedView = button 83 | } 84 | 85 | imageView.contentMode = .scaleAspectFill 86 | imageView.widthAnchor.constraint(equalToConstant: 87).isActive = true 87 | imageView.heightAnchor.constraint(equalToConstant: 64).isActive = true 88 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.EdgePadding.left).isActive = true 89 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 90 | 91 | titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 20).isActive = true 92 | titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 93 | 94 | button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.EdgePadding.right).isActive = true 95 | button.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /iCookTV/Views/SectionHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeaderView.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 21/03/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A customized view with a layout of `H:|-[icon]-[title]-(>=0)-[accessory]-|`. 30 | class SectionHeaderView: UICollectionReusableView { 31 | 32 | static let requiredHeight = CGFloat(140) 33 | 34 | private let imageView = UIImageView(image: UIImage(named: "icook-tv-logo")) 35 | 36 | private(set) lazy var titleLabel: UILabel = { 37 | let _title = UILabel() 38 | _title.font = UIFont.tvFontForHeaderTitle() 39 | _title.textColor = UIColor.tvHeaderTitleColor() 40 | _title.text = NSLocalizedString("icook-tv", comment: "") 41 | return _title 42 | }() 43 | 44 | private(set) lazy var accessoryLabel: UILabel = { 45 | let _accessory = UILabel() 46 | _accessory.font = UIFont.tvFontForHeaderTitle() 47 | _accessory.textColor = UIColor.tvHeaderTitleColor() 48 | return _accessory 49 | }() 50 | 51 | // MARK: - Initialization 52 | 53 | override init(frame: CGRect) { 54 | super.init(frame: frame) 55 | setUpSubviews() 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | super.init(coder: aDecoder) 60 | setUpSubviews() 61 | } 62 | 63 | // MARK: - Private Methods 64 | 65 | private func setUpSubviews() { 66 | addSubview(imageView) 67 | addSubview(titleLabel) 68 | addSubview(accessoryLabel) 69 | 70 | imageView.translatesAutoresizingMaskIntoConstraints = false 71 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 72 | accessoryLabel.translatesAutoresizingMaskIntoConstraints = false 73 | 74 | imageView.contentMode = .scaleAspectFill 75 | imageView.widthAnchor.constraint(equalToConstant: 87).isActive = true 76 | imageView.heightAnchor.constraint(equalToConstant: 64).isActive = true 77 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.EdgePadding.left).isActive = true 78 | imageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true 79 | 80 | titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 20).isActive = true 81 | titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10).isActive = true 82 | 83 | accessoryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.EdgePadding.right).isActive = true 84 | accessoryLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /iCookTV/Views/VideoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCell.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 19/02/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import Kingfisher 29 | 30 | class VideoCell: UICollectionViewCell { 31 | 32 | private(set) lazy var imageView: UIImageView = { 33 | let _imageView = UIImageView() 34 | _imageView.image = UIImage.placeholderImage(with: self.bounds.size) 35 | _imageView.contentMode = .scaleAspectFill 36 | return _imageView 37 | }() 38 | 39 | private(set) lazy var titleLabel: UILabel = { 40 | let _title = UILabel() 41 | _title.font = UIFont.tvFontForVideoCell() 42 | _title.textColor = UIColor.tvTextColor() 43 | _title.textAlignment = .center 44 | return _title 45 | }() 46 | 47 | private(set) lazy var timeLabel: UILabel = { 48 | let _time = InsetLabel(contentEdgeInsets: UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20)) 49 | _time.font = UIFont.tvFontForVideoLength() 50 | _time.textColor = UIColor.white 51 | _time.backgroundColor = UIColor.Palette.GreyishBrown 52 | _time.textAlignment = .center 53 | _time.alpha = 0 54 | return _time 55 | }() 56 | 57 | private var timeLabelConstraints = (left: NSLayoutConstraint(), bottom: NSLayoutConstraint()) 58 | 59 | /// Offsets due to the adjusted image bounds when focused. 60 | private let timeLabelOffsets = ( 61 | left: (normal: CGFloat(0), focused: CGFloat(-20)), 62 | bottom: (normal: CGFloat(0), focused: CGFloat(-10)) 63 | ) 64 | 65 | // MARK: - Initialization 66 | 67 | override init(frame: CGRect) { 68 | super.init(frame: frame) 69 | setUpAppearance() 70 | } 71 | 72 | required init?(coder aDecoder: NSCoder) { 73 | super.init(coder: aDecoder) 74 | setUpAppearance() 75 | } 76 | 77 | // MARK: - UICollectionViewCell 78 | 79 | override func prepareForReuse() { 80 | super.prepareForReuse() 81 | imageView.kf.cancelDownloadTask() 82 | imageView.image = UIImage.placeholderImage(with: bounds.size) 83 | titleLabel.text = nil 84 | } 85 | 86 | // MARK: - UIFocusEnvironment 87 | 88 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 89 | let focused = (context.nextFocusedView == self) 90 | 91 | let color = focused ? UIColor.tvFocusedTextColor() : UIColor.tvTextColor() 92 | let transform = focused ? CGAffineTransform(translationX: 0, y: 15) : CGAffineTransform.identity 93 | let font = focused ? UIFont.tvFontForFocusedVideoCell() : UIFont.tvFontForVideoCell() 94 | let alpha: CGFloat = focused ? 1 : 0 95 | let offset = ( 96 | left: focused ? self.timeLabelOffsets.left.focused : self.timeLabelOffsets.left.normal, 97 | bottom: focused ? self.timeLabelOffsets.bottom.focused : self.timeLabelOffsets.bottom.normal 98 | ) 99 | 100 | coordinator.addCoordinatedAnimations({ 101 | self.titleLabel.textColor = color 102 | self.titleLabel.transform = transform 103 | self.titleLabel.font = font 104 | self.timeLabel.alpha = alpha 105 | self.timeLabelConstraints.left.constant = offset.left 106 | self.timeLabelConstraints.bottom.constant = offset.bottom 107 | self.contentView.layoutIfNeeded() 108 | }, completion: nil) 109 | } 110 | 111 | // MARK: - Private Methods 112 | 113 | private func setUpAppearance() { 114 | clipsToBounds = false 115 | imageView.adjustsImageWhenAncestorFocused = true 116 | 117 | contentView.addSubview(imageView) 118 | contentView.addSubview(titleLabel) 119 | contentView.addSubview(timeLabel) 120 | 121 | imageView.translatesAutoresizingMaskIntoConstraints = false 122 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true 123 | imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true 124 | imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true 125 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true 126 | 127 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 128 | titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20).isActive = true 129 | titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true 130 | titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true 131 | 132 | timeLabel.translatesAutoresizingMaskIntoConstraints = false 133 | timeLabelConstraints.left = timeLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: timeLabelOffsets.left.normal) 134 | timeLabelConstraints.bottom = imageView.bottomAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: timeLabelOffsets.bottom.normal) 135 | timeLabelConstraints.left.isActive = true 136 | timeLabelConstraints.bottom.isActive = true 137 | } 138 | 139 | // MARK: - Public Methods 140 | 141 | func configure(with video: Video) { 142 | if let url = video.coverURL { 143 | imageView.kf.setImage(with: url, placeholder: UIImage.placeholderImage(with: bounds.size)) 144 | } 145 | titleLabel.text = video.title 146 | timeLabel.text = video.timestamp 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /iCookTV/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | "CFBundleDisplayName" = "iCook"; 4 | -------------------------------------------------------------------------------- /iCookTV/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iCookTV 4 | 5 | Created by Ben on 09/04/2016. 6 | Copyright © 2016 Polydice, Inc. All rights reserved. 7 | */ 8 | 9 | "icook-tv" = "iCook TV"; 10 | 11 | "history" = "History"; 12 | 13 | "home" = "Home"; 14 | 15 | "retry" = "Retry"; 16 | 17 | "ok" = "OK"; 18 | 19 | "launch-screen-upper-tagline" = "icook.tw"; 20 | 21 | "launch-screen-lower-tagline" = ""; 22 | 23 | "no-history-found" = "You haven't watched any video."; 24 | 25 | "no-video-found" = "No video found."; 26 | 27 | "error-title" = "Error\n"; 28 | 29 | "video-error" = "There's something wrong with this video."; 30 | 31 | "contact-info" = "Please contact us at hi@icook.tw if this issue keeps happening."; 32 | -------------------------------------------------------------------------------- /iCookTV/iCookTVKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iCookTVKeys.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 25/04/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import Keys 29 | 30 | /// A pod keys wrapper. 31 | struct iCookTVKeys { 32 | 33 | static let baseAPIURL: String = { 34 | ICookTVKeys().baseAPIURL 35 | }() 36 | 37 | static let FacebookAppID: String = { 38 | ICookTVKeys().facebookAppID 39 | }() 40 | 41 | static let ComScorePublisherID: String = { 42 | ICookTVKeys().comScorePublisherID 43 | }() 44 | } 45 | -------------------------------------------------------------------------------- /iCookTV/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | "CFBundleDisplayName" = "爱料理"; 4 | -------------------------------------------------------------------------------- /iCookTV/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iCookTV 4 | 5 | Created by Ben on 09/04/2016. 6 | Copyright © 2016 Polydice, Inc. All rights reserved. 7 | */ 8 | 9 | "icook-tv" = "爱料理 TV"; 10 | 11 | "history" = "浏览纪录"; 12 | 13 | "home" = "首页"; 14 | 15 | "retry" = "再试一次"; 16 | 17 | "ok" = "OK"; 18 | 19 | "launch-screen-upper-tagline" = "爱料理食谱社群"; 20 | 21 | "launch-screen-lower-tagline" = "icook.tw"; 22 | 23 | "no-history-found" = "先看些视频吧!"; 24 | 25 | "no-video-found" = "目前没有视频"; 26 | 27 | "error-title" = "发生错误\n"; 28 | 29 | "video-error" = "视频目前无法播放,请稍候再试"; 30 | 31 | "contact-info" = "如果问题持续发生,请来信 hi@icook.tw 告诉我们,谢谢"; 32 | -------------------------------------------------------------------------------- /iCookTV/zh-Hant.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | "CFBundleDisplayName" = "愛料理"; 4 | -------------------------------------------------------------------------------- /iCookTV/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iCookTV 4 | 5 | Created by Ben on 09/04/2016. 6 | Copyright © 2016 Polydice, Inc. All rights reserved. 7 | */ 8 | 9 | "icook-tv" = "愛料理 TV"; 10 | 11 | "history" = "瀏覽紀錄"; 12 | 13 | "home" = "首頁"; 14 | 15 | "retry" = "再試一次"; 16 | 17 | "ok" = "OK"; 18 | 19 | "launch-screen-upper-tagline" = "愛料理食譜社群"; 20 | 21 | "launch-screen-lower-tagline" = "icook.tw"; 22 | 23 | "no-history-found" = "先看些影片吧!"; 24 | 25 | "no-video-found" = "目前沒有影片"; 26 | 27 | "error-title" = "發生錯誤\n"; 28 | 29 | "video-error" = "影片目前無法播放,請稍候再試"; 30 | 31 | "contact-info" = "如果問題持續發生,請來信 hi@icook.tw 告訴我們,謝謝"; 32 | -------------------------------------------------------------------------------- /iCookTVTests/Category.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "9527", 3 | "type": "categories", 4 | "links": {}, 5 | "attributes": { 6 | "name": "愛料理廚房", 7 | "cover-urls": [ 8 | "https://imag.es/1.jpg", 9 | "https://imag.es/2.jpg", 10 | "https://imag.es/3.jpg", 11 | "https://imag.es/4.jpg" 12 | ] 13 | }, 14 | "relationships": {} 15 | } 16 | -------------------------------------------------------------------------------- /iCookTVTests/CategorySpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategorySpec.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 26/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import iCookTV 28 | import XCTest 29 | 30 | final class CategorySpec: XCTestCase { 31 | 32 | func testDecoding() throws { 33 | // Given Category.json 34 | let data: Data = Resources.testData(named: "Category.json")! 35 | 36 | // When decoding 37 | let decoder = JSONDecoder() 38 | let category = try decoder.decode(Category.self, from: data) 39 | 40 | // It should parse JSON as Category 41 | XCTAssertEqual(category.id, "9527") 42 | XCTAssertEqual(category.name, "愛料理廚房") 43 | XCTAssertEqual(category.coverURLs, [ 44 | "https://imag.es/1.jpg", 45 | "https://imag.es/2.jpg", 46 | "https://imag.es/3.jpg", 47 | "https://imag.es/4.jpg" 48 | ]) 49 | } 50 | 51 | func testEncoding() throws { 52 | // Given a Category object 53 | let category = Category( 54 | id: "9527", 55 | name: "愛料理廚房", 56 | coverURLs: [ 57 | "https://imag.es/1.jpg", 58 | "https://imag.es/2.jpg" 59 | ] 60 | ) 61 | 62 | // When encoding 63 | let encoder = JSONEncoder() 64 | let json = try encoder.encode(category) 65 | let jsonString = String(data: json, encoding: .utf8) 66 | 67 | // It should encode Category to JSON 68 | XCTAssert(jsonString!.contains("\"id\":\"9527\"")) 69 | XCTAssert(jsonString!.contains("\"attributes\":{")) 70 | XCTAssert(jsonString!.contains("\"name\":\"愛料理廚房\"")) 71 | XCTAssert(jsonString!.contains("\"cover-urls\":[\"https:\\/\\/imag.es\\/1.jpg\",\"https:\\/\\/imag.es\\/2.jpg\"]")) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /iCookTVTests/DataCollectionSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCollectionSpec.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import iCookTV 28 | import XCTest 29 | 30 | final class DataCollectionSpec: XCTestCase { 31 | 32 | private struct TestCollection: DataCollection { 33 | typealias DataType = Int 34 | private(set) var items: [Int] 35 | } 36 | 37 | private var dataCollection = TestCollection(items: []) 38 | 39 | override func setUp() { 40 | dataCollection = TestCollection(items: [1, 1, 2, 3, 5, 8]) 41 | } 42 | 43 | func testCount() { 44 | // It should return the count of items 45 | XCTAssertEqual(dataCollection.count, 6) 46 | } 47 | 48 | func testSubscript() { 49 | // It should return the item at index 50 | XCTAssertEqual(dataCollection[0], 1) 51 | XCTAssertEqual(dataCollection[1], 1) 52 | XCTAssertEqual(dataCollection[2], 2) 53 | XCTAssertEqual(dataCollection[3], 3) 54 | XCTAssertEqual(dataCollection[4], 5) 55 | XCTAssertEqual(dataCollection[5], 8) 56 | } 57 | 58 | func testAppendItems() { 59 | // Given 60 | let newItems = [13, 21, 34, 55] 61 | 62 | // When 63 | let newCollection = dataCollection.append(newItems) 64 | 65 | // Then the original data collection should remain the same 66 | XCTAssertEqual(dataCollection.count, 6) 67 | 68 | // It should append items to the new collection 69 | XCTAssertEqual(newCollection.count, 10) 70 | XCTAssertEqual(newCollection[6], 13) 71 | XCTAssertEqual(newCollection[7], 21) 72 | XCTAssertEqual(newCollection[8], 34) 73 | XCTAssertEqual(newCollection[9], 55) 74 | } 75 | 76 | func testInsertItemAtIndex() { 77 | // When 78 | let newCollection = dataCollection.insert(42, atIndex: 3) 79 | 80 | // Then the original data collection should remain the same 81 | XCTAssertEqual(dataCollection.count, 6) 82 | 83 | // It should insert item to the new collection 84 | XCTAssertEqual(newCollection.count, 7) 85 | XCTAssertEqual(newCollection.items, [1, 1, 2, 42, 3, 5, 8]) 86 | } 87 | 88 | func testDeleteItemAtIndex() { 89 | // When 90 | let newCollection = dataCollection.deleteItem(atIndex: 3) 91 | 92 | // Then the original data collection should remain the same 93 | XCTAssertEqual(dataCollection.count, 6) 94 | 95 | // It should delete item at index in the new collection 96 | XCTAssertEqual(newCollection.count, 5) 97 | XCTAssertEqual(newCollection.items, [1, 1, 2, 5, 8]) 98 | } 99 | 100 | func testMoveItemFromIndexToIndex() { 101 | // When 102 | let newCollection = dataCollection.moveItem(fromIndex: 1, toIndex: 4) 103 | 104 | // Then the original data collection should remain the same 105 | XCTAssertEqual(dataCollection.count, 6) 106 | 107 | // It should reorder the items in the new collection 108 | XCTAssertEqual(newCollection.count, 6) 109 | XCTAssertEqual(newCollection.items, [1, 2, 3, 5, 1, 8]) 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /iCookTVTests/DataSourceSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSourceSpec.swift 3 | // TryTVOS 4 | // 5 | // Created by Ben on 14/08/2016. 6 | // Copyright © 2016 bcylin. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import iCookTV 28 | import UIKit 29 | import XCTest 30 | 31 | final class DataSourceSpec: XCTestCase { 32 | 33 | private struct TestCollection: DataCollection { 34 | typealias DataType = String 35 | private(set) var items: [String] 36 | } 37 | 38 | private var dataSource = DataSource(dataCollection: TestCollection(items: [])) 39 | private let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewLayout()) 40 | 41 | override func setUp() { 42 | dataSource = DataSource(dataCollection: TestCollection(items: [ 43 | "Lorem", "ipsum", "dolor", "sit", "amet" 44 | ])) 45 | } 46 | 47 | func testNumberOfItems() { 48 | // It should return the count of items 49 | XCTAssertEqual(dataSource.numberOfItems, 5) 50 | } 51 | 52 | func testSubscript() { 53 | // It should return the item at index 54 | XCTAssertEqual(dataSource[0], "Lorem") 55 | XCTAssertEqual(dataSource[1], "ipsum") 56 | XCTAssertEqual(dataSource[2], "dolor") 57 | XCTAssertEqual(dataSource[3], "sit") 58 | XCTAssertEqual(dataSource[4], "amet") 59 | } 60 | 61 | func testAppendItemsToCollectionView() { 62 | // Given 63 | let items = ["consectetur", "adipisicing", "elit"] 64 | 65 | // When 66 | dataSource.append(items, toCollectionView: collectionView) 67 | 68 | // It should append items to collection 69 | XCTAssertEqual(dataSource.numberOfItems, 8) 70 | XCTAssertEqual(dataSource[5], "consectetur") 71 | XCTAssertEqual(dataSource[6], "adipisicing") 72 | XCTAssertEqual(dataSource[7], "elit") 73 | } 74 | 75 | func testMoveItemAtIndexPathToTopInCollectionView() { 76 | // Given 77 | let indexPath = IndexPath(row: 2, section: 0) 78 | 79 | // When 80 | dataSource.moveItem(atIndexPathToTop: indexPath, inCollectionView: collectionView) 81 | 82 | // It should reorder the items 83 | XCTAssertEqual(dataSource.numberOfItems, 5) 84 | XCTAssertEqual(dataSource[0], "dolor") 85 | XCTAssertEqual(dataSource[1], "Lorem") 86 | XCTAssertEqual(dataSource[2], "ipsum") 87 | XCTAssertEqual(dataSource[3], "sit") 88 | XCTAssertEqual(dataSource[4], "amet") 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /iCookTVTests/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.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | develop 23 | 24 | 25 | -------------------------------------------------------------------------------- /iCookTVTests/ResourceHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceHelper.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 26/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | class Resources { 30 | 31 | private class func pathForResource(_ relativePath: String) -> String? { 32 | let bundlePath = Bundle(for: Resources.self).resourcePath! as NSString 33 | return bundlePath.appendingPathComponent(relativePath) 34 | } 35 | 36 | class func testData(named filename: String) -> Data? { 37 | if let path = pathForResource(filename) { 38 | return (try? Data(contentsOf: URL(fileURLWithPath: path))) 39 | } 40 | return nil 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /iCookTVTests/Video.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "42", 3 | "type": "videos", 4 | "links": {}, 5 | "attributes": { 6 | "title": "Lorem", 7 | "subtitle": "ipsum", 8 | "description": "dolor sit amet", 9 | "length": 123.0, 10 | "video-url": "https://vide.os/source.m3u8", 11 | "embed-url": "https://www.youtube.com/watch?v=3345678", 12 | "cover-url": "https://imag.es/cover.jpg" 13 | } 14 | } -------------------------------------------------------------------------------- /iCookTVTests/VideoSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoSpec.swift 3 | // iCookTV 4 | // 5 | // Created by Ben on 26/04/2016. 6 | // Copyright © 2016 Polydice, Inc. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | // 26 | 27 | @testable import iCookTV 28 | import XCTest 29 | 30 | final class VideoSpec: XCTestCase { 31 | 32 | func testDecoding() throws { 33 | // Given Video.json 34 | let data: Data = Resources.testData(named: "Video.json")! 35 | 36 | // When decoding 37 | let decoder = JSONDecoder() 38 | let video = try! decoder.decode(Video.self, from: data) 39 | 40 | // It should parse JSON as Video 41 | XCTAssertEqual(video.id, "42") 42 | XCTAssertEqual(video.title, "Lorem") 43 | XCTAssertEqual(video.subtitle, "ipsum") 44 | XCTAssertEqual(video.description, "dolor sit amet") 45 | XCTAssertEqual(video.length, 123) 46 | XCTAssertEqual(video.youtube, "https://www.youtube.com/watch?v=3345678") 47 | XCTAssertEqual(video.source, "https://vide.os/source.m3u8") 48 | XCTAssertEqual(video.cover, "https://imag.es/cover.jpg") 49 | } 50 | 51 | func testEncoding() throws { 52 | // Given a Video object 53 | let video = Video( 54 | id: "42", 55 | title: "Lorem", 56 | subtitle: "ipsum", 57 | description: "dolor sit amet", 58 | length: 123, 59 | youtube: "https://www.youtube.com/watch?v=3345678", 60 | source: "https://vide.os/source.m3u8", 61 | cover: "https://imag.es/cover.jpg" 62 | ) 63 | 64 | // When encoding 65 | let encoder = JSONEncoder() 66 | let json = try encoder.encode(video) 67 | let jsonString = String(data: json, encoding: .utf8) 68 | 69 | // It should encode Video to JSON 70 | XCTAssert(jsonString!.contains("\"id\":\"42\"")) 71 | XCTAssert(jsonString!.contains("\"title\":\"Lorem\"")) 72 | XCTAssert(jsonString!.contains("\"subtitle\":\"ipsum\"")) 73 | XCTAssert(jsonString!.contains("\"description\":\"dolor sit amet\"")) 74 | XCTAssert(jsonString!.contains("\"length\":123")) 75 | XCTAssert(jsonString!.contains("\"embed-url\":\"https:\\/\\/www.youtube.com\\/watch?v=3345678\"")) 76 | XCTAssert(jsonString!.contains("\"video-url\":\"https:\\/\\/vide.os\\/source.m3u8\"")) 77 | XCTAssert(jsonString!.contains("\"cover-url\":\"https:\\/\\/imag.es\\/cover.jpg\"")) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /mock-GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AD_UNIT_ID_FOR_BANNER_TEST 6 | ca-app-pub-3940256099942544/2934735716 7 | AD_UNIT_ID_FOR_INTERSTITIAL_TEST 8 | ca-app-pub-3940256099942544/4411468910 9 | API_KEY 10 | AIzaSyAzlj4APqi5S58nFtE52Da-fYBOHA2MhaY 11 | BUNDLE_ID 12 | id 13 | CLIENT_ID 14 | 123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com 15 | DATABASE_URL 16 | https://mockproject-1234.firebaseio.com 17 | GCM_SENDER_ID 18 | 123456789000 19 | GOOGLE_APP_ID 20 | 1:123456789000:ios:f1bf012572b04063 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | PLIST_VERSION 32 | 1 33 | PROJECT_ID 34 | mockproject-1234 35 | REVERSED_CLIENT_ID 36 | com.googleusercontent.apps.123456789000-hjugbg6ud799v4c49dim8ce2usclthar 37 | STORAGE_BUCKET 38 | mockproject-1234.appspot.com 39 | 40 | 41 | -------------------------------------------------------------------------------- /scripts/crashlytics.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$CONFIGURATION" = "Release" ] && [ "$CI" != true ]; then 4 | "${PODS_ROOT}/FirebaseCrashlytics/run" 5 | else 6 | echo "Skip Crashlytics script" 7 | fi 8 | --------------------------------------------------------------------------------