├── .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 | 
4 | 
5 | [](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 | [](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 |
--------------------------------------------------------------------------------