├── .hound.yml ├── SampleRecipe ├── img.jpg ├── music_1.lcr ├── Assets.xcassets │ ├── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - Large.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - Small.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Oneup.xml ├── Info.plist ├── MySearchRecipe.swift ├── SampleResource.swift ├── AppDelegate.swift ├── ViewController.swift ├── Catalog.xml └── Base.lproj │ └── ViewController.storyboard ├── NativeBaseSample ├── Assets.xcassets │ ├── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - Large.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - Small.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift ├── ViewController.swift └── Base.lproj │ └── Main.storyboard ├── .gitmodules ├── MoviePlaybackSample ├── Assets.xcassets │ ├── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - Large.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - Small.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Info.plist ├── sample.xml └── AppDelegate.swift ├── Sources ├── Templates │ ├── KitchenTabBar.xml │ ├── AlertRecipe.xml │ ├── Base.xml │ ├── LoadingRecipe.xml │ ├── SearchResult.xml │ ├── SearchRecipe.xml │ └── DescriptiveAlertRecipe.xml ├── Recipes │ ├── DescriptiveAlertRecipe.swift │ ├── Loading.swift │ ├── SearchRecipe.swift │ ├── AlertRecipe.swift │ ├── RecipeTheme.swift │ └── Recipe.swift ├── TVMLKitchen.h ├── Info.plist ├── PresentationType.swift ├── Cookbook.swift ├── KitchenTabBar.swift ├── TVMLBridging.swift ├── kitchen.js └── Kitchen.swift ├── TVMLKitchen.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── TVMLKitchenTests.xcscheme │ ├── SampleRecipeUITests.xcscheme │ └── TVMLKitchen.xcscheme ├── config └── Kitchen.xcconfig ├── TVMLKitchenTests ├── ExpectedAlertRecipe.xml ├── SampleTemplateRecipe.xml ├── ExpectedTabBar.xml ├── ExpectedLoadingRecipe.xml ├── ExpectedSearchRecipe.xml ├── LoadingRecipeTests.swift ├── SearchRecipeTests.swift ├── ExpectedSampleTemplateRecipe.xml ├── AlertRecipeTests.swift ├── TabBarTests.swift ├── RecipeTests.swift ├── Info.plist └── TestHelpers.swift ├── .swiftlint.yml ├── script └── test.sh ├── Documentation ├── DevelopersGuide.md ├── HandlingActionsAndErrors.md ├── GettingStarted.md └── Recipes.md ├── TVMLKitchen.podspec ├── SampleRecipeUITests ├── Info.plist └── SampleRecipeUITests.swift ├── .gitignore ├── LICENSE ├── README.md └── CHANGELOG.md /.hound.yml: -------------------------------------------------------------------------------- 1 | swift: 2 | config_file: .swiftlint.yml 3 | -------------------------------------------------------------------------------- /SampleRecipe/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshi0383/TVMLKitchen/HEAD/SampleRecipe/img.jpg -------------------------------------------------------------------------------- /SampleRecipe/music_1.lcr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshi0383/TVMLKitchen/HEAD/SampleRecipe/music_1.lcr -------------------------------------------------------------------------------- /NativeBaseSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SampleRecipe/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "config/xcconfigs"] 2 | path = config/xcconfigs 3 | url = git@github.com:jspahrsummers/xcconfigs.git 4 | -------------------------------------------------------------------------------- /MoviePlaybackSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/Templates/KitchenTabBar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{menuItems}} 4 | 5 | -------------------------------------------------------------------------------- /Sources/Templates/AlertRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | {{TITLE}} 3 | {{DESCRIPTION}} 4 | {{BUTTONS}} 5 | -------------------------------------------------------------------------------- /Sources/Templates/Base.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | {{template}} 9 | -------------------------------------------------------------------------------- /Sources/Templates/LoadingRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{LOADING}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sources/Templates/SearchResult.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 | Results 4 |
5 |
6 | {{results}} 7 |
8 |
-------------------------------------------------------------------------------- /SampleRecipe/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 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } -------------------------------------------------------------------------------- /Sources/Templates/SearchRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | Search 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Templates/DescriptiveAlertRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | {{TITLE}} 3 | {{DESCRIPTION}} 4 | 5 | {{BUTTONS}} 6 | 7 | -------------------------------------------------------------------------------- /TVMLKitchen.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/Kitchen.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Kitchen.xcconfig 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | TVOS_DEPLOYMENT_TARGET=9.0 9 | INFOPLIST_FILE = Sources/Info.plist 10 | -------------------------------------------------------------------------------- /NativeBaseSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SampleRecipe/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Sources/Recipes/DescriptiveAlertRecipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DescriptiveAlertRecipe.swift 3 | // TVMLKitchen 4 | // 5 | // Created by Stephen Radford on 15/03/2016. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | open class DescriptiveAlertRecipe: AlertRecipe { 10 | } 11 | -------------------------------------------------------------------------------- /TVMLKitchenTests/ExpectedAlertRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | TVMLKitchen 10 | bra bra bra 11 | 12 | 13 | -------------------------------------------------------------------------------- /MoviePlaybackSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVMLKitchenTests/SampleTemplateRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | Search 3 | 4 | 5 |
6 | Results 7 |
8 |
9 |
10 |
-------------------------------------------------------------------------------- /TVMLKitchenTests/ExpectedTabBar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | abcdef 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SampleRecipe/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /SampleRecipe/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVMLKitchenTests/ExpectedLoadingRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | Loading... 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NativeBaseSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVMLKitchenTests/ExpectedSearchRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | Search 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SampleRecipe/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "9.0", 8 | "scale" : "1x" 9 | } 10 | ], 11 | "info" : { 12 | "version" : 1, 13 | "author" : "xcode" 14 | } 15 | } -------------------------------------------------------------------------------- /MoviePlaybackSample/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "9.0", 8 | "scale" : "1x" 9 | } 10 | ], 11 | "info" : { 12 | "version" : 1, 13 | "author" : "xcode" 14 | } 15 | } -------------------------------------------------------------------------------- /NativeBaseSample/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "9.0", 8 | "scale" : "1x" 9 | } 10 | ], 11 | "info" : { 12 | "version" : 1, 13 | "author" : "xcode" 14 | } 15 | } -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: # paths to ignore during linting. overridden by `included`. 2 | - Carthage 3 | - library/ 4 | - SampleRecipe/ 5 | - NativeBaseSample/ 6 | disabled_rules: 7 | - opening_brace 8 | - statement_position 9 | line_length: 110 10 | type_body_length: 11 | warning: 300 12 | error: 400 13 | variable_name: 14 | min_length: 0 15 | max_length: 30 16 | excluded: 17 | - id 18 | - URL 19 | -------------------------------------------------------------------------------- /SampleRecipe/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 | -------------------------------------------------------------------------------- /SampleRecipe/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 | -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | -------------------------------------------------------------------------------- /NativeBaseSample/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 | -------------------------------------------------------------------------------- /NativeBaseSample/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 | -------------------------------------------------------------------------------- /script/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o pipefail 3 | xcodebuild "-project" "TVMLKitchen.xcodeproj" "-scheme" "TVMLKitchen" "clean" "test" "-destination" "platform=tvOS Simulator,name=Apple TV 1080p,OS=latest" | xcpretty "--color" 4 | st=$? 5 | if [ $st -ne 0 ];then 6 | exit $st 7 | fi 8 | #xcodebuild "-project" "TVMLKitchen.xcodeproj" "-scheme" "SampleRecipeUITests" "test" "-destination" "platform=tvOS Simulator,name=Apple TV 1080p,OS=latest" | xcpretty "--color" 9 | -------------------------------------------------------------------------------- /TVMLKitchenTests/LoadingRecipeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingRecipeTests.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/27/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TVMLKitchen 11 | 12 | class LoadingRecipeTests: XCTestCase { 13 | func testLoadingRecipe() { 14 | let loading = LoadingRecipe() 15 | testTemplateRecipe(loading, expectedFileName: "ExpectedLoadingRecipe") 16 | } 17 | } -------------------------------------------------------------------------------- /TVMLKitchenTests/SearchRecipeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRecipeTests.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TVMLKitchen 11 | 12 | class SearchRecipeTests: XCTestCase { 13 | 14 | func testSearchRecipe() { 15 | let search = SearchRecipe() 16 | testTemplateRecipe(search, expectedFileName: "ExpectedSearchRecipe") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TVMLKitchenTests/ExpectedSampleTemplateRecipe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | Search 10 | 11 | 12 |
13 | Results 14 |
15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /TVMLKitchenTests/AlertRecipeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertRecipeTests.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TVMLKitchen 11 | 12 | class AlertRecipeTests: XCTestCase { 13 | func testAlertRecipe() { 14 | let alert = AlertRecipe(title: "TVMLKitchen", description: "bra bra bra") 15 | testTemplateRecipe(alert, expectedFileName: "ExpectedAlertRecipe") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TVMLKitchen.h: -------------------------------------------------------------------------------- 1 | // 2 | // TVMLKitchen.h 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TVMLKitchen. 12 | FOUNDATION_EXPORT double TVMLKitchenVersionNumber; 13 | 14 | //! Project version string for TVMLKitchen. 15 | FOUNDATION_EXPORT const unsigned char TVMLKitchenVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /TVMLKitchenTests/TabBarTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarTests.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TVMLKitchen 11 | 12 | struct MyTab: TabItem { 13 | let title = "abcdef" 14 | func handler() { } 15 | } 16 | 17 | class TabBarTests: XCTestCase { 18 | 19 | func testTabBar() { 20 | let tabbar = KitchenTabBar(items: [ 21 | MyTab() 22 | ]) 23 | testTemplateRecipe(tabbar, expectedFileName: "ExpectedTabBar") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Recipes/Loading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loading.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/26/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class LoadingRecipe: TemplateRecipeType { 12 | 13 | public typealias Theme = EmptyTheme 14 | open var theme: Theme = EmptyTheme() 15 | 16 | let message: String 17 | 18 | public init(message: String = "Loading...") { 19 | self.message = message 20 | } 21 | 22 | open var replacementDictionary: [String : String] { 23 | return ["LOADING": message] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SampleRecipe/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" : "1920x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image.imageset", 19 | "role" : "top-shelf-image" 20 | } 21 | ], 22 | "info" : { 23 | "version" : 1, 24 | "author" : "xcode" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NativeBaseSample/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" : "1920x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image.imageset", 19 | "role" : "top-shelf-image" 20 | } 21 | ], 22 | "info" : { 23 | "version" : 1, 24 | "author" : "xcode" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Documentation/DevelopersGuide.md: -------------------------------------------------------------------------------- 1 | ## Inject native code into TVML(javascript) context 2 | ``` 3 | cookbook.evaluateAppJavaScriptInContext = {appController, jsContext in 4 | /// set Exception handler 5 | /// called on JS error 6 | jsContext.exceptionHandler = {context, value in 7 | debugPrint(context) 8 | debugPrint(value) 9 | assertionFailure("You got JS error. Check your javascript code.") 10 | } 11 | 12 | /// - SeeAlso: http://nshipster.com/javascriptcore/ 13 | /// Inject native code block named 'debug'. 14 | let consoleLog: @convention(block) String -> Void = { message in 15 | print(message) 16 | } 17 | jsContext.setObject(unsafeBitCast(consoleLog, AnyObject.self), 18 | forKeyedSubscript: "debug") 19 | } 20 | ``` 21 | 22 | TBD... 23 | -------------------------------------------------------------------------------- /TVMLKitchenTests/RecipeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecipeTests.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TVMLKitchen 11 | 12 | struct SampleTemplateRecipe: TemplateRecipeType { 13 | let theme = EmptyTheme() 14 | /// Presentation type is defined in the recipe to keep things consistent. 15 | var presentationType = PresentationType.search 16 | 17 | static var bundle: Bundle { 18 | return Bundle(for: RecipeTests.self) 19 | } 20 | } 21 | 22 | class RecipeTests: XCTestCase { 23 | func testTemplateRecipeType() { 24 | let recipe = SampleTemplateRecipe() 25 | testTemplateRecipe(recipe, expectedFileName: "ExpectedSampleTemplateRecipe") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TVMLKitchen.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "TVMLKitchen" 3 | s.version = "1.0.1" 4 | s.summary = "Swifty TVML template manager with or without client-server" 5 | s.description = <<-DESC 6 | TVMLKitchen helps to manage your TVML with or without additional client-server. 7 | DESC 8 | 9 | s.homepage = "https://github.com/toshi0383/TVMLKitchen" 10 | s.license = 'MIT' 11 | s.author = { "Toshihiro Suzuki" => "t.suzuki326@gmail.com" } 12 | s.source = { :git => "https://github.com/toshi0383/TVMLKitchen.git", :tag => s.version.to_s } 13 | 14 | s.platform = :tvos, '9.0' 15 | s.requires_arc = true 16 | 17 | s.source_files = 'Sources/**/*', 'library/**/*' 18 | s.resources = 'Sources/**/*.{js,xml}' 19 | end 20 | -------------------------------------------------------------------------------- /TVMLKitchenTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SampleRecipeUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /TVMLKitchenTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TVMLKitchen 11 | 12 | extension String { 13 | var trim: String { 14 | return self.replacingOccurrences(of: " ", with: "") 15 | } 16 | } 17 | 18 | func testTemplateRecipe(_ recipe: R, expectedFileName name: String) { 19 | let target = recipe.xmlString.trim 20 | let bundle = Bundle(for: RecipeTests.self) 21 | let url = bundle.url(forResource: name, withExtension: "xml")! 22 | // swiftlint:disable:next force_try 23 | let expected = try! String(contentsOf: url).trim 24 | XCTAssert(target == expected, "\n===result===:\n\(target)\n\n===expected===:\n\(expected)\n\n=======") 25 | } 26 | -------------------------------------------------------------------------------- /MoviePlaybackSample/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 | } 33 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SampleRecipe/Oneup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | WWDC Roadtrip 8 | 9 | San Francisco 10 | June 8, 2015 11 | 12 | 13 | 14 | 15 | Beach time 16 | 17 | Santa Cruz 18 | May 1, 2015 19 | 20 | 21 | 22 | 23 | Spaced out 24 | 25 | Space station 26 | July 15, 2015 27 | 28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /Sources/PresentationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationType.swift 3 | // TVMLKitchen 4 | // 5 | // Created by Stephen Radford on 14/03/2016. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | public enum PresentationType: Int { 10 | case `default` = 0 11 | case modal = 1 12 | case tab = 2 13 | case search = 3 14 | /// Mix of `.Tab` and `.Search`. 15 | /// Expected to be used when presenting SearchRecipe as a TabItem. 16 | case tabSearch = 4 17 | case defaultWithLoadingIndicator = 6 18 | 19 | init(string: String) { 20 | switch string.lowercased() { 21 | case "modal": 22 | self = .modal 23 | case "tab": 24 | self = .tab 25 | case "search": 26 | self = .search 27 | case "tabsearch": 28 | self = .tabSearch 29 | case "defaultwithloadingindicator": 30 | self = .defaultWithLoadingIndicator 31 | default: 32 | self = .default 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/a2f49b7b14276e68c40fcd55345eb3013d8a3375/swift.gitignore 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | 22 | ## Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | 32 | # CocoaPods 33 | # 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 37 | # 38 | # Pods/ 39 | 40 | # Carthage 41 | Carthage/ 42 | 43 | 44 | **/.DS_Store 45 | -------------------------------------------------------------------------------- /Documentation/HandlingActionsAndErrors.md: -------------------------------------------------------------------------------- 1 | # Handling Action and Errors 2 | 3 | ## Add error handlers 4 | ``` 5 | cookbook.onError = { error in 6 | let title = "Error Launching Application" 7 | let message = error.localizedDescription 8 | let alertController = UIAlertController(title: title, message: message, preferredStyle:.Alert ) 9 | 10 | Kitchen.navigationController.presentViewController(alertController, animated: true) { } 11 | } 12 | ``` 13 | 14 | ## Handling Actions 15 | You can set `actionID` and `playActionID` attributes in your focusable elements. (e.g. `lockup` or `button` SeeAlso: https://forums.developer.apple.com/thread/17704 ) Kitchen receives Select or Play events, then fires `actionIDHandler` or `playActionHandler` if exists. 16 | 17 | ``` 18 | 19 | ``` 20 | 21 | ``` 22 | cookbook.actionIDHandler = { actionID in 23 | print(actionID) 24 | } 25 | cookbook.playActionIDHandler = {actionID in 26 | print(actionID) 27 | } 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /SampleRecipeUITests/SampleRecipeUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleRecipeUITests.swift 3 | // SampleRecipeUITests 4 | // 5 | // Created by toshi0383 on 12/31/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class SampleRecipeUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | continueAfterFailure = true 16 | XCUIApplication().launch() 17 | } 18 | 19 | func testUI() { 20 | // Here we want to verify that at least initial UI loads correctly. 21 | let xmlFileFromMainBundleButton = XCUIApplication().buttons["XML File from main bundle"] 22 | XCTAssert(xmlFileFromMainBundleButton.hasFocus) 23 | XCUIRemote.shared().press(.select) 24 | XCTAssert(xmlFileFromMainBundleButton.hasFocus) 25 | XCUIRemote.shared().press(.select) 26 | XCUIRemote.shared().press(.menu) 27 | XCTAssert(xmlFileFromMainBundleButton.hasFocus) 28 | XCUIRemote.shared().press(.down) 29 | XCUIRemote.shared().press(.down) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SampleRecipe/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UIRequiredDeviceCapabilities 31 | 32 | arm64 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /MoviePlaybackSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | UIUserInterfaceStyle 28 | Automatic 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 toshi0383 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 | 23 | -------------------------------------------------------------------------------- /NativeBaseSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | arm64 30 | 31 | NSAppTransportSecurity 32 | 33 | NSAllowsArbitraryLoads 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /SampleRecipe/MySearchRecipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MySearchRecipe.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TVMLKitchen 11 | 12 | class MySearchRecipe: SearchRecipe { 13 | override func filterSearchText(_ text: String, callback: @escaping ((String) -> Void)) { 14 | let titles = [ 15 | "hello", 16 | "yellow" 17 | ] 18 | let imageUrl = "https://i.warosu.org/data/cgl/img/0075/02/1397765684315.png" 19 | let width = 350, height = 520 20 | var results = "" 21 | for title in titles { 22 | results += "" 23 | results += "" 24 | results += "\(title)" 25 | results += "" 26 | } 27 | let url = SearchRecipe.bundle.url(forResource: "SearchResult", withExtension: "xml")! 28 | let resultBase = try! String(contentsOf: url) 29 | let result = resultBase.replacingOccurrences(of: "{{results}}", with: results) 30 | 31 | callback(result) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Recipes/SearchRecipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRecipe.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// SearchRecipe 12 | /// 13 | /// Usage: 14 | /// 1. Subclass and override filterSearchText method. 15 | /// 2. Instantiate with preferred PresentationType. 16 | open class SearchRecipe: SearchRecipeType { 17 | 18 | open let theme = EmptyTheme() 19 | 20 | /// Presentation type is defined in the recipe to keep things consistent. 21 | open var presentationType = PresentationType.search 22 | 23 | public init(type: PresentationType = .search) { 24 | self.presentationType = type 25 | } 26 | 27 | open var templateFileName: String { 28 | // Always use SearchRecipe.xml unless this property is overridden. 29 | return "SearchRecipe" 30 | } 31 | 32 | open func filterSearchText(_ text: String, callback: @escaping ((String) -> Void)) { 33 | fatalError("Must be overridden.") 34 | } 35 | 36 | open var noData: String { 37 | return "
No Results
" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SampleRecipe/SampleResource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleResource.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // swiftlint:disable line_length 12 | struct Sample { 13 | static let title = "TVMLKitchen" 14 | static let description = "Swift is a high-performance system programming language. It has a clean and modern syntax, offers seamless access to existing C and Objective-C code and frameworks, and is memory safe by default." 15 | static let tvmlUrl = "https://raw.githubusercontent.com/toshi0383/TVMLKitchen/swift2.2/SampleRecipe/Catalog.xml" 16 | static var tvmlString: String { 17 | return XMLString.Catalog.description 18 | } 19 | } 20 | // swiftlint:enable line_length 21 | 22 | enum XMLString: String { 23 | case Catalog 24 | } 25 | 26 | extension XMLString: CustomStringConvertible { 27 | var description: String { 28 | switch self { 29 | case .Catalog: 30 | let path = Bundle.main.path(forResource: "Oneup", ofType: "xml")! 31 | // swiftlint:disable force_try 32 | return try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String 33 | // swiftlint:enable force_try 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Cookbook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cookbook.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/21/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ResponseObjectHandler = (HTTPURLResponse) -> Bool 12 | 13 | open class Cookbook { 14 | 15 | /// launchOptions 16 | internal var launchOptions: [AnyHashable: Any]? 17 | /// inject functions or a exceptionHandler into JSContext 18 | open var evaluateAppJavaScriptInContext: JavaScriptEvaluationHandler? 19 | /// handles "select" event 20 | open var actionIDHandler: KitchenActionIDHandler? 21 | /// handles "play" event 22 | open var playActionIDHandler: KitchenActionIDHandler? 23 | /// handles "tabChanged" event 24 | open var tabChangedHandler: KitchenTabItemHandler? 25 | 26 | /// Subclass object of SearchRecipe. 27 | /// Required when presenting SearchRecipe somewhere. 28 | internal var searchRecipe: SearchRecipe? { 29 | didSet { 30 | if let recipe = searchRecipe , type(of: recipe) == SearchRecipe.self { 31 | fatalError("searchRecipe must be subclassed.") 32 | } 33 | } 34 | } 35 | 36 | /// error handler that gets called when any errors occured 37 | /// in Kitchen(both JS and Swift context) 38 | open var onError: KitchenErrorHandler? 39 | open var httpHeaders: [String: String] = [:] 40 | open var responseObjectHandler: ResponseObjectHandler? 41 | 42 | /// - parameter launchOptions: launchOptions 43 | public init(launchOptions: [AnyHashable: Any]?) { 44 | self.launchOptions = launchOptions 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MoviePlaybackSample/sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | Title 22 | Series 23 | 24 | 39 minutes 25 | 26 | 27 |
28 |
29 | This is a description 30 |
31 |
32 | 33 | 1 34 | Title 35 | 04:10 36 | 37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /Sources/Recipes/AlertRecipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertRecipe.swift 3 | // TVMLKitchen 4 | // 5 | // Created by Stephen Radford on 14/03/2016. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | public struct AlertButton { 10 | 11 | let title: String 12 | let actionID: String? 13 | 14 | public init(title: String, actionID: String? = nil) { 15 | self.title = title 16 | self.actionID = actionID 17 | } 18 | 19 | } 20 | 21 | open class AlertRecipe: TemplateRecipeType { 22 | 23 | open let theme = EmptyTheme() 24 | open let presentationType: PresentationType 25 | open let title: String 26 | open let description: String 27 | open let buttons: [AlertButton] 28 | 29 | public init(title: String, description: String, 30 | buttons: [AlertButton] = [], 31 | presentationType: PresentationType = .modal) { 32 | self.title = title 33 | self.description = description 34 | self.buttons = buttons 35 | self.presentationType = presentationType 36 | } 37 | 38 | fileprivate var buttonString: String { 39 | let mapped: [String] = buttons.map { 40 | var string = ($0.actionID != nil) ? "" 43 | return string 44 | } 45 | return mapped.joined(separator: "") 46 | } 47 | 48 | open var replacementDictionary: [String: String] { 49 | return [ 50 | "TITLE": title, 51 | "DESCRIPTION": description, 52 | "BUTTONS": buttonString 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Documentation/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Showing a Template from main bundle 4 | 5 | 1. Put your Sample.xml to your app's main bundle. 6 | 7 | 2. Prepare your Kitchen in AppDelegate's `didFinishLaunchingWithOptions:`. 8 | ``` 9 | let cookbook = Cookbook(launchOptions: launchOptions) 10 | Kitchen.prepare(cookbook) 11 | ``` 12 | 13 | 3. Launch the template from anywhere. 14 | ``` 15 | Kitchen.serve(xmlFile: "Sample.xml") 16 | ``` 17 | 18 | ## Showing a Template from client-server 19 | 20 |
    21 |
  1. Got TVML server ? Just pass the URL String and you're good to go.

    22 |
    Kitchen.serve(urlString: "https://raw.githubusercontent.com/toshi0383/TVMLKitchen/master/SampleRecipe/Catalog.xml")
    23 | 24 |
25 | 26 | ## Open Other TVML from TVML 27 | 28 | Set URL to `template` attributes of focusable element. Kitchen will send asynchronous request and present TVML. You can specify preferred `presentationType` too. Note that if `actionID` present, these attributes are ignored. 29 | 30 | ``` 31 | 35 | ``` 36 | 37 | ## Presentation Styles 38 | 39 | There are currently three presentation styles that can be used when serving views: Default, Modal and Tab. The default style acts as a "Push" and will change the current view. Modal will overlay the new view atop the existing view and is commonly used for alerts. Tab is only to be used when defining the first view in a tabcontroller. 40 | 41 | ````swift 42 | Kitchen.serve(xmlFile: "Sample.xml") 43 | Kitchen.serve(xmlFile: "Sample.xml", type: .Default) 44 | Kitchen.serve(xmlFile: "Sample.xml", type: .Modal) 45 | Kitchen.serve(xmlFile: "Sample.xml", type: .Tab) 46 | ```` 47 | -------------------------------------------------------------------------------- /Sources/KitchenTabBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KitchenTabBar.swift 3 | // TVMLKitchen 4 | // 5 | // Created by Stephen Radford on 15/03/2016. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | public protocol TabItem { 10 | 11 | /// The title that will be displayed on the tab bar. 12 | var title: String { get } 13 | 14 | /** 15 | This handler will be called whenever the focus changes to it. 16 | */ 17 | func handler() 18 | 19 | } 20 | 21 | public struct KitchenTabBar: TemplateRecipeType { 22 | 23 | public let theme = EmptyTheme() 24 | 25 | /// The items that are displayed in the tab bar. 26 | /// The `displayTabBar` method will automatically be called. 27 | public var items: [TabItem]! { 28 | didSet { 29 | displayTabBar() 30 | } 31 | } 32 | 33 | /// for UnitTesting use. 34 | /// - parameter items: 35 | public init(items: [TabItem]? = nil) { 36 | self.items = items 37 | } 38 | 39 | /// Constructed string from the `items` array. 40 | public var template: String { 41 | let url = KitchenTabBar.bundle.url(forResource: templateFileName, withExtension: "xml")! 42 | // swiftlint:disable:next force_try 43 | let xml = try! String(contentsOf: url) 44 | var string = "" 45 | for (index, item) in items.enumerated() { 46 | string += "\n" 47 | string += "\(item.title)\n" 48 | string += "\n" 49 | } 50 | return xml.replacingOccurrences(of: "{{menuItems}}", with: string) 51 | } 52 | 53 | /** 54 | Display the tab bar using the generated `xmlString`. 55 | */ 56 | func displayTabBar() { 57 | openTVMLTemplateFromXMLString(xmlString) 58 | } 59 | 60 | /** 61 | Called whenever the tab view changes. 62 | The handler defined by the relevant `TabItem` will automatically be called. 63 | 64 | - parameter index: The new selected index 65 | */ 66 | func tabChanged(_ index: Int) { 67 | items[index].handler() 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /TVMLKitchen.xcodeproj/xcshareddata/xcschemes/TVMLKitchenTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /TVMLKitchen.xcodeproj/xcshareddata/xcschemes/SampleRecipeUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Documentation/Recipes.md: -------------------------------------------------------------------------------- 1 | # Kitchen Recipes 2 | Though TVML is static xmls, we can generate TVML dynamically by defining **Recipe**. 3 | 4 | ## Available Recipes 5 | 6 | - [x] Alert with button handler 7 | - [x] Descriptive Alert with button handler 8 | - [x] Search 9 | - [x] TabBar 10 | 11 | You can create your own Recipe by conforming to `RecipeType` protocol. 12 | 13 | ## AlertRecipe 14 | ``` 15 | let alert = AlertRecipe( 16 | title: Sample.title, 17 | description: Sample.description) 18 | ) 19 | Kitchen.serve(recipe: alert) 20 | ``` 21 | 22 | ## Tab Controller 23 | 24 | Should you wish to use tabs within your application you can use `KitchenTabBar` recipe. First, create a `TabItem` struct with a title and a `handler` method. The `handler` method will be called every time the tab becomes active. 25 | 26 | **Note:** The `PresentationType` for initial view should always be set to `.Tab`. 27 | 28 | ````swift 29 | struct MoviesTab: TabItem { 30 | 31 | let title = "Movies" 32 | 33 | func handler() { 34 | Kitchen.serve(xmlFile: "Sample.xml", type: .Tab) 35 | } 36 | 37 | } 38 | ```` 39 | 40 | Present tabbar using `serve(recipe:)` method. 41 | 42 | ````swift 43 | let tabbar = KitchenTabBar(items:[ 44 | MoviesTab(), 45 | MusicsTab() 46 | ]) 47 | Kitchen.serve(recipe: tabbar) 48 | ```` 49 | 50 | Reload tab using `reloadTab(atIndex:_:)` method. 51 | 52 | ```swift 53 | // reload with xmlFile 54 | Kitchen.reloadTab(atIndex: 0, xmlFile: "Oneup.xml") 55 | 56 | // reload with xmlString 57 | Kitchen.reloadTab(atIndex: 0, urlString: Sample.tvmlUrl) 58 | 59 | // reload with Recipe 60 | let search = MySearchRecipe() 61 | Kitchen.reloadTab(atIndex: 0, recipe: search) 62 | ``` 63 | 64 | ## SearchRecipe 65 | SearchRecipe supports dynamic view manipulation. 66 | 67 | #### Configuring SearchRecipe 68 | Subclass `SearchRecipe` and override `filterSearchText` method. 69 | SeeAlso: [SampleRecipe/MySearchRecipe.swift](https://github.com/toshi0383/TVMLKitchen/blob/master/SampleRecipe/MySearchRecipe.swift), [SearchResult.xml](https://github.com/toshi0383/TVMLKitchen/blob/master/Sources/Templates/SearchResult.xml) 70 | 71 | #### SearchRecipe as TabItem 72 | Use `PresentationType.TabSearch`. This will create keyboard observer in addition to `.Tab` behavior. 73 | ``` 74 | struct SearchTab: TabItem { 75 | let title = "Search" 76 | func handler() { 77 | let search = MySearchRecipe(type: .TabSearch) 78 | Kitchen.serve(recipe: search) 79 | } 80 | } 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TVMLKitchen😋🍴 [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/Carthage/Carthage/master/LICENSE.md) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![CocoaPods](https://img.shields.io/cocoapods/v/TVMLKitchen.svg)]() [![CocoaPods](https://img.shields.io/cocoapods/p/TVMLKitchen.svg)]() [![Build Status](https://www.bitrise.io/app/de994b854e5c425f.svg?token=GZp-KU8RDjmewA2Hdj27fQ)](https://www.bitrise.io/app/de994b854e5c425f) 2 | 3 | TVMLKitchen helps to manage your TVML **with or without additional client-server**. 4 | 5 | # Requirements 6 | - Swift3.0 7 | - tvOS 9.0+ 8 | 9 | Use 0.9.6 for Swift2.2. 10 | Swift2.3 is not supported. Feel free to send PR. 11 | 12 | # What's TVML? 13 | Please refer to [Apple's Documentation](https://developer.apple.com/library/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/). 14 | It's a markup language which can be used only on tvOS. 15 | TVML makes it easy to build awesome apps for tvOS. 16 | 17 | # Why ? 18 | 19 | TVML is easy, but TVJS is not really. 20 | With TVMLKitchen, loading a TVML view is in this short. 21 | 22 | ``` 23 | Kitchen.serve(xmlFile: "Catalog.xml") 24 | ``` 25 | 26 | You don't have to write any JavaScript code at all! 27 | 28 | Kitchen automatically looks for the xmlFile in your Main Bundle, parse it, then finally pushes it to navigationController. 29 | Please refer to the [Documentation](./Documentation) for more information. 30 | 31 | # Available Features 32 | - [x] Load TVML from URL. 33 | - [x] Load TVML from raw XML String. 34 | - [x] XML syntax validation API 35 | - [x] Multi UIWindow Support 36 | - [x] TVML *Recipe* Protocol 37 | 38 | # Examples 39 | - TVJS Base Hybrid App (Demo: [SampleRecipe](./SampleRecipe)) 40 | - UIKit Base Hybrid App (Demo: [NativeBaseSample](./NativeBaseSample)) 41 | 42 | # Installation 43 | 44 | ## Carthage 45 | Put this to your Cartfile, 46 | ``` 47 | github "toshi0383/TVMLKitchen" 48 | ``` 49 | 50 | Follow the instruction in [carthage's Getting Started section](https://github.com/Carthage/Carthage#getting-started). 51 | 52 | ## Cocoapods 53 | Add the following to your Podfile 54 | ``` 55 | pod 'TVMLKitchen' 56 | ``` 57 | 58 | # References 59 | For implementation details, my slide is available. 60 | [TVML + Native = Hybrid](https://speakerdeck.com/toshi0383/tvml-plus-native-equals-hybrid) 61 | 62 | # Contribution 63 | Any contribution is welcomed🎉 64 | -------------------------------------------------------------------------------- /NativeBaseSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NativeBaseSample 4 | // 5 | // Created by toshi0383 on 8/26/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TVMLKitchen 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | Kitchen.prepare(Cookbook(launchOptions: launchOptions)) 20 | window?.backgroundColor = .black 21 | Kitchen.window.backgroundColor = .black 22 | Kitchen.navigationController.view.backgroundColor = .black 23 | return true 24 | } 25 | 26 | func applicationWillResignActive(_ application: UIApplication) { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | func applicationWillEnterForeground(_ application: UIApplication) { 37 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) { 41 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 42 | } 43 | 44 | func applicationWillTerminate(_ application: UIApplication) { 45 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 46 | } 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Sources/Recipes/RecipeTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecipeTheme.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 3/19/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: ThemeType 12 | public protocol ThemeType { 13 | var backgroundColor: String {get} 14 | var color: String {get} 15 | var highlightBackgroundColor: String {get} 16 | var highlightTextColor: String {get} 17 | var style: String {get} 18 | init() 19 | } 20 | 21 | extension ThemeType { 22 | 23 | public var backgroundColor: String { 24 | return "transparent" 25 | } 26 | 27 | public var color: String { 28 | return "rgb(0, 0, 0)" 29 | } 30 | 31 | public var highlightBackgroundColor: String { 32 | return "rgb(255, 255, 255)" 33 | } 34 | 35 | public var highlightTextColor: String { 36 | return "rgb(0, 0, 0)" 37 | } 38 | 39 | public var style: String { 40 | return parse(styleTemplate) 41 | } 42 | 43 | fileprivate var styleTemplate: String { 44 | return "* { background-color: {{__kitchenBackgroundColor}};" 45 | + " color: {{__kitchenColor}};" 46 | + " tv-highlight-color:{{__kitchenHighlightBackgroundColor}};" 47 | + "}" 48 | + ".kitchen_highlight_bg { background-color:transparent;" 49 | + " tv-highlight-color:{{__kitchenHighlightTextColor}}; }" 50 | + ".kitchen_no_highlight_bg { background-color:transparent;" 51 | + " tv-highlight-color:{{__kitchenHighlightBackgroundColor}}; }" 52 | } 53 | 54 | fileprivate func parse(_ xml: String) -> String { 55 | var result = xml 56 | result = result.replacingOccurrences( 57 | of: "{{__kitchenBackgroundColor}}", with: backgroundColor 58 | ) 59 | result = result.replacingOccurrences( 60 | of: "{{__kitchenHighlightBackgroundColor}}", with: highlightBackgroundColor 61 | ) 62 | result = result.replacingOccurrences( 63 | of: "{{__kitchenHighlightTextColor}}", with: highlightTextColor 64 | ) 65 | result = result.replacingOccurrences( 66 | of: "{{__kitchenColor}}", with: color 67 | ) 68 | return result 69 | } 70 | 71 | } 72 | 73 | public struct EmptyTheme: ThemeType { 74 | public let style: String = "" 75 | public init() {} 76 | } 77 | 78 | public struct DefaultTheme: ThemeType { 79 | public init() {} 80 | } 81 | 82 | public struct BlackTheme: ThemeType { 83 | public let backgroundColor: String = "rgb(0, 0, 0)" 84 | public let color: String = "rgb(255, 255, 255)" 85 | public init() {} 86 | } 87 | -------------------------------------------------------------------------------- /NativeBaseSample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // NativeBaseSample 4 | // 5 | // Created by toshi0383 on 8/26/16. 6 | // Copyright © 2016 toshi0383. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TVMLKitchen 11 | 12 | class ViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | navigationController?.view.backgroundColor = .black 17 | view.backgroundColor = .black 18 | } 19 | 20 | @IBAction func urlString(_ sender: AnyObject!) { 21 | let appdelegateWindow = (UIApplication.shared.delegate as! AppDelegate).window! 22 | Kitchen.serve( 23 | urlString: Sample.tvmlUrl, 24 | redirectWindow: appdelegateWindow, 25 | 26 | // - Note: This is what Kitchen would do when animatedWindowTransition is true. 27 | kitchenWindowWillBecomeVisible: { 28 | Kitchen.window.alpha = 0.0 29 | UIView.animate( 30 | withDuration: 0.3, 31 | animations: { 32 | Kitchen.window.alpha = 1.0 33 | }, 34 | completion: { 35 | _ in 36 | appdelegateWindow.alpha = 0.0 37 | } 38 | ) 39 | }, 40 | willRedirectToWindow: { 41 | appdelegateWindow.alpha = 0.0 42 | UIView.animate( 43 | withDuration: 0.3, 44 | animations: { 45 | appdelegateWindow.alpha = 1.0 46 | }, 47 | completion: { 48 | _ in 49 | Kitchen.window.alpha = 0.0 50 | } 51 | ) 52 | } 53 | ) 54 | } 55 | @IBAction func xmlString(_ sender: AnyObject!) { 56 | let appdelegateWindow = (UIApplication.shared.delegate as! AppDelegate).window! 57 | Kitchen.window.alpha = 1.0 58 | Kitchen.serve( 59 | xmlString: Sample.tvmlString, 60 | redirectWindow: appdelegateWindow, 61 | animatedWindowTransition: true 62 | ) 63 | 64 | let seconds: Double = 2.0 65 | let nanoSeconds = Int64(seconds * Double(NSEC_PER_SEC)) 66 | let time = DispatchTime.now() + Double(nanoSeconds) / Double(NSEC_PER_SEC) 67 | DispatchQueue.main.asyncAfter(deadline: time) { 68 | self.reset() 69 | } 70 | } 71 | @IBAction func urlStringError() { 72 | let appdelegateWindow = (UIApplication.shared.delegate as! AppDelegate).window! 73 | Kitchen.serve( 74 | urlString: "tvmlkitchen://helloworld.com/helloworld", 75 | redirectWindow: appdelegateWindow 76 | ) 77 | } 78 | @IBAction func reset() { 79 | Kitchen.navigationController.popToRootViewController(animated: false) 80 | Kitchen.navigationController.setViewControllers([], animated: false) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SampleRecipe/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SampleRecipe 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TVMLKitchen 11 | import JavaScriptCore 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate, TVApplicationControllerDelegate { 15 | 16 | func application(_ application: UIApplication, 17 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool 18 | { 19 | _ = prepareMyKitchen(launchOptions, delegate: self) 20 | return true 21 | } 22 | 23 | // Custom TVApplicationControllerDelegate 24 | func appController(_ appController: TVApplicationController, evaluateAppJavaScriptIn jsContext: JSContext) { 25 | print("\(#function): hello") 26 | } 27 | 28 | } 29 | 30 | private func prepareMyKitchen(_ launchOptions: [AnyHashable: Any]?, delegate: TVApplicationControllerDelegate) -> Bool 31 | { 32 | let cookbook = Cookbook(launchOptions: launchOptions) 33 | cookbook.evaluateAppJavaScriptInContext = {appController, jsContext in 34 | /// set Exception handler 35 | /// called on JS error 36 | jsContext.exceptionHandler = {context, value in 37 | #if DEBUG 38 | debugPrint(context) 39 | debugPrint(value) 40 | #endif 41 | assertionFailure("You got JS error. Check your javascript code.") 42 | } 43 | 44 | // - SeeAlso: http://nshipster.com/javascriptcore/ 45 | 46 | } 47 | cookbook.actionIDHandler = { 48 | actionID in 49 | print("actionID: \(actionID)") 50 | } 51 | cookbook.playActionIDHandler = { 52 | playActionID in 53 | print("playActionID: \(playActionID)") 54 | } 55 | cookbook.httpHeaders = [ 56 | "hello": "world" 57 | ] 58 | 59 | cookbook.responseObjectHandler = { response in 60 | /// Save cookies 61 | if let fields = response.allHeaderFields as? [String: String], 62 | let url = response.url 63 | { 64 | let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: url) 65 | for c in cookies { 66 | HTTPCookieStorage.sharedCookieStorage( 67 | forGroupContainerIdentifier: "group.jp.toshi0383.tvmlkitchen.samplerecipe").setCookie(c) 68 | } 69 | } 70 | return true 71 | } 72 | 73 | Kitchen.prepare(cookbook, delegate: delegate) 74 | openViewController() 75 | 76 | return true 77 | } 78 | 79 | struct SearchTab: TabItem { 80 | let title = "Search" 81 | func handler() { 82 | let search = MySearchRecipe(type: .tabSearch) 83 | Kitchen.serve(recipe: search) 84 | } 85 | } 86 | 87 | private func openViewController() { 88 | let sb = UIStoryboard(name: "ViewController", bundle: Bundle.main) 89 | let vc = sb.instantiateInitialViewController()! 90 | Kitchen.navigationController.pushViewController(vc, animated: true) 91 | } 92 | -------------------------------------------------------------------------------- /MoviePlaybackSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MoviePlaybackSample 4 | // 5 | // Created by toshi0383 on 2017/01/05. 6 | // Copyright © 2017 toshi0383. All rights reserved. 7 | // 8 | 9 | import AVKit 10 | import UIKit 11 | import TVMLKitchen 12 | 13 | /// Custom Action struct 14 | struct Action { 15 | private let f: (()->()) 16 | func run() { 17 | f() 18 | } 19 | /// Parse string to a function. 20 | /// Returns nil if the parameter does not match expectation. 21 | /// - parameter string: comma separated string 22 | init?(string: String) { 23 | let ss = string.components(separatedBy: ",") 24 | guard ss.count == 2 else { 25 | return nil 26 | } 27 | switch ss[0] { 28 | case "playbackURL": 29 | guard let url = URL(string: ss[1]) else { 30 | return nil 31 | } 32 | self.f = { 33 | startPlayback(url: url) 34 | } 35 | default: 36 | return nil 37 | } 38 | } 39 | } 40 | 41 | func startPlayback(url: URL) { 42 | DispatchQueue.global().async { 43 | 44 | let vc = AVPlayerViewController() 45 | 46 | /* 47 | This is extremely expensive if url points at monolithic mp4. 48 | So we're doing this in background. 49 | You should do like this if you need to handle error case. 50 | ``` 51 | let asset = AVURLAsset(url: url) 52 | asset.loadValuesAsynchronously(forKeys: [playableKey, statusKey]) { 53 | // start playback 54 | } 55 | ``` 56 | */ 57 | vc.player = AVPlayer(url: url) 58 | DispatchQueue.main.async { 59 | // Navigation should be in main thread. 60 | Kitchen.navigationController.pushViewController(vc, animated: true) 61 | } 62 | } 63 | } 64 | 65 | private func xmlString() -> String { 66 | guard let url = Bundle.main.url(forResource: "sample", withExtension: "xml"), 67 | let data = try? Data(contentsOf: url) else { 68 | fatalError() 69 | } 70 | return String(data: data, encoding: .utf8)! 71 | } 72 | 73 | @UIApplicationMain 74 | class AppDelegate: UIResponder, UIApplicationDelegate { 75 | 76 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 77 | let cookbook = Cookbook(launchOptions: launchOptions) 78 | cookbook.actionIDHandler = { 79 | actionID in 80 | // actionID can be any string. 81 | // In this sample, we're expecting comma separated string. 82 | if let action = Action(string: actionID) { 83 | action.run() 84 | } 85 | } 86 | 87 | // This callback is triggered by `play/pause` button. 88 | cookbook.playActionIDHandler = { 89 | playActionID in 90 | if let action = Action(string: playActionID) { 91 | action.run() 92 | } 93 | } 94 | Kitchen.prepare(cookbook) 95 | Kitchen.serve(xmlString: xmlString()) 96 | return true 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/TVMLBridging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVMLBridging.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 12/11/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | // MARK: - Verify 10 | class VerifyMediator { 11 | var _error = false 12 | var __verifyComplete = false 13 | var error: Bool { 14 | set(newValue) { 15 | _mediatorLock.lock() 16 | defer {_mediatorLock.unlock()} 17 | _error = newValue 18 | _verifyComplete = true 19 | } 20 | get { 21 | _mediatorLock.lock() 22 | defer {_mediatorLock.unlock()} 23 | return _error 24 | } 25 | } 26 | fileprivate var _verifyComplete: Bool { 27 | set(newValue) { 28 | _mediatorLock.lock() 29 | defer {_mediatorLock.unlock()} 30 | __verifyComplete = newValue 31 | } 32 | get { 33 | _mediatorLock.lock() 34 | defer {_mediatorLock.unlock()} 35 | return __verifyComplete 36 | } 37 | } 38 | func waitForVerifyComplete() { 39 | while !_verifyComplete { 40 | if _verifyComplete { 41 | break 42 | } 43 | } 44 | } 45 | } 46 | var verifyMediator = VerifyMediator() 47 | private let _verifyLock = NSRecursiveLock() 48 | private let _mediatorLock = NSRecursiveLock() 49 | 50 | internal func isValidXMLString(_ xmlString: String) -> Bool { 51 | _verifyLock.lock() 52 | defer {_verifyLock.unlock()} 53 | verifyMediator = VerifyMediator() 54 | let js = "verifyXMLString(`\(xmlString)`);" 55 | evaluateInTVMLContext(js) 56 | // - Note: `evaluateInTVMLContext(_:)` escapes from current scrope. 57 | // It has completion callback, but we cannot use it here 58 | // because it's not marked as rethrows. 59 | verifyMediator.waitForVerifyComplete() 60 | return !verifyMediator.error 61 | } 62 | 63 | // MARK: - Open TMVL Templates 64 | internal func openTVMLTemplateFromXMLString(_ xmlString: String, type: PresentationType = .default) { 65 | let js = "openTemplateFromXMLString(`\(xmlString)`, \(type.rawValue));" 66 | evaluateInTVMLContext(js) 67 | } 68 | 69 | internal func xmlStringFromMainBundle(_ xmlFile: String) throws -> String { 70 | let mainBundle = Bundle.main 71 | let path = mainBundle.path(forResource: xmlFile, ofType: nil)! 72 | let xmlString = try NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String 73 | let mainBundlePath = mainBundle.bundleURL.absoluteString 74 | let replaced = xmlString 75 | .replacingOccurrences(of: "((MAIN_BUNDLE_URL))", with: mainBundlePath) 76 | return replaced 77 | } 78 | 79 | internal func openTVMLTemplateFromXMLFile(_ xmlFile: String, 80 | type: PresentationType = .default) throws 81 | { 82 | let xmlString = try xmlStringFromMainBundle(xmlFile) 83 | openTVMLTemplateFromXMLString(xmlString, type: type) 84 | } 85 | 86 | // MARK: - Reload Tab 87 | internal func _reloadTab(atIndex index: Int, xmlString: String) { 88 | let js = "reloadTab(\(index), `\(xmlString)`);" 89 | evaluateInTVMLContext(js) 90 | } 91 | 92 | // MARK: - dismissTVMLModal 93 | internal func dismissTVMLModal() { 94 | let js = "dismissModal()" 95 | evaluateInTVMLContext(js) 96 | } 97 | 98 | // MARK: - Utilities 99 | private func evaluateInTVMLContext(_ js: String, completion: ((Void)->Void)? = nil) { 100 | Kitchen.appController.evaluate(inJavaScriptContext: {context in 101 | context.evaluateScript(js) 102 | }, completion: {_ in completion?()}) 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Recipes/Recipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recipe.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | // MARK: RecipeType 10 | public protocol RecipeType { 11 | 12 | /// Theme 13 | associatedtype Theme 14 | var theme: Theme {get} 15 | 16 | /// Presentation type is defined in the recipe to keep things consistent. 17 | var presentationType: PresentationType {get} 18 | 19 | /// Template part of TVML which is used to format full page xmlString. 20 | /// - SeeAlso: RecipeType.xmlString 21 | var template: String {get} 22 | 23 | /// XML string representation of whole TVML page. 24 | /// Uses RecipeType.template for template part by default. 25 | var xmlString: String {get} 26 | } 27 | 28 | public protocol TemplateRecipeType: RecipeType { 29 | 30 | /// File name of this TemplateRecipe. 31 | /// Defaults to name of the class. (No need to implement) 32 | var templateFileName: String {get} 33 | 34 | /// Custom pairs of replacement strings. 35 | /// 36 | /// e.g. 37 | /// 38 | /// ["title": "Sherlock Holmes: A Game of Shadows (2011)"] 39 | /// will modifies this `{{title}}` 40 | /// to this `Sherlock Holmes: A Game of Shadows (2011)` 41 | var replacementDictionary: [String: String] {get} 42 | 43 | /// The Bundle in which the corresponding Template file. 44 | /// Defaults to the bundle of this class/struct/enum. 45 | static var bundle: Bundle {get} 46 | } 47 | 48 | public protocol SearchRecipeType: TemplateRecipeType { 49 | 50 | /// Filter text and pass the result to callback. 51 | /// - parameter text: keyword 52 | /// - parameter callback: pass the result template xmlString. 53 | /// - SeeAlso: SampleRecipe.MySearchRecipe.swift, SearchResult.xml 54 | func filterSearchText(_ text: String, callback: @escaping ((String) -> Void)) 55 | } 56 | 57 | // MARK: - Default Implementations 58 | extension RecipeType { 59 | 60 | public var theme: ThemeType { 61 | return EmptyTheme() 62 | } 63 | 64 | public var presentationType: PresentationType { 65 | return .default 66 | } 67 | } 68 | 69 | extension TemplateRecipeType { 70 | 71 | public static var bundle: Bundle { 72 | return Kitchen.bundle() 73 | } 74 | 75 | public var replacementDictionary: [String: String] { 76 | return [:] 77 | } 78 | 79 | public var base: String { 80 | let url = Kitchen.bundle().url(forResource: "Base", withExtension: "xml")! 81 | // swiftlint:disable:next force_try 82 | let xml = try! String(contentsOf: url) 83 | return xml 84 | } 85 | 86 | public var template: String { 87 | let url = Self.bundle.url(forResource: templateFileName, withExtension: "xml")! 88 | // swiftlint:disable:next force_try 89 | let xml = try! String(contentsOf: url) 90 | return xml 91 | } 92 | 93 | public var templateFileName: String { 94 | return String(describing: type(of: self)) 95 | .components(separatedBy: ".") 96 | .last! 97 | } 98 | } 99 | 100 | extension TemplateRecipeType where Self.Theme: ThemeType { 101 | 102 | public var xmlString: String { 103 | 104 | // Start with Base 105 | var result = base 106 | 107 | // Replace template part. 108 | result = result 109 | .replacingOccurrences(of: "{{template}}", with: template) 110 | .replacingOccurrences(of: "{{style}}", with: theme.style) 111 | 112 | // Replace user-defined variables. 113 | for (k, v) in replacementDictionary { 114 | result = result.replacingOccurrences( 115 | of: "{{\(k)}}", with: v 116 | ) 117 | } 118 | return result 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /TVMLKitchen.xcodeproj/xcshareddata/xcschemes/TVMLKitchen.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /SampleRecipe/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SampleRecipe 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TVMLKitchen 11 | 12 | class MusicsTab: TabItem { 13 | fileprivate var presented = false 14 | var title: String { 15 | return "Musics" 16 | } 17 | func handler() { 18 | Kitchen.serve(xmlFile: "Catalog.xml", type: .tab) 19 | if !presented { 20 | presented = true 21 | } else { 22 | // Kitchen.reloadTab(atIndex: 0, xmlFile: "Oneup.xml") 23 | // Kitchen.reloadTab(atIndex: 0, urlString: Sample.tvmlUrl) 24 | let search = MySearchRecipe() 25 | Kitchen.reloadTab(atIndex: 0, recipe: search) 26 | } 27 | } 28 | } 29 | 30 | class MoviesTab: TabItem { 31 | var title: String { 32 | return "Movies" 33 | } 34 | func handler() { 35 | Kitchen.serve(xmlFile: "Catalog.xml", type: .tab) 36 | } 37 | } 38 | 39 | class ViewController: UIViewController { 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | } 44 | 45 | @IBAction func tabbarRecipe() { 46 | let tabbar = KitchenTabBar(items: [ 47 | MoviesTab(), 48 | MusicsTab(), 49 | SearchTab() 50 | ]) 51 | Kitchen.serve(recipe: tabbar) 52 | } 53 | 54 | @IBAction func openCatalogTemplate() { 55 | let search = MySearchRecipe() 56 | Kitchen.serve(recipe: search) 57 | } 58 | 59 | @IBAction func openXMLString(_ sender: AnyObject!) { 60 | Kitchen.serve(xmlString:XMLString.Catalog.description, type: .defaultWithLoadingIndicator) 61 | } 62 | 63 | @IBAction func openXMLFileFromMainBundle(_ sender: AnyObject!) { 64 | Kitchen.serve(xmlFile: "Catalog.xml") 65 | } 66 | 67 | @IBAction func openTemplateFromURL(_ sender: AnyObject!) { 68 | Kitchen.serve(urlString: "https://raw.githubusercontent.com/toshi0383/TVMLKitchen/master/SampleRecipe/Oneup.xml") { 69 | result in 70 | switch result { 71 | case .success(let xml): 72 | // Return the xml if you think it's valid xml. 73 | // You don't have to use `Kitchen.verify`. 74 | // the `xml` is just a utf8 String representation of HTTP response body. 75 | // It can be arbitrary data (e.g. a JSON representing error). 76 | do { 77 | try Kitchen.verify(xml) 78 | } catch { 79 | fatalError("error: \(error)") 80 | } 81 | return xml 82 | case .failure(let error): 83 | // Handle Network error 84 | fatalError("error: \(error)") 85 | } 86 | return nil 87 | } 88 | } 89 | 90 | @IBAction func descriptiveAlertRecipe(_ sender: AnyObject) { 91 | let alert = DescriptiveAlertRecipe( 92 | title: Sample.title, 93 | description: Sample.description, 94 | presentationType: .modal 95 | ) 96 | Kitchen.serve(recipe: alert) 97 | } 98 | 99 | @IBAction func alertRecipe(_ sender: AnyObject) { 100 | Kitchen.serve(recipe: AlertRecipe( 101 | title: Sample.title, 102 | description: Sample.description) 103 | ) 104 | } 105 | 106 | struct MyTheme: ThemeType { 107 | let backgroundColor: String = "rgb(0, 20, 70)" 108 | let color: String = "rgb(237, 237, 255)" 109 | init() {} 110 | } 111 | 112 | @IBAction func verify() { 113 | let valid = Sample.tvmlString 114 | let invalid = "<><<<<>>aaaa" 115 | do { 116 | try Kitchen.verify(valid) 117 | print("Verify success!!") 118 | } catch _ as KitchenError { 119 | fatalError() 120 | } catch { 121 | fatalError() 122 | } 123 | do { 124 | try Kitchen.verify(invalid) 125 | fatalError() 126 | } catch _ as KitchenError { 127 | print("Verify success!!") 128 | } catch { 129 | fatalError() 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /NativeBaseSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 36 | 44 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Sources/kitchen.js: -------------------------------------------------------------------------------- 1 | /* 2 | kitchen.js 3 | Copyright (C) 2015 toshi0383 All Rights Reserved. 4 | */ 5 | 6 | var parser; 7 | var currentTab; 8 | var tabItemDictionary; 9 | 10 | /// Presenters 11 | function defaultPresenter(xml) { 12 | dismissModal(); 13 | if(this.loadingIndicatorVisible) { 14 | navigationDocument.replaceDocument(xml, this.loadingIndicator); 15 | this.loadingIndicatorVisible = false; 16 | } else { 17 | navigationDocument.pushDocument(xml); 18 | } 19 | } 20 | 21 | function searchPresenter(xml) { 22 | this.defaultPresenter.call(this, xml); 23 | this.observeKeyboard.call(this, xml); 24 | } 25 | 26 | function observeKeyboard(xml) { 27 | var doc = xml; 28 | 29 | var searchField = doc.getElementsByTagName("searchField").item(0); 30 | var keyboard = searchField.getFeature("Keyboard"); 31 | 32 | keyboard.onTextChange = function() { 33 | var searchText = keyboard.text; 34 | buildResults(doc, searchText); 35 | } 36 | } 37 | 38 | function menuBarSearchPresenter(xml) { 39 | this.menuBarItemPresenter.call(this, xml); 40 | this.observeKeyboard.call(this, xml); 41 | } 42 | 43 | /** 44 | * Displays an XML string as a "document" within a tab. 45 | * A new document will be created for the tab if it doesn't exist. 46 | * 47 | * @param xml The XML string to be presented 48 | * 49 | * @return void 50 | */ 51 | function menuBarItemPresenter(xml) { 52 | var feature = currentTab.parentNode.getFeature("MenuBarDocument"); 53 | if (feature) { 54 | var currentDoc = feature.getDocument(currentTab); 55 | if (!currentDoc) { 56 | feature.setDocument(xml, currentTab); 57 | } 58 | } 59 | } 60 | 61 | /// Documents presented in the modal view are presented in fullscreen 62 | /// with a semi-transparent background that blurs the document below it. 63 | function modalDialogPresenter(xml) { 64 | navigationDocument.presentModal(xml); 65 | } 66 | 67 | /// Dismisses the current modal window 68 | function dismissModal() { 69 | navigationDocument.dismissModal(); 70 | } 71 | 72 | /// Event Handlers 73 | function play(event) { 74 | var ele = event.target, 75 | actionID = ele.getAttribute('playActionID'); 76 | if(actionID && typeof playActionIDHandler !== 'undefined'){ 77 | playActionIDHandler(actionID); 78 | return; 79 | } 80 | } 81 | 82 | function holdselect(event) { 83 | } 84 | 85 | function highlight(event) { 86 | } 87 | 88 | function load(event) { 89 | var self = this, 90 | ele = event.target, 91 | templateURL = ele.getAttribute("template"), 92 | presentationType = ele.getAttribute("presentationType"), 93 | actionID = ele.getAttribute("actionID"), 94 | menuIndex = ele.getAttribute("menuIndex"), 95 | tagName = ele.tagName; 96 | 97 | 98 | // If this is a menu item then trigger the relevent handler 99 | if(menuIndex){ 100 | currentTab = ele; 101 | tabItemDictionary[menuIndex] = ele; 102 | tabBarHandler(menuIndex); 103 | } 104 | 105 | if(actionID && typeof actionIDHandler !== 'undefined'){ 106 | actionIDHandler(actionID); 107 | return; 108 | } 109 | 110 | if (!templateURL) { 111 | return; 112 | } 113 | 114 | loadTemplateFromURL(templateURL, presentationType); 115 | } 116 | 117 | /// Create TVML from XML string. 118 | function makeDocument(resource) { 119 | var doc = parser.parseFromString(resource, "application/xml"); 120 | return doc; 121 | } 122 | 123 | /** 124 | Show the loading indicator for the presentation type passed from UIKit 125 | */ 126 | function showLoadingIndicatorForType(presentationType) { 127 | // guard 128 | if (presentationType == 6 && 129 | !this.loadingIndicatorVisible) { 130 | } else { 131 | return; 132 | } 133 | 134 | if (!this.loadingIndicator) { 135 | this.loadingIndicator = this.makeDocument(loadingTemplate()); 136 | } 137 | 138 | navigationDocument.pushDocument(this.loadingIndicator); 139 | this.loadingIndicatorVisible = true; 140 | } 141 | 142 | function removeLoadingIndicator() { 143 | if (this.loadingIndicatorVisible) { 144 | navigationDocument.removeDocument(this.loadingIndicator); 145 | this.loadingIndicatorVisible = false; 146 | } 147 | } 148 | 149 | function presenterForType(type) { 150 | switch(type) { 151 | case 1: 152 | return modalDialogPresenter; 153 | case 2: 154 | return menuBarItemPresenter; 155 | case 3: 156 | return searchPresenter; 157 | case 4: 158 | return menuBarSearchPresenter; 159 | default: 160 | return defaultPresenter; 161 | } 162 | } 163 | 164 | 165 | /** 166 | * @description - an example implementation of search that reacts to the 167 | * keyboard onTextChange to filter the lockup items based on the search text 168 | * @param {Document} doc - active xml document 169 | * @param {String} searchText - current text value of keyboard search input 170 | */ 171 | var buildResults = function(doc, searchText) { 172 | 173 | //Create parser and new input element 174 | var domImplementation = doc.implementation; 175 | var lsParser = domImplementation.createLSParser(1, null); 176 | var lsInput = domImplementation.createLSInput(); 177 | 178 | //set default template fragment to display no results 179 | filterSearchText(searchText, function(stringData) { 180 | lsInput.stringData = stringData 181 | //add the new input element to the document by providing the newly created input, the context, 182 | //and the operator integer flag (1 to append as child, 2 to overwrite existing children) 183 | lsParser.parseWithContext(lsInput, doc.getElementsByTagName("collectionList").item(0), 2); 184 | }); 185 | } 186 | 187 | function verifyXMLString(xmlString) { 188 | try { 189 | var doc = makeDocument(xmlString); 190 | verifyXMLStringComplete(true); 191 | } catch(error) { 192 | verifyXMLStringComplete(false); 193 | } 194 | } 195 | 196 | /// load template from Main Bundle URL 197 | /// Expected to be called from native context. 198 | function openTemplateFromXMLString(xmlString, presentationType) { 199 | showLoadingIndicatorForType(presentationType); 200 | var doc = makeDocument(xmlString); 201 | doc.addEventListener("select", load.bind(this)); 202 | doc.addEventListener("highlight", highlight.bind(this)); 203 | doc.addEventListener("holdselect", holdselect.bind(this)); 204 | doc.addEventListener("play", play.bind(this)); 205 | 206 | presenterForType(presentationType).call(this, doc); 207 | } 208 | 209 | function reloadTab(atIndex, xmlString) { 210 | var tabItemToReload = tabItemDictionary[atIndex]; 211 | var feature = currentTab.parentNode.getFeature("MenuBarDocument"); 212 | if (feature) { 213 | var doc = makeDocument(xmlString); 214 | doc.addEventListener("select", load.bind(this)); 215 | doc.addEventListener("highlight", highlight.bind(this)); 216 | doc.addEventListener("holdselect", holdselect.bind(this)); 217 | doc.addEventListener("play", play.bind(this)); 218 | feature.setDocument(doc, tabItemToReload); 219 | } 220 | } 221 | 222 | App.onLaunch = function(options) { 223 | App.options = options 224 | parser = new DOMParser(); 225 | tabItemDictionary = {}; 226 | } 227 | -------------------------------------------------------------------------------- /SampleRecipe/Catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | Title 13 | 14 | 15 |
16 |
17 | Section Header 18 |
19 | 20 | Title 1 21 | 6 22 | 23 | 24 |
25 | 29 | 30 | Title 1 31 | 32 | 36 | 37 | Title 2 38 | 39 | 43 | 44 | Title 3 45 | 46 | 49 | 50 | Title 4 51 | 52 | 53 | 54 | Title 5 55 | 56 | 57 | 58 | Title 6 59 | 60 |
61 |
62 |
63 |
64 | 65 | Title 2 66 | 8 67 | 68 | 69 |
70 | 71 | 72 | Title 1 73 | 74 | 75 | 76 | Title 2 77 | 78 | 79 | 80 | Title 3 81 | 82 | 83 | 84 | Title 4 85 | 86 | 87 | 88 | Title 5 89 | 90 | 91 | 92 | Title 6 93 | 94 | 95 | 96 | Title 7 97 | 98 | 99 | 100 | Title 8 101 | 102 |
103 |
104 |
105 |
106 | 107 | Title 3 108 | 12 109 | 110 | 111 |
112 | 113 | 114 | Title 1 115 | 116 | 117 | 118 | Title 2 119 | 120 | 121 | 122 | Title 3 123 | 124 | 125 | 126 | Title 4 127 | 128 | 129 | 130 | Title 5 131 | 132 | 133 | 134 | Title 6 135 | 136 | 137 | 138 | Title 7 139 | 140 | 141 | 142 | Title 8 143 | 144 | 145 | 146 | Title 9 147 | 148 | 149 | 150 | Title 10 151 | 152 | 153 | 154 | Title 11 155 | 156 | 157 | 158 | Title 12 159 | 160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master 2 | ##### Breaking 3 | * Remove CatalogRecipe 4 | [Toshihiro Suzuki](https://github.com/toshi0383) 5 | [#138](https://github.com/toshi0383/TVMLKitchen/pull/138) 6 | 7 | ##### Enhancement 8 | * Add delegate parameter to prepare API 9 | Users can now have their own TVApplicationControllerDelegate. 10 | [Toshihiro Suzuki](https://github.com/toshi0383) 11 | [#140](https://github.com/toshi0383/TVMLKitchen/pull/140) 12 | 13 | ## 1.0.1 14 | ##### Enhancement 15 | * Add @escaping to SearchRecipe.filterSearchText(_:) 16 | [Flavia Bond](https://github.com/FlaviaBondJamesBondsHusband) 17 | [#125](https://github.com/toshi0383/TVMLKitchen/pull/125) 18 | 19 | ## 1.0.0 20 | ##### Breaking & Enhancement 21 | * Swift3 Support 22 | [Toshihiro Suzuki](https://github.com/toshi0383) 23 | [#119](https://github.com/toshi0383/TVMLKitchen/pull/119) 24 | 25 | * Add @discardableResult to Kitchen.prepare(_:) 26 | [Toshihiro Suzuki](https://github.com/toshi0383) 27 | [#97](https://github.com/toshi0383/TVMLKitchen/pull/97) 28 | 29 | ## 0.9.6 30 | ##### Bugfix 31 | * Improve verify reliability 32 | [Toshihiro Suzuki](https://github.com/toshi0383) 33 | [#116](https://github.com/toshi0383/TVMLKitchen/pull/116) 34 | 35 | ##### Enhancement 36 | * Add Result Handler to serve(urlString:...) 37 | [Toshihiro Suzuki](https://github.com/toshi0383) 38 | [#114](https://github.com/toshi0383/TVMLKitchen/pull/114) 39 | 40 | ## 0.9.5 41 | ##### Bugfix 42 | * [Multi Window] Fixed crashing bug when emptying Kitchen.navigationController during Kitchen.window's presentation 43 | [Toshihiro Suzuki](https://github.com/toshi0383) 44 | [#111](https://github.com/toshi0383/TVMLKitchen/pull/111) 45 | 46 | ## 0.9.4 47 | ##### Breaking 48 | * [Multi Window Support] Improve Window Transition Animation 49 | [Toshihiro Suzuki](https://github.com/toshi0383) 50 | [#108](https://github.com/toshi0383/TVMLKitchen/pull/108) 51 | 52 | ## 0.9.3 53 | ##### Enhancement 54 | * [Multi Window Support] Reuse UIViewController 55 | [Toshihiro Suzuki](https://github.com/toshi0383) 56 | [#107](https://github.com/toshi0383/TVMLKitchen/pull/107) 57 | 58 | ## 0.9.2 59 | ##### New Feature 60 | * XML Validate API 61 | [Toshihiro Suzuki](https://github.com/toshi0383) 62 | [#106](https://github.com/toshi0383/TVMLKitchen/pull/106) 63 | 64 | * [Multiple UIWindow Support] Add kitchenWindowWillBecomeVisible callback 65 | [Toshihiro Suzuki](https://github.com/toshi0383) 66 | [#105](https://github.com/toshi0383/TVMLKitchen/pull/105) 67 | 68 | ##### Breaking 69 | * Multiple UIWindow Support: Remove UIWindow parameter from didRedirectToWindow callback 70 | [Toshihiro Suzuki](https://github.com/toshi0383) 71 | [#104](https://github.com/toshi0383/TVMLKitchen/pull/104) 72 | 73 | ## 0.9.1 74 | ##### New Feature 75 | * Multiple UIWindow Support for Kitchen.serve(urlString:) 76 | [Toshihiro Suzuki](https://github.com/toshi0383) 77 | [#100](https://github.com/toshi0383/TVMLKitchen/pull/100) 78 | 79 | ## 0.9.0 80 | ##### New Feature 81 | * Multiple UIWindow Support 82 | [Toshihiro Suzuki](https://github.com/toshi0383) 83 | [#98](https://github.com/toshi0383/TVMLKitchen/pull/98) 84 | 85 | ## 0.8.1 86 | ##### Bugfix 87 | * Reloaded documents now respond with actions. 88 | [Toshihiro Suzuki](https://github.com/toshi0383) 89 | [#93](https://github.com/toshi0383/TVMLKitchen/pull/93) 90 | 91 | ## 0.8.0 92 | ##### New Feature 93 | * Tab Reloading 94 | [Toshihiro Suzuki](https://github.com/toshi0383) 95 | [#90](https://github.com/toshi0383/TVMLKitchen/pull/90) 96 | 97 | ##### Breaking 98 | * Remove deprecated APIs 99 | [Toshihiro Suzuki](https://github.com/toshi0383) 100 | [#91](https://github.com/toshi0383/TVMLKitchen/pull/91) 101 | 102 | ## 0.7.2 103 | ##### New Feature 104 | * Make LoadingRecipe public 105 | [Lukas Kuster](https://github.com/lukaskuster) 106 | [#88](https://github.com/toshi0383/TVMLKitchen/pull/88) 107 | 108 | ## 0.7.1 109 | ##### New Feature 110 | * Replace ((MAIN_BUNDLE_URL)) with main bundle path on Kitchen.serve(xmlFile:) 111 | [Toshihiro Suzuki](https://github.com/toshi0383) 112 | [#83](https://github.com/toshi0383/TVMLKitchen/pull/83) 113 | 114 | ## 0.7.0 115 | ##### Breaking 116 | * Add PresentationTypes with Loading Indicators 117 | [Toshihiro Suzuki](https://github.com/toshi0383) 118 | [#67](https://github.com/toshi0383/TVMLKitchen/issues/67) 119 | [#76](https://github.com/toshi0383/TVMLKitchen/issues/76) 120 | Note: Loading indicator will not be shown unless presentationType is set to `DefaultWithLoadingIndicator`. 121 | 122 | ##### Enhancements 123 | * Presenting the tabBar from an Action 124 | [Anthony](https://github.com/anthonycastelli) 125 | [#68](https://github.com/toshi0383/TVMLKitchen/pull/68) 126 | 127 | * Handle TabItem's handler() when presented via serve(recipe:) method. 128 | [Toshihiro Suzuki](https://github.com/toshi0383) 129 | [#72](https://github.com/toshi0383/TVMLKitchen/issues/72) 130 | 131 | ## 0.6.2 132 | ##### Enhancements 133 | * Add support for Swift 2.2 134 | [Anthony](https://github.com/anthonycastelli) 135 | [#64](https://github.com/toshi0383/TVMLKitchen/pull/64) 136 | 137 | ## 0.6.1 138 | ##### Bugfix 139 | * Fix CocoaPods Podspec 140 | [Stephen Radford](https://github.com/steve228uk) 141 | [#63](https://github.com/toshi0383/TVMLKitchen/pull/63) 142 | 143 | ## 0.6.0: Sommelier 144 | ##### New Feature 145 | * SearchRecipe 146 | [Toshihiro Suzuki](https://github.com/toshi0383) 147 | [#56](https://github.com/toshi0383/TVMLKitchen/issues/56) 148 | 149 | ## 0.5.0: HTTP Kitchen 150 | ##### Breaking 151 | * Remove Kitchen.serve(jsFile:) API 152 | [Toshihiro Suzuki](https://github.com/toshi0383) 153 | [#46](https://github.com/toshi0383/TVMLKitchen/issues/46) 154 | 155 | ##### New Feature 156 | * Custom HTTP Header 157 | [Toshihiro Suzuki](https://github.com/toshi0383) 158 | [#44](https://github.com/toshi0383/TVMLKitchen/pull/44) 159 | 160 | ##### Enhancements 161 | * Cookbook Configuration Object 162 | [Toshihiro Suzuki](https://github.com/toshi0383) 163 | [#42](https://github.com/toshi0383/TVMLKitchen/pull/42) 164 | 165 | ##### Bugfix 166 | * Dismiss modal before new presentation 167 | [Stephen Radford](https://github.com/steve228uk) 168 | [#43](https://github.com/toshi0383/TVMLKitchen/pull/43) 169 | 170 | * Fix AlertRecipe 171 | [Toshihiro Suzuki](https://github.com/toshi0383) 172 | [#52](https://github.com/toshi0383/TVMLKitchen/pull/52) 173 | 174 | ## 0.4.1: Easter Egg 175 | ##### Enhancements 176 | * @exported import TVMLKit 177 | [Toshihiro Suzuki](https://github.com/toshi0383) 178 | [#39](https://github.com/toshi0383/TVMLKitchen/pull/39) 179 | 180 | ## 0.4.0: Chef Stephen Radford 181 | ##### New Feature 182 | 183 | * Add Tab Bar Support 184 | [Stephen Radford](https://github.com/steve228uk) 185 | [#31](https://github.com/toshi0383/TVMLKitchen/pull/31) 186 | 187 | * Add Modal Support 188 | [Stephen Radford](https://github.com/steve228uk) 189 | [#26](https://github.com/toshi0383/TVMLKitchen/pull/26) 190 | 191 | * Add Alert Recipes 192 | [Stephen Radford](https://github.com/steve228uk) 193 | [#31](https://github.com/toshi0383/TVMLKitchen/pull/31) 194 | 195 | ##### Enhancements 196 | 197 | * Add Cocoapods Podspec 198 | [Stephen Radford](https://github.com/steve228uk) 199 | [#29](https://github.com/toshi0383/TVMLKitchen/pull/29) 200 | 201 | ## 0.3.0: Player 202 | ##### New Feature 203 | * Introduce playActionIDHandler 204 | [Toshihiro Suzuki](https://github.com/toshi0383) 205 | [#23](https://github.com/toshi0383/TVMLKitchen/issues/23) 206 | 207 | ##### Enhancements 208 | * Remove debug function injection 209 | [Toshihiro Suzuki](https://github.com/toshi0383) 210 | [#3](https://github.com/toshi0383/TVMLKitchen/issues/3) 211 | 212 | ## 0.2.2: Zuppa di SwiftLint 213 | ##### Bugfix 214 | * Fix unwanted SwiftLint Error on `carthage update` 215 | [Toshihiro Suzuki](https://github.com/toshi0383) 216 | [#16](https://github.com/toshi0383/TVMLKitchen/pull/16) 217 | 218 | ## 0.2.1: SwiftLint Burger 219 | ##### Bugfix 220 | * Fix SwiftLint Error on `carthage update` 221 | [Toshihiro Suzuki](https://github.com/toshi0383) 222 | [#12](https://github.com/toshi0383/TVMLKitchen/issues/12) 223 | 224 | ## 0.2.0: URL Kitchen 225 | ##### Enhancements 226 | * Open template from URL String 227 | [Toshihiro Suzuki](https://github.com/toshi0383) 228 | [#8](https://github.com/toshi0383/TVMLKitchen/pull/8) 229 | 230 | ##### Bugfix 231 | * actionID had been overwritten everytime `Kitchen.serve()` with actionIDHandler. 232 | [Toshihiro Suzuki](https://github.com/toshi0383) 233 | [#9](https://github.com/toshi0383/TVMLKitchen/issues/9) 234 | 235 | ## 0.1.2: Hot chili pepper 236 | ##### Breaking Change 237 | * Improve error handling 238 | [Toshihiro Suzuki](https://github.com/toshi0383) 239 | [#4](https://github.com/toshi0383/TVMLKitchen/issues/4) 240 | 241 | ##### Bug Fixes 242 | * Fixed kitchen.js runtime error 243 | [Toshihiro Suzuki](https://github.com/toshi0383) 244 | 245 | ## 0.1.1: Recipe 246 | * DefaultTheme and BlackTheme 247 | * Customizable Recipe Theme Interface 248 | [Toshihiro Suzuki](https://github.com/toshi0383) 249 | 250 | ## 0.1.0: Bonapetit 251 | First Version 252 | -------------------------------------------------------------------------------- /SampleRecipe/Base.lproj/ViewController.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 | 35 | 43 | 44 | 45 | 54 | 63 | 72 | 81 | 90 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Sources/Kitchen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kitchen.swift 3 | // TVMLKitchen 4 | // 5 | // Created by toshi0383 on 12/28/15. 6 | // Copyright © 2015 toshi0383. All rights reserved. 7 | // 8 | 9 | @_exported import TVMLKit 10 | 11 | public enum Result { 12 | case success(T) 13 | case failure(E) 14 | } 15 | 16 | public typealias JavaScriptEvaluationHandler = (TVApplicationController, JSContext) -> Void 17 | public typealias KitchenErrorHandler = (Error) -> Void 18 | public typealias KitchenActionIDHandler = ((String) -> Void) 19 | public typealias KitchenTabItemHandler = ((Int) -> Void) 20 | 21 | let kitchenErrorDomain = "jp.toshi0383.TVMLKitchen.error" 22 | 23 | open class Kitchen: NSObject { 24 | /// singleton instance 25 | fileprivate static let sharedKitchen = Kitchen() 26 | fileprivate weak var _delegate: TVApplicationControllerDelegate? 27 | fileprivate static weak var redirectWindow: UIWindow? 28 | fileprivate static var animatedWindowTransition: Bool = false 29 | fileprivate static var _navigationControllerDelegateWillShowCount = 0 30 | fileprivate static var willRedirectToWindow: (() -> ())? 31 | 32 | fileprivate var evaluateAppJavaScriptInContext: JavaScriptEvaluationHandler? 33 | 34 | fileprivate var kitchenErrorHandler: KitchenErrorHandler? 35 | 36 | fileprivate static let defaultErrorHandler: KitchenErrorHandler = { error in 37 | let alert = UIAlertController(title: "Oops, something's wrong.", 38 | message: "\(error.localizedDescription)", 39 | preferredStyle: UIAlertControllerStyle.alert) 40 | let ok = UIAlertAction(title: "OK", style: .cancel, handler: nil) 41 | alert.addAction(ok) 42 | DispatchQueue.main.async { 43 | Kitchen.navigationController.present(alert, animated: true, completion: nil) 44 | } 45 | } 46 | 47 | fileprivate var window: UIWindow 48 | 49 | fileprivate var appController: TVApplicationController! 50 | 51 | fileprivate var actionIDHandler: KitchenActionIDHandler? 52 | fileprivate var playActionIDHandler: KitchenActionIDHandler? 53 | fileprivate var cookbook: Cookbook! 54 | 55 | open static var mainBundlePath: String! 56 | 57 | override init() { 58 | window = UIWindow(frame: UIScreen.main.bounds) 59 | super.init() 60 | } 61 | 62 | } 63 | 64 | 65 | // MARK: - Public API 66 | public enum KitchenError: Error { 67 | case tvmlDecodeError, tvmlurlNetworkError(Error?) 68 | case invalidTVMLURL 69 | } 70 | 71 | extension Kitchen { 72 | 73 | fileprivate static var _rootViewController: UIViewController = { 74 | let vc = UIViewController() 75 | vc.view.alpha = 0.0 76 | return vc 77 | }() 78 | 79 | // MARK: verify 80 | 81 | /// Verify xmlString syntax. 82 | /// This function actually calls `DOMParser#parseFromString` to verify syntax. 83 | /// - parameter xmlString: 84 | /// - throws: KitchenError.TVMLDecodeError 85 | public static func verify(_ xmlString: String) throws { 86 | if !isValidXMLString(xmlString) { 87 | throw KitchenError.tvmlDecodeError 88 | } 89 | } 90 | 91 | // MARK: serve 92 | public static func serve(xmlString: String, type: PresentationType = .default) { 93 | openTVMLTemplateFromXMLString(xmlString, type: type) 94 | } 95 | 96 | public static func serve(xmlFile: String, type: PresentationType = .default) { 97 | do { 98 | try openTVMLTemplateFromXMLFile(xmlFile, type: type) 99 | } catch let error as NSError { 100 | sharedKitchen.kitchenErrorHandler?(error) 101 | } 102 | } 103 | 104 | static func _kitchenWindowWillBecomeVisible(_ redirectWindow: UIWindow) { 105 | window.alpha = 0.0 106 | UIView.animate( 107 | withDuration: 0.3, 108 | animations: { 109 | window.alpha = 1.0 110 | }, 111 | completion: { 112 | _ in 113 | redirectWindow.alpha = 0.0 114 | } 115 | ) 116 | } 117 | 118 | static func _willRedirectToWindow(_ redirectWindow: UIWindow) { 119 | redirectWindow.alpha = 0.0 120 | UIView.animate( 121 | withDuration: 0.3, 122 | animations: { 123 | redirectWindow.alpha = 1.0 124 | }, 125 | completion: { 126 | _ in 127 | window.alpha = 0.0 128 | } 129 | ) 130 | } 131 | 132 | /// Serve TVML with xmlString 133 | /// Calls `redirectWindow.makeKeyAndVisible()` when the TVML is dismissing. 134 | /// - parameter urlString: 135 | /// - parameter type: 136 | /// - parameter redirectWindow: UIWindow. 137 | /// Expected to be the parent window of Native Views(not Kitchen.window) 138 | /// - parameter animatedWindowTransition: If true, ignores Redirect callbacks. 139 | /// - parameter kitchenWindowWillBecomeVisible: Redirect Callback 140 | /// - parameter willRedirectToWindow: Redirect Callback 141 | public static func serve(xmlString: String, 142 | type: PresentationType = .default, redirectWindow: UIWindow, 143 | animatedWindowTransition: Bool = false, 144 | kitchenWindowWillBecomeVisible: (() -> ())? = nil, 145 | willRedirectToWindow: (() -> ())? = nil) 146 | { 147 | Kitchen._navigationControllerDelegateWillShowCount = 0 148 | let vc = _rootViewController 149 | Kitchen.navigationController.setViewControllers([vc], animated: true) 150 | Kitchen.navigationController.delegate = sharedKitchen 151 | Kitchen.willRedirectToWindow = willRedirectToWindow 152 | Kitchen.redirectWindow = redirectWindow 153 | Kitchen.animatedWindowTransition = animatedWindowTransition 154 | if animatedWindowTransition { 155 | _kitchenWindowWillBecomeVisible(redirectWindow) 156 | } else { 157 | kitchenWindowWillBecomeVisible?() 158 | } 159 | Kitchen.serve(xmlString: xmlString, type: type) 160 | Kitchen.window.makeKeyAndVisible() 161 | } 162 | 163 | /// Serve TVML with urlString 164 | /// Calls `redirectWindow.makeKeyAndVisible()` when the TVML is dismissing. 165 | /// 166 | /// Redirects to redirectWindow on error by default. 167 | /// You can overwrite that behavior by setting resultHandler parameter. 168 | /// 169 | /// - parameter urlString: 170 | /// - parameter type: 171 | /// - parameter redirectWindow: UIWindow. 172 | /// Expected to be the parent window of Native Views(not Kitchen.window) 173 | /// - parameter animatedWindowTransition: If true, ignores Redirect callbacks. 174 | /// - parameter kitchenWindowWillBecomeVisible: Redirect Callback 175 | /// - parameter willRedirectToWindow: Redirect Callback 176 | /// - parameter resultHandler: Result Handler 177 | public static func serve(urlString: String, 178 | type: PresentationType = .default, redirectWindow: UIWindow, 179 | animatedWindowTransition: Bool = false, 180 | kitchenWindowWillBecomeVisible: (() -> ())? = nil, 181 | willRedirectToWindow: (() -> ())? = nil, 182 | resultHandler: ((Result) -> String?)? = nil) 183 | { 184 | Kitchen._navigationControllerDelegateWillShowCount = 0 185 | let vc = _rootViewController 186 | Kitchen.navigationController.setViewControllers([vc], animated: true) 187 | Kitchen.navigationController.delegate = sharedKitchen 188 | Kitchen.willRedirectToWindow = willRedirectToWindow 189 | Kitchen.redirectWindow = redirectWindow 190 | Kitchen.animatedWindowTransition = animatedWindowTransition 191 | 192 | 193 | func transitionToKitchen() { 194 | DispatchQueue.main.async { 195 | if animatedWindowTransition { 196 | _kitchenWindowWillBecomeVisible(redirectWindow) 197 | } else { 198 | kitchenWindowWillBecomeVisible?() 199 | } 200 | Kitchen.window.makeKeyAndVisible() 201 | } 202 | } 203 | func redirectBack() { 204 | DispatchQueue.main.async { 205 | redirectWindow.makeKeyAndVisible() 206 | } 207 | } 208 | if let resultHandler = resultHandler { 209 | Kitchen.serve(urlString: urlString, type: type, resultHandler: resultHandler) 210 | } else { 211 | Kitchen.serve(urlString: urlString, type: type) { 212 | result in 213 | switch result { 214 | case .success(let xmlString): 215 | do { 216 | try Kitchen.verify(xmlString) 217 | transitionToKitchen() 218 | return xmlString 219 | } catch { 220 | redirectBack() 221 | return nil 222 | } 223 | case .failure: 224 | redirectBack() 225 | return nil 226 | } 227 | } 228 | } 229 | } 230 | 231 | public static func serve 232 | (urlString: String, type: PresentationType = .default, 233 | resultHandler: ((Result) -> String?)? = nil) { 234 | Kitchen.appController.evaluate(inJavaScriptContext: { 235 | context in 236 | let js = "showLoadingIndicatorForType(\(type.rawValue))" 237 | context.evaluateScript(js) 238 | }, completion: nil) 239 | sharedKitchen.sendRequest(urlString) { 240 | result in 241 | if let resultHandler = resultHandler { 242 | if let xmlString = resultHandler(result) { 243 | openTVMLTemplateFromXMLString(xmlString, type: type) 244 | } 245 | } else { 246 | switch result { 247 | case .success(let xmlString): 248 | openTVMLTemplateFromXMLString(xmlString, type: type) 249 | case .failure(let error): 250 | if case KitchenError.tvmlurlNetworkError(let e) = error { 251 | if let e = e { 252 | sharedKitchen.kitchenErrorHandler?(e) 253 | } 254 | } 255 | } 256 | } 257 | } 258 | } 259 | 260 | public static func serve(recipe: R) { 261 | if let recipe = recipe as? SearchRecipe { 262 | sharedKitchen.cookbook.searchRecipe = recipe 263 | } 264 | if let recipe = recipe as? KitchenTabBar { 265 | sharedKitchen.cookbook.tabChangedHandler = recipe.tabChanged 266 | } 267 | openTVMLTemplateFromXMLString(recipe.xmlString, type: recipe.presentationType) 268 | } 269 | 270 | // MARK: reloadTab 271 | public static func reloadTab(atIndex index: Int, recipe: R) { 272 | _reloadTab(atIndex: index, xmlString: recipe.xmlString) 273 | } 274 | 275 | public static func reloadTab(atIndex index: Int, xmlFile: String) { 276 | do { 277 | _reloadTab(atIndex: index, xmlString: try xmlStringFromMainBundle(xmlFile)) 278 | } catch let error as NSError { 279 | sharedKitchen.kitchenErrorHandler?(error) 280 | } 281 | } 282 | 283 | public static func reloadTab(atIndex index: Int, urlString: String) { 284 | sharedKitchen.sendRequest(urlString) { 285 | result in 286 | switch result { 287 | case .success(let xmlString): 288 | _reloadTab(atIndex: index, xmlString: xmlString) 289 | case .failure(let error): 290 | if case KitchenError.tvmlurlNetworkError(let e) = error { 291 | if let e = e { 292 | sharedKitchen.kitchenErrorHandler?(e) 293 | } 294 | } 295 | } 296 | } 297 | } 298 | 299 | public static func reloadTab(atIndex index: Int, xmlString: String) { 300 | _reloadTab(atIndex: index, xmlString: xmlString) 301 | } 302 | 303 | // MARK: dismissModal 304 | public static func dismissModal() { 305 | dismissTVMLModal() 306 | } 307 | 308 | // MARK: bundle 309 | public static func bundle() -> Bundle { 310 | return Bundle(for: self) 311 | } 312 | } 313 | 314 | 315 | // MARK: Network Request 316 | 317 | extension Kitchen { 318 | internal func sendRequest(_ urlString: String, responseHandler: @escaping (Result) -> ()) { 319 | guard let url = URL(string: urlString) else { 320 | print("Invalid URL") 321 | responseHandler(.failure( 322 | KitchenError.invalidTVMLURL 323 | )) 324 | return 325 | } 326 | 327 | /// Create Request 328 | let req = NSMutableURLRequest(url: url) 329 | 330 | /// Custom Headers 331 | for (k, v) in cookbook.httpHeaders { 332 | req.setValue(v, forHTTPHeaderField: k) 333 | } 334 | 335 | /// Session Handler 336 | let session = URLSession(configuration: URLSessionConfiguration.default) 337 | let task = session.dataTask(with: req as URLRequest, completionHandler: { 338 | [unowned self] data, res, error in 339 | if let error = error { 340 | responseHandler(.failure(KitchenError.tvmlurlNetworkError(error))) 341 | } 342 | 343 | /// Call user-defined responseObjectHander if no errors. 344 | if let res = res as? HTTPURLResponse, 345 | let resume = self.cookbook.responseObjectHandler?(res) 346 | , resume == false 347 | { 348 | return 349 | } 350 | 351 | if let data = data, 352 | let xml = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String 353 | { 354 | responseHandler(Result.success(xml)) 355 | } else { 356 | responseHandler(Result.failure( 357 | KitchenError.tvmlurlNetworkError(nil) 358 | )) 359 | } 360 | }) 361 | task.resume() 362 | } 363 | } 364 | 365 | // MARK: window 366 | extension Kitchen { 367 | 368 | public static var window: UIWindow { 369 | return sharedKitchen.window 370 | } 371 | 372 | } 373 | 374 | // MARK: UINavigationControllerDelegate 375 | extension Kitchen: UINavigationControllerDelegate { 376 | public func navigationController( 377 | _ navigationController: UINavigationController, 378 | willShow viewController: UIViewController, animated: Bool) 379 | { 380 | // Workaround: This delegate is called on presenting, too.. 381 | // We want to handle this only on dismissing. 382 | guard Kitchen._navigationControllerDelegateWillShowCount == 1 else { 383 | Kitchen._navigationControllerDelegateWillShowCount = 1 384 | return 385 | } 386 | if Kitchen.navigationController.viewControllers.count == 0 || 387 | viewController == Kitchen.navigationController.viewControllers[0] { 388 | if let redirectWindow = Kitchen.redirectWindow { 389 | if Kitchen.animatedWindowTransition { 390 | Kitchen._willRedirectToWindow(redirectWindow) 391 | } else { 392 | Kitchen.willRedirectToWindow?() 393 | } 394 | Kitchen.willRedirectToWindow = nil 395 | redirectWindow.makeKeyAndVisible() 396 | } 397 | } 398 | } 399 | } 400 | // MARK: TVApplicationControllerDelegate 401 | extension Kitchen { 402 | 403 | public static var appController: TVApplicationController { 404 | return sharedKitchen.appController 405 | } 406 | 407 | public static var navigationController: UINavigationController { 408 | return sharedKitchen.appController.navigationController 409 | } 410 | } 411 | 412 | extension Kitchen { 413 | 414 | /** 415 | create TVApplicationControllerContext using launchOptions 416 | 417 | Supposed to be called in application:didFinishedLaunchingWithOptions: 418 | in UIApplicationDelegate of your @UIApplicationMain . 419 | - parameter cookbook: a Cookbook configuration object 420 | - parameter delegate: TVApplicationControllerDelegate 421 | - returns: If launch process was successfully or not. 422 | */ 423 | @discardableResult 424 | public static func prepare(_ cookbook: Cookbook, delegate: TVApplicationControllerDelegate? = nil) -> Bool { 425 | sharedKitchen._delegate = delegate 426 | sharedKitchen.cookbook = cookbook 427 | sharedKitchen.window = UIWindow(frame: UIScreen.main.bounds) 428 | sharedKitchen.evaluateAppJavaScriptInContext = cookbook.evaluateAppJavaScriptInContext 429 | 430 | /// Create the TVApplicationControllerContext 431 | let appControllerContext = TVApplicationControllerContext() 432 | 433 | let javaScriptURL = Bundle(for: self).url(forResource: "kitchen", withExtension: "js")! 434 | appControllerContext.javaScriptApplicationURL = javaScriptURL 435 | appControllerContext.launchOptions[UIApplicationLaunchOptionsKey.url.rawValue] = javaScriptURL 436 | 437 | /// Cutting `kitchen.js` off 438 | let TVBaseURL = javaScriptURL.deletingLastPathComponent() 439 | 440 | /// Define framework bundle URL 441 | appControllerContext.launchOptions["BASEURL"] = TVBaseURL.absoluteString 442 | let info = Bundle(for: self).infoDictionary! 443 | let bundleid = info[kCFBundleIdentifierKey as String]! 444 | appControllerContext.launchOptions[UIApplicationLaunchOptionsKey.sourceApplication.rawValue] = bundleid 445 | 446 | /// Define mainBundle URL 447 | mainBundlePath = Bundle.main.bundleURL.absoluteString 448 | appControllerContext.launchOptions["MAIN_BUNDLE_URL"] = mainBundlePath 449 | 450 | if let launchOptions = cookbook.launchOptions as? [String: AnyObject] { 451 | for (kind, value) in launchOptions { 452 | appControllerContext.launchOptions[kind] = value 453 | } 454 | } 455 | 456 | sharedKitchen.appController = TVApplicationController(context: appControllerContext, 457 | window: sharedKitchen.window, delegate: sharedKitchen) 458 | 459 | /// Must be place this statement after appController is initialized 460 | sharedKitchen.kitchenErrorHandler = cookbook.onError 461 | sharedKitchen.actionIDHandler = cookbook.actionIDHandler 462 | sharedKitchen.playActionIDHandler = cookbook.playActionIDHandler 463 | 464 | return true 465 | } 466 | 467 | /// Calls TVApplicationController.stop() 468 | public static func stop() { 469 | sharedKitchen.appController.stop() 470 | } 471 | 472 | } 473 | 474 | // MARK: TVApplicationControllerDelegate 475 | extension Kitchen: TVApplicationControllerDelegate { 476 | 477 | public func appController(_ appController: TVApplicationController, 478 | didFinishLaunching options: [String: Any]?) 479 | { 480 | // Call user's delegate 481 | _delegate?.appController?(appController, didFinishLaunching: options) 482 | } 483 | 484 | public func appController(_ appController: TVApplicationController, 485 | didFail error: Error) 486 | { 487 | // Call user's delegate 488 | _delegate?.appController?(appController, didFail: error) 489 | self.kitchenErrorHandler?(error as NSError) 490 | } 491 | 492 | public func appController(_ appController: TVApplicationController, 493 | didStop options: [String: Any]?) 494 | { 495 | // Call user's delegate 496 | _delegate?.appController?(appController, didStop: options) 497 | } 498 | 499 | public func appController(_ appController: TVApplicationController, 500 | evaluateAppJavaScriptIn jsContext: JSContext) 501 | { 502 | // Call user's delegate 503 | _delegate?.appController?(appController, evaluateAppJavaScriptIn: jsContext) 504 | 505 | if let playActionIDHandler = playActionIDHandler { 506 | let playActionIDHandler: @convention(block) (String) -> Void = { actionID in 507 | playActionIDHandler(actionID) 508 | } 509 | jsContext.setObject(unsafeBitCast(playActionIDHandler, to: AnyObject.self), 510 | forKeyedSubscript: "playActionIDHandler" as (NSCopying & NSObjectProtocol)!) 511 | } 512 | 513 | if let actionIDHandler = actionIDHandler { 514 | let actionIDHandler: @convention(block) (String) -> Void = { actionID in 515 | actionIDHandler(actionID) 516 | } 517 | jsContext.setObject(unsafeBitCast(actionIDHandler, to: AnyObject.self), 518 | forKeyedSubscript: "actionIDHandler" as (NSCopying & NSObjectProtocol)!) 519 | } 520 | 521 | // Add loadTemplateFromURL 522 | let loadTemplateFromURL: @convention(block) (String, String) -> Void = 523 | { (url, presentationType) -> () in 524 | self.sendRequest(url) {[unowned self] result in 525 | switch result { 526 | case .success(let xmlString): 527 | openTVMLTemplateFromXMLString( 528 | xmlString, 529 | type: PresentationType(string: presentationType) 530 | ) 531 | case .failure(let error): 532 | if case KitchenError.tvmlurlNetworkError(let e) = error { 533 | if let e = e { 534 | self.cookbook.onError?(e) 535 | } 536 | } 537 | } 538 | } 539 | } 540 | jsContext.setObject(unsafeBitCast(loadTemplateFromURL, to: AnyObject.self), 541 | forKeyedSubscript: "loadTemplateFromURL" as (NSCopying & NSObjectProtocol)!) 542 | 543 | let filterSearchTextBlock: @convention(block) (String, JSValue) -> () = 544 | {[unowned self] (text, callback) in 545 | self.cookbook.searchRecipe?.filterSearchText(text) { string in 546 | if callback.isObject { 547 | callback.call(withArguments: [string]) 548 | } 549 | } 550 | } 551 | jsContext.setObject(unsafeBitCast(filterSearchTextBlock, to: AnyObject.self), 552 | forKeyedSubscript: "filterSearchText" as (NSCopying & NSObjectProtocol)!) 553 | 554 | let loadingTemplate: @convention(block) (Void) -> String = 555 | { 556 | return LoadingRecipe().xmlString 557 | } 558 | jsContext.setObject(unsafeBitCast(loadingTemplate, to: AnyObject.self), 559 | forKeyedSubscript: "loadingTemplate" as (NSCopying & NSObjectProtocol)!) 560 | 561 | 562 | // Add the tab bar handler 563 | let tabBarHandler: @convention(block) (Int) -> Void = { 564 | index in 565 | self.cookbook.tabChangedHandler?(index) 566 | } 567 | jsContext.setObject(unsafeBitCast(tabBarHandler, to: AnyObject.self), 568 | forKeyedSubscript: "tabBarHandler" as (NSCopying & NSObjectProtocol)!) 569 | 570 | // Verify Error 571 | let verifyXMLStringComplete: @convention(block) (Bool) -> () = { 572 | success in 573 | verifyMediator.error = !success 574 | } 575 | jsContext.setObject(unsafeBitCast(verifyXMLStringComplete, to: AnyObject.self), 576 | forKeyedSubscript: "verifyXMLStringComplete" as (NSCopying & NSObjectProtocol)!) 577 | 578 | // Done 579 | self.evaluateAppJavaScriptInContext?(appController, jsContext) 580 | } 581 | } 582 | --------------------------------------------------------------------------------